Dùng đồng thời nhiều luồng máy ảnh

Lưu ý: Trang này đề cập đến gói Camera2. Bạn nên sử dụng CameraX, trừ phi ứng dụng yêu cầu các tính năng cụ thể ở cấp thấp trong Camera2. Cả CameraX và Camera2 đều hỗ trợ Android 5.0 (API cấp 21) trở lên.

Một ứng dụng máy ảnh có thể sử dụng đồng thời nhiều luồng khung hình. Trong một số trường hợp, các luồng khác nhau thậm chí yêu cầu độ phân giải khung hình hoặc định dạng pixel khác. Sau đây là một số trường hợp sử dụng thường gặp:

  • Quay video: một luồng để xem trước, một luồng khác được mã hoá và lưu vào tệp.
  • Quét mã vạch: một luồng để xem trước, một luồng để phát hiện mã vạch.
  • Chụp ảnh điện toán: một luồng để xem trước, một luồng khác để phát hiện khuôn mặt/cảnh.

Chi phí hiệu suất không nhỏ khi xử lý khung hình và chi phí sẽ được nhân lên khi xử lý quy trình hoặc luồng song song.

Các tài nguyên như CPU, GPU và DSP có thể tận dụng khả năng xử lý lại của khung, nhưng các tài nguyên như bộ nhớ sẽ phát triển tuyến tính.

Nhiều mục tiêu mỗi yêu cầu

Bạn có thể kết hợp nhiều luồng máy ảnh thành một CameraCaptureRequest duy nhất. Đoạn mã sau đây minh hoạ cách thiết lập phiên máy ảnh với một luồng cho bản xem trước của máy ảnh và một luồng khác để xử lý hình ảnh:

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback

// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD
val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
val combinedRequest = session.device.createCaptureRequest(requestTemplate)

// Link the Surface targets with the combined request
combinedRequest.addTarget(previewSurface)
combinedRequest.addTarget(imReaderSurface)

// In this simple case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request
session.setRepeatingRequest(combinedRequest.build(), null, null)

Java

CameraCaptureSession session = …;  // from CameraCaptureSession.StateCallback

// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD
        CaptureRequest.Builder combinedRequest = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

// Link the Surface targets with the combined request
        combinedRequest.addTarget(previewSurface);
        combinedRequest.addTarget(imReaderSurface);

// In this simple case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request
        session.setRepeatingRequest(combinedRequest.build(), null, null);

Nếu bạn định cấu hình các nền tảng mục tiêu đúng cách, mã này sẽ chỉ tạo các luồng đáp ứng FPS tối thiểu được xác định bởi StreamComfigurationMap.GetOutputMinFrameDuration(int, Size)StreamComfigurationMap.GetOutputStallDuration(int, Size). Hiệu suất thực tế thay đổi tuỳ theo thiết bị, mặc dù Android đưa ra một số đảm bảo để hỗ trợ các kết hợp cụ thể tuỳ thuộc vào 3 biến: loại đầu ra, kích thước đầu racấp độ phần cứng.

Việc sử dụng tổ hợp các biến không được hỗ trợ có thể hoạt động ở tốc độ khung hình thấp. Nếu không, việc này sẽ kích hoạt một trong các lệnh gọi lại không thành công. Tài liệu về createCaptureSession mô tả những gì được đảm bảo hoạt động.

Loại đầu ra

Loại đầu ra đề cập đến định dạng mã hoá khung. Các giá trị có thể có là PRIV, YUV, JPEG và RAW. Tài liệu về createCaptureSession mô tả các kiểu dữ liệu đó.

Khi chọn loại đầu ra của ứng dụng, nếu mục tiêu là tối đa hoá khả năng tương thích, hãy sử dụng ImageFormat.YUV_420_888 để phân tích khung hình và ImageFormat.JPEG cho hình ảnh tĩnh. Đối với các tình huống xem trước và quay video, có thể bạn sẽ sử dụng SurfaceView, TextureView, MediaRecorder, MediaCodec hoặc RenderScript.Allocation. Trong những trường hợp đó, đừng chỉ định định dạng hình ảnh. Để có khả năng tương thích, kích thước này sẽ được tính là ImageFormat.PRIVATE, bất kể định dạng thực tế được sử dụng nội bộ. Để truy vấn các định dạng mà thiết bị hỗ trợ dựa trên CameraCharacteristics, hãy dùng mã sau:

Kotlin

val characteristics: CameraCharacteristics = ...
val supportedFormats = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).outputFormats

Java

CameraCharacteristics characteristics = …;
        int[] supportedFormats = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputFormats();

Kích thước đầu ra

Tất cả kích thước đầu ra có sẵn đều được liệt kê trong StreamConfigurationMap.getOutputSizes(), nhưng chỉ có hai kích thước liên quan đến khả năng tương thích: PREVIEWMAXIMUM. Kích thước đóng vai trò là giới hạn trên. Nếu mục nào đó có kích thước PREVIEW hoạt động, thì mọi mục có kích thước nhỏ hơn PREVIEW cũng sẽ hoạt động. Điều này cũng đúng đối với MAXIMUM. Tài liệu về CameraDevice giải thích các kích thước này.

Kích thước đầu ra có sẵn phụ thuộc vào việc lựa chọn định dạng. Với CameraCharacteristics và một định dạng, bạn có thể truy vấn các kích thước đầu ra có sẵn như sau:

Kotlin

val characteristics: CameraCharacteristics = ...
val outputFormat: Int = ...  // such as ImageFormat.JPEG
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    .getOutputSizes(outputFormat)

Java

CameraCharacteristics characteristics = …;
        int outputFormat = …;  // such as ImageFormat.JPEG
Size[] sizes = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(outputFormat);

Trong các trường hợp sử dụng bản xem trước và ghi lại của máy ảnh, hãy dùng lớp mục tiêu để xác định các kích thước được hỗ trợ. Định dạng sẽ do khung máy ảnh xử lý:

Kotlin

val characteristics: CameraCharacteristics = ...
val targetClass: Class <T> = ...  // such as SurfaceView::class.java
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    .getOutputSizes(targetClass)

Java

CameraCharacteristics characteristics = …;
   int outputFormat = …;  // such as ImageFormat.JPEG
   Size[] sizes = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(outputFormat);

Để có kích thước MAXIMUM, hãy sắp xếp kích thước đầu ra theo khu vực và trả về kích thước lớn nhất:

Kotlin

fun <T>getMaximumOutputSize(
    characteristics: CameraCharacteristics, targetClass: Class <T>, format: Int? = null):
    Size {
  val config = characteristics.get(
      CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)

  // If image format is provided, use it to determine supported sizes; or else use target class
  val allSizes = if (format == null)
    config.getOutputSizes(targetClass) else config.getOutputSizes(format)
  return allSizes.maxBy { it.height * it.width }
}

Java

 @RequiresApi(api = Build.VERSION_CODES.N)
    <T> Size getMaximumOutputSize(CameraCharacteristics characteristics,
                                            Class <T> targetClass,
                                            Integer format) {
        StreamConfigurationMap config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

        // If image format is provided, use it to determine supported sizes; else use target class
        Size[] allSizes;
        if (format == null) {
            allSizes = config.getOutputSizes(targetClass);
        } else {
            allSizes = config.getOutputSizes(format);
        }
        return Arrays.stream(allSizes).max(Comparator.comparing(s -> s.getHeight() * s.getWidth())).get();
    }

PREVIEW là kích thước phù hợp nhất với độ phân giải màn hình của thiết bị hoặc với 1080p (1920x1080), tuỳ theo kích thước nào nhỏ hơn. Tỷ lệ khung hình có thể không khớp chính xác với tỷ lệ khung hình của màn hình. Vì vậy, bạn có thể cần áp dụng hiệu ứng hòm thư hoặc cắt ảnh cho luồng để hiển thị ở chế độ toàn màn hình. Để có kích thước xem trước phù hợp, hãy so sánh kích thước đầu ra hiện có với kích thước màn hình, đồng thời tính đến việc màn hình có thể xoay.

Mã sau đây xác định một lớp trợ giúp, SmartSize, sẽ giúp việc so sánh kích thước trở nên dễ dàng hơn một chút:

Kotlin

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
class SmartSize(width: Int, height: Int) {
    var size = Size(width, height)
    var long = max(size.width, size.height)
    var short = min(size.width, size.height)
    override fun toString() = "SmartSize(${long}x${short})"
}

/** Standard High Definition size for pictures and video */
val SIZE_1080P: SmartSize = SmartSize(1920, 1080)

/** Returns a [SmartSize] object for the given [Display] */
fun getDisplaySmartSize(display: Display): SmartSize {
    val outPoint = Point()
    display.getRealSize(outPoint)
    return SmartSize(outPoint.x, outPoint.y)
}

/**
 * Returns the largest available PREVIEW size. For more information, see:
 * https://d.android.com/reference/android/hardware/camera2/CameraDevice
 */
fun <T>getPreviewOutputSize(
        display: Display,
        characteristics: CameraCharacteristics,
        targetClass: Class <T>,
        format: Int? = null
): Size {

    // Find which is smaller: screen or 1080p
    val screenSize = getDisplaySmartSize(display)
    val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short
    val maxSize = if (hdScreen) SIZE_1080P else screenSize

    // If image format is provided, use it to determine supported sizes; else use target class
    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    if (format == null)
        assert(StreamConfigurationMap.isOutputSupportedFor(targetClass))
    else
        assert(config.isOutputSupportedFor(format))
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)

    // Get available sizes and sort them by area from largest to smallest
    val validSizes = allSizes
            .sortedWith(compareBy { it.height * it.width })
            .map { SmartSize(it.width, it.height) }.reversed()

    // Then, get the largest output size that is smaller or equal than our max size
    return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size
}

Java

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
    class SmartSize {
        Size size;
        double longSize;
        double shortSize;

        public SmartSize(Integer width, Integer height) {
            size = new Size(width, height);
            longSize = max(size.getWidth(), size.getHeight());
            shortSize = min(size.getWidth(), size.getHeight());
        }

        @Override
        public String toString() {
            return String.format("SmartSize(%sx%s)", longSize, shortSize);
        }
    }

    /** Standard High Definition size for pictures and video */
    SmartSize SIZE_1080P = new SmartSize(1920, 1080);

    /** Returns a [SmartSize] object for the given [Display] */
    SmartSize getDisplaySmartSize(Display display) {
        Point outPoint = new Point();
        display.getRealSize(outPoint);
        return new SmartSize(outPoint.x, outPoint.y);
    }

    /**
     * Returns the largest available PREVIEW size. For more information, see:
     * https://d.android.com/reference/android/hardware/camera2/CameraDevice
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    <T> Size getPreviewOutputSize(
            Display display,
            CameraCharacteristics characteristics,
            Class <T> targetClass,
            Integer format
    ){

        // Find which is smaller: screen or 1080p
        SmartSize screenSize = getDisplaySmartSize(display);
        boolean hdScreen = screenSize.longSize >= SIZE_1080P.longSize || screenSize.shortSize >= SIZE_1080P.shortSize;
        SmartSize maxSize;
        if (hdScreen) {
            maxSize = SIZE_1080P;
        } else {
            maxSize = screenSize;
        }

        // If image format is provided, use it to determine supported sizes; else use target class
        StreamConfigurationMap config = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (format == null)
            assert(StreamConfigurationMap.isOutputSupportedFor(targetClass));
        else
            assert(config.isOutputSupportedFor(format));
        Size[] allSizes;
        if (format == null) {
            allSizes = config.getOutputSizes(targetClass);
        } else {
            allSizes = config.getOutputSizes(format);
        }

        // Get available sizes and sort them by area from largest to smallest
        List <Size> sortedSizes = Arrays.asList(allSizes);
        List <SmartSize> validSizes =
                sortedSizes.stream()
                        .sorted(Comparator.comparing(s -> s.getHeight() * s.getWidth()))
                        .map(s -> new SmartSize(s.getWidth(), s.getHeight()))
                        .sorted(Collections.reverseOrder()).collect(Collectors.toList());

        // Then, get the largest output size that is smaller or equal than our max size
        return validSizes.stream()
                .filter(s -> s.longSize <= maxSize.longSize && s.shortSize <= maxSize.shortSize)
                .findFirst().get().size;
    }

Kiểm tra mức phần cứng được hỗ trợ

Để xác định các tính năng có sẵn trong thời gian chạy, hãy kiểm tra cấp độ phần cứng được hỗ trợ bằng cách sử dụng CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL.

Với đối tượng CameraCharacteristics, bạn có thể truy xuất cấp độ phần cứng bằng một câu lệnh duy nhất:

Kotlin

val characteristics: CameraCharacteristics = ...

// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
val hardwareLevel = characteristics.get(
        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)

Java

CameraCharacteristics characteristics = ...;

// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
Integer hardwareLevel = characteristics.get(
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);

Đang kết hợp tất cả các chi tiết

Với loại đầu ra, kích thước đầu ra và cấp độ phần cứng, bạn có thể xác định những tổ hợp luồng nào là hợp lệ. Biểu đồ sau đây cung cấp thông tin tổng quan nhanh về các cấu hình được CameraDevice hỗ trợ ở cấp độ phần cứng LEGACY.

Mục tiêu 1 Mục tiêu 2 Mục tiêu 3 (Các) trường hợp sử dụng mẫu
Loại Kích thước tối đa Loại Kích thước tối đa Loại Kích thước tối đa
PRIV MAXIMUM Chế độ xem trước đơn giản, xử lý video bằng GPU hoặc quay video mà không cần xem trước.
JPEG MAXIMUM Chụp ảnh tĩnh không dùng kính ngắm.
YUV MAXIMUM Xử lý hình ảnh/video trong ứng dụng.
PRIV PREVIEW JPEG MAXIMUM Ảnh tĩnh tiêu chuẩn.
YUV PREVIEW JPEG MAXIMUM Xử lý trong ứng dụng cộng với tính năng chụp ảnh tĩnh.
PRIV PREVIEW PRIV PREVIEW Bản ghi tiêu chuẩn.
PRIV PREVIEW YUV PREVIEW Xem trước cộng với xử lý trong ứng dụng.
PRIV PREVIEW YUV PREVIEW Xem trước cộng với xử lý trong ứng dụng.
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM Vẫn chụp ảnh và xử lý trong ứng dụng.

LEGACY là cấp độ phần cứng thấp nhất có thể. Bảng này cho thấy mọi thiết bị hỗ trợ Camera2 (API cấp 21 trở lên) có thể xuất tối đa 3 luồng đồng thời bằng cách sử dụng cấu hình phù hợp và nếu không có quá nhiều mức hao tổn hiệu suất hạn chế hiệu suất, chẳng hạn như các giới hạn về bộ nhớ, CPU hoặc nhiệt.

Ứng dụng của bạn cũng cần định cấu hình vùng đệm đầu ra nhắm mục tiêu. Ví dụ: để nhắm đến một thiết bị có cấp độ phần cứng LEGACY, bạn có thể thiết lập 2 nền tảng đầu ra mục tiêu: một nền tảng sử dụng ImageFormat.PRIVATE và một nền tảng khác sử dụng ImageFormat.YUV_420_888. Đây là cách kết hợp được hỗ trợ khi sử dụng kích thước PREVIEW. Khi sử dụng hàm được xác định trước đó trong chủ đề này, việc lấy kích thước xem trước bắt buộc cho mã máy ảnh yêu cầu bạn phải sử dụng mã sau:

Kotlin

val characteristics: CameraCharacteristics = ...
val context = this as Context  // assuming you are inside of an activity

val surfaceViewSize = getPreviewOutputSize(
    context, characteristics, SurfaceView::class.java)
val imageReaderSize = getPreviewOutputSize(
    context, characteristics, ImageReader::class.java, format = ImageFormat.YUV_420_888)

Java

CameraCharacteristics characteristics = ...;
        Context context = this; // assuming you are inside of an activity

        Size surfaceViewSize = getPreviewOutputSize(
                context, characteristics, SurfaceView.class);
        Size imageReaderSize = getPreviewOutputSize(
                context, characteristics, ImageReader.class, format = ImageFormat.YUV_420_888);

Bạn phải đợi cho đến khi SurfaceView sẵn sàng bằng cách sử dụng các lệnh gọi lại đã cung cấp:

Kotlin

val surfaceView = findViewById <SurfaceView>(...)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
  override fun surfaceCreated(holder: SurfaceHolder) {
    // You do not need to specify image format, and it will be considered of type PRIV
    // Surface is now ready and you could use it as an output target for CameraSession
  }
  ...
})

Java

SurfaceView surfaceView = findViewById <SurfaceView>(...);

surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
                // You do not need to specify image format, and it will be considered of type PRIV
                // Surface is now ready and you could use it as an output target for CameraSession
            }
            ...
        });

Bạn có thể buộc SurfaceView khớp với kích thước đầu ra của máy ảnh bằng cách gọi SurfaceHolder.setFixedSize() hoặc thực hiện phương pháp tương tự như AutoFitSurfaceView trong Mô-đun chung của các mẫu máy ảnh trên GitHub. Việc này sẽ đặt kích thước tuyệt đối, xem xét cả tỷ lệ khung hình và không gian còn trống, đồng thời tự động điều chỉnh thời điểm kích hoạt các thay đổi về hoạt động.

Việc thiết lập nền tảng khác từ ImageReader theo định dạng mong muốn sẽ dễ dàng hơn vì không có lệnh gọi lại nào cần chờ:

Kotlin

val frameBufferCount = 3  // just an example, depends on your usage of ImageReader
val imageReader = ImageReader.newInstance(
    imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
    frameBufferCount)

Java

int frameBufferCount = 3;  // just an example, depends on your usage of ImageReader
ImageReader imageReader = ImageReader.newInstance(
                imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
                frameBufferCount);

Khi sử dụng vùng đệm mục tiêu chặn như ImageReader, hãy loại bỏ các khung sau khi sử dụng:

Kotlin

imageReader.setOnImageAvailableListener({
  val frame =  it.acquireNextImage()
  // Do something with "frame" here
  it.close()
}, null)

Java

imageReader.setOnImageAvailableListener(listener -> {
            Image frame = listener.acquireNextImage();
            // Do something with "frame" here
            listener.close();
        }, null);

Cấp độ phần cứng LEGACY nhắm mục tiêu đến các thiết bị mẫu số chung nhỏ nhất. Bạn có thể thêm phân nhánh có điều kiện và sử dụng kích thước RECORD cho một trong các nền tảng đích đầu ra trên các thiết bị có cấp độ phần cứng LIMITED hoặc thậm chí tăng kích thước lên kích thước MAXIMUM cho các thiết bị có cấp độ phần cứng FULL.