استفاده هم‌زمان از چند جاری‌سازی دوربین

توجه: این صفحه به بسته Camera2 اشاره دارد. توصیه می‌کنیم از CameraX استفاده کنید، مگراینکه برنامه‌تان به ویژگی‌های خاص و سطح پایین Camera2 نیاز داشته باشد. ‫CameraX و Camera2 هر دو از Android 5.0 (سطح میانای برنامه‌سازی کاربردی ۲۱) و بالاتر پشتیبانی می‌کنند.

برنامه دوربین می‌تواند به‌طور هم‌زمان از بیش‌از یک جریان فریم استفاده کند. در برخی موارد، جاری‌سازی‌های مختلف حتی به وضوح قاب یا قالب پیکسل متفاوتی نیاز دارند. برخی‌از موارد استفاده معمول عبارت‌اند از:

  • ضبط ویدیو: یک جاری‌سازی برای پیش‌نمایش، و جاری‌سازی دیگری که کدگذاری و در فایل ذخیره می‌شود.
  • اسکن بارکد: یک جاری‌سازی برای پیش‌نمایش، دیگری برای تشخیص بارکد.
  • عکاسی رایانشی: یک جاری‌سازی برای پیش‌نمایش، دیگری برای تشخیص چهره/صحنه.

هنگام پردازش فریم‌ها، هزینه عملکرد غیرجزئی وجود دارد و این هزینه هنگام انجام پردازش موازی جاری‌سازی یا خط لوله چندبرابر می‌شود.

منابعی مانند CPU،‏ GPU، و DSP ممکن است بتوانند از قابلیت‌های پردازش مجدد چارچوب بهره ببرند، اما منابعی مانند حافظه به‌صورت خطی رشد خواهند کرد.

چندین هدف در هر درخواست

می‌توان چند جاری‌سازی دوربین را در یک CameraCaptureRequest ترکیب کرد. تکه‌کد زیر نحوه راه‌اندازی جلسه دوربین را با یک جاری‌سازی برای پیش‌نمایش دوربین و جاری‌سازی دیگری برای پردازش تصویر نشان می‌دهد:

کاتلین

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)

جاوا

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);

اگر سطوح هدف را به‌درستی پیکربندی کنید، این کد فقط جاری‌سازی‌هایی را تولید می‌کند که حداقل FPS تعیین‌شده توسط StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) و StreamComfigurationMap.GetOutputStallDuration(int, Size) را برآورده می‌کنند. عملکرد واقعی از دستگاهی به دستگاه دیگر متفاوت است، هرچند Android تضمین‌هایی برای پشتیبانی از ترکیب‌های خاص بسته به سه متغیر ارائه می‌دهد: نوع خروجی، اندازه خروجی، و سطح سخت‌افزار.

استفاده از ترکیب پشتیبانی‌نشده‌ای از متغیرها ممکن است با نرخ فریم پایینی کار کند؛ اگر کار نکند، یکی از فراخوان‌های برگشتی ناموفق را راه‌اندازی خواهد کرد. اسناد createCaptureSession توضیح می‌دهد که چه چیزی تضمین شده است که کار کند.

نوع خروجی

نوع خروجی به قالبی اشاره دارد که قاب‌ها در آن کدبندی می‌شوند. مقادیر ممکن عبارت‌اند از PRIV،‏ YUV،‏ JPEG، و RAW. مستندات createCaptureSession آن‌ها را شرح می‌دهد.

هنگام انتخاب نوع برونداد برنامه، اگر هدف به‌حداکثر رساندن سازگاری است، از ImageFormat.YUV_420_888 برای تحلیل قاب و ImageFormat.JPEG برای تصاویر ثابت استفاده کنید. برای سناریوهای پیش‌نمایش و ضبط، احتمالاً از SurfaceView، TextureView، MediaRecorder، MediaCodec، یا RenderScript.Allocation استفاده خواهید کرد. در این موارد، قالب تصویر را مشخص نکنید. برای سازگاری، به‌عنوان ImageFormat.PRIVATE محسوب می‌شود، صرف‌نظر از قالب واقعی که به‌طور داخلی استفاده می‌شود. برای پرس‌وجو درباره قالب‌های پشتیبانی‌شده توسط دستگاه با داشتن CameraCharacteristics، از کد زیر استفاده کنید:

کاتلین

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

جاوا

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

اندازه برونداد

همه اندازه‌های خروجی دردسترس توسط StreamConfigurationMap.getOutputSizes() فهرست شده‌اند، اما فقط دو مورد به سازگاری مربوط می‌شوند: PREVIEW و MAXIMUM. اندازه‌ها به‌عنوان کران‌های بالا عمل می‌کنند. اگر چیزی با اندازه PREVIEW کار کند، هر چیزی با اندازه کوچک‌تر از PREVIEW نیز کار خواهد کرد. این موضوع برای MAXIMUM نیز صادق است. سند مربوط به CameraDevice این اندازه‌ها را توضیح می‌دهد.

اندازه‌های خروجی دردسترس به قالب انتخابی بستگی دارد. با داشتن CameraCharacteristics و یک قالب، می‌توانید اندازه‌های خروجی دردسترس را به‌این‌صورت پُرسمان کنید:

کاتلین

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

جاوا

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

در موارد استفاده از پیش‌نمایش و ضبط دوربین، از کلاس هدف برای تعیین اندازه‌های پشتیبانی‌شده استفاده کنید. قالب توسط خود چارچوب دوربین مدیریت خواهد شد:

کاتلین

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

جاوا

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

برای دریافت اندازه MAXIMUM، اندازه‌های خروجی را براساس مساحت مرتب کنید و بزرگ‌ترین را برگردانید:

کاتلین

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 }
}

جاوا

 @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 به بهترین اندازه مطابق با وضوح صفحه‌نمایش دستگاه یا ۱۰۸۰ پیکسل (۱۹۲۰×۱۰۸۰)، هرکدام که کوچک‌تر باشد، اشاره دارد. ممکن است نسبت ابعادی دقیقاً با نسبت ابعادی صفحه‌نمایش مطابقت نداشته باشد، بنابراین ممکن است لازم باشد برای نمایش آن در حالت تمام‌صفحه، از جعبه‌بندی یا برش در جاری‌سازی استفاده کنید. برای دریافت اندازه پیش‌نمایش مناسب، اندازه‌های خروجی دردسترس را با اندازه نمایش مقایسه کنید و درعین‌حال توجه داشته باشید که ممکن است نمایشگر چرخانده شود.

کد زیر کلاس کمکی SmartSize را تعریف می‌کند که مقایسه اندازه را کمی آسان‌تر می‌کند:

کاتلین

/** 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
}

جاوا

/** 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;
    }

بررسی سطح سخت‌افزار پشتیبانی‌شده

برای تعیین قابلیت‌های دردسترس در زمان اجرا، سطح سخت‌افزار پشتیبانی‌شده را بااستفاده از CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL بررسی کنید.

با شیء CameraCharacteristics می‌توانید سطح سخت‌افزار را با یک عبارت بازیابی کنید:

کاتلین

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)

جاوا

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);

درحال کنارهم گذاشتن اطلاعات

با نوع خروجی، اندازه خروجی، و سطح سخت‌افزار، می‌توانید تعیین کنید کدام ترکیب‌های جاری‌سازی معتبر هستند. نمودار زیر نمای سریعی از پیکربندی‌های پشتیبانی‌شده توسط CameraDevice با سطح سخت‌افزار LEGACY است.

هدف ۱ هدف ۲ هدف ۳ مورد(های) استفاده نمونه
نوع حداکثر اندازه نوع حداکثر اندازه نوع حداکثر اندازه
PRIV MAXIMUM پیش‌نمایش ساده، پردازش ویدیو GPU، یا ضبط ویدیو بدون پیش‌نمایش.
JPEG MAXIMUM ضبط تصویر ثابت بدون نمایاب.
YUV MAXIMUM پردازش ویدیو/تصویر درون‌برنامه.
PRIV PREVIEW JPEG MAXIMUM تصویربرداری ثابت استاندارد.
YUV PREVIEW JPEG MAXIMUM پردازش درون‌برنامه به‌علاوه ثبت عکس ثابت.
PRIV PREVIEW PRIV PREVIEW ضبط استاندارد.
PRIV PREVIEW YUV PREVIEW پیش‌نمایش به‌علاوه پردازش درون‌برنامه.
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM ضبط عکس به‌علاوه پردازش درون‌برنامه.

LEGACY پایین‌ترین سطح سخت‌افزار ممکن است. این جدول نشان می‌دهد که هر دستگاهی که از Camera2 (سطح میانای برنامه‌سازی کاربردی ۲۱ و بالاتر) پشتیبانی می‌کند می‌تواند بااستفاده از پیکربندی مناسب و درصورت عدم وجود سربار بیش‌ازحد که عملکرد را محدود کند (مانند محدودیت‌های حافظه، CPU، یا حرارتی)، تا سه جریان هم‌زمان را خروجی دهد.

برنامه شما همچنین باید میان‌گیرهای خروجی هدف‌یابی را پیکربندی کند. برای مثال، برای هدف‌یابی دستگاهی با سطح سخت‌افزار LEGACY، می‌توانید دو سطح خروجی هدف راه‌اندازی کنید، یکی بااستفاده از ImageFormat.PRIVATE و دیگری بااستفاده از ImageFormat.YUV_420_888. این ترکیب هنگام استفاده از اندازه PREVIEW پشتیبانی می‌شود. بااستفاده از تابع تعریف‌شده در ابتدای این موضوع، دریافت اندازه‌های پیش‌نمایش موردنیاز برای شناسه دوربین به کد زیر نیاز دارد:

کاتلین

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)

جاوا

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);

این کار مستلزم انتظار تا زمانی است که SurfaceView بااستفاده از فراخوانی‌های برگشتی ارائه‌شده آماده شود:

کاتلین

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
  }
  ...
})

جاوا

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
            }
            ...
        });

می‌توانید با فراخوانی SurfaceHolder.setFixedSize() SurfaceView را مجبور کنید با اندازه خروجی دوربین مطابقت داشته باشد یا می‌توانید رویکردی مشابه AutoFitSurfaceView از ماژول Common نمونه‌های دوربین در GitHub اتخاذ کنید که اندازه مطلق را تنظیم می‌کند و هم نسبت ابعادی و هم فضای دردسترس را درنظر می‌گیرد، درحالی‌که به‌طور خودکار هنگام راه‌اندازی تغییرات فعالیت تنظیم می‌شود.

راه‌اندازی سطح دیگر از ImageReader با قالب مطلوب آسان‌تر است، زیرا هیچ فراخوانی‌ای وجود ندارد که منتظرش بمانید:

کاتلین

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)

جاوا

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);

هنگام استفاده از میان‌گیر هدف مسدودکننده مانند ImageReader، فریم‌ها را پس‌از استفاده دور بیندازید:

کاتلین

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

جاوا

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

سطح سخت‌افزار LEGACY دستگاه‌های دارای پایین‌ترین مخرج مشترک را هدف قرار می‌دهد. می‌توانید انشعاب شرطی اضافه کنید و از اندازه RECORD برای یکی از سطوح هدف خروجی در دستگاه‌های دارای سطح سخت‌افزار LIMITED استفاده کنید، یا حتی آن را به اندازه MAXIMUM برای دستگاه‌های دارای سطح سخت‌افزار FULL افزایش دهید.