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

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

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

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

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

منابعی مانند 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) برآورده می‌کنند. عملکرد واقعی از دستگاهی به دستگاه دیگر متفاوت است، اگرچه اندروید بسته به سه متغیر، ضمانت‌هایی برای پشتیبانی از ترکیب‌های خاص ارائه می‌دهد: نوع خروجی ، اندازه خروجی و سطح سخت‌افزار .

استفاده از ترکیبی از متغیرهای پشتیبانی نشده ممکن است با نرخ فریم پایین کار کند؛ اگر این اتفاق نیفتد، یکی از فراخوانی‌های خطا را فعال می‌کند. مستندات 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 به بهترین اندازه‌ای که با وضوح صفحه نمایش دستگاه یا با وضوح تصویر 1080p (1920x1080)، هر کدام که کوچکتر باشد، مطابقت دارد، اشاره دارد. نسبت ابعاد ممکن است دقیقاً با نسبت ابعاد صفحه نمایش مطابقت نداشته باشد، بنابراین ممکن است لازم باشد برای نمایش آن در حالت تمام صفحه، از کادربندی حروف یا برش استفاده کنید. برای به دست آوردن اندازه پیش‌نمایش مناسب، اندازه‌های خروجی موجود را با اندازه صفحه نمایش مقایسه کنید و در عین حال احتمال چرخش صفحه نمایش را نیز در نظر بگیرید.

کد زیر یک کلاس کمکی به 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 (سطح API 21 و بالاتر) پشتیبانی می‌کند، می‌تواند با استفاده از پیکربندی مناسب و در صورتی که سربار زیادی مانند محدودیت‌های حافظه، پردازنده یا حرارتی وجود نداشته باشد، حداکثر سه جریان همزمان را خروجی دهد.

برنامه شما همچنین باید بافرهای خروجی هدف‌گیری را پیکربندی کند. برای مثال، برای هدف‌گیری دستگاهی با سطح سخت‌افزار 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 با استفاده از callbackهای ارائه شده است:

کاتلین

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

شما می‌توانید SurfaceView مجبور کنید تا با فراخوانی SurfaceHolder.setFixedSize() اندازه خروجی دوربین را مطابقت دهد یا می‌توانید رویکردی مشابه 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 استفاده کنید، یا حتی آن را برای دستگاه‌هایی با سطح سخت‌افزار FULL به اندازه MAXIMUM افزایش دهید.