واجهة برمجة تطبيقات كاميرات متعددة

ملاحظة: تشير هذه الصفحة إلى حزمة camera2. ننصحك باستخدام cameraX ما لم يكن تطبيقك يتطلب ميزات محدّدة ومنخفضة المستوى من Camera2. يتوافق كل من CameraX و Camera2 مع الإصدار Android 5.0 (المستوى 21 من واجهة برمجة التطبيقات) والإصدارات الأحدث.

تم طرح ميزة "الكاميرات المتعددة" في نظام التشغيل Android 9 (المستوى 28 من واجهة برمجة التطبيقات). منذ إصداره، تم طرح أجهزة تدعم واجهة برمجة التطبيقات. تقترن العديد من حالات استخدام الكاميرات المتعددة بإحكام مع تكوين جهاز محدد. بعبارة أخرى، لا تتوافق جميع حالات الاستخدام مع كل جهاز، ما يجعل ميزات الكاميرات المتعددة أكثر تناسبًا مع عرض الميزات في Play.

وتشمل بعض حالات الاستخدام المعتادة ما يلي:

  • التكبير/التصغير: التبديل بين الكاميرات بناءً على منطقة الاقتصاص أو البعد البؤري المطلوب.
  • العمق: استخدام كاميرات متعددة لإنشاء خريطة عمق.
  • ضبابية: استخدام معلومات العمق المستنتَجة لمحاكاة نطاق تركيز ضيق يشبه DSLR.

الفرق بين الكاميرات المنطقية والكاميرات المادية

يتطلب فهم واجهة برمجة التطبيقات الخاصة بالكاميرات المتعددة فهم الفرق بين الكاميرات المنطقية والمادية. كمرجع لك، فكر في جهاز به ثلاث كاميرات خلفية. في هذا المثال، تعد كل كاميرا من الكاميرات الخلفية الثلاث كاميرا مادية. الكاميرا المنطقية هي حينها مجموعة من كاميرتين أو أكثر من تلك الكاميرات المادية. قد يكون ناتج الكاميرا المنطقية بثًا صادرًا من إحدى الكاميرات الفعلية الأساسية أو بث مدمج صادر من أكثر من كاميرا مادية أساسية في الوقت نفسه. وفي كلتا الحالتين، تتم معالجة البث بواسطة طبقة التجريد لأجهزة الكاميرا (HAL).

يطوِّر العديد من الشركات المصنّعة للهواتف تطبيقات كاميرا الطرف الأول، والتي عادةً يتم تثبيتها مسبقًا على أجهزتهم. لاستخدام جميع إمكانات الأجهزة، قد تستخدم هذه الأجهزة واجهات برمجة تطبيقات خاصة أو مخفية أو قد تتلقى معاملة خاصة من تطبيق برنامج التشغيل الذي لا يمكن للتطبيقات الأخرى الوصول إليه. تنفذ بعض الأجهزة مفهوم الكاميرات المنطقية من خلال توفير تدفق مدمج للإطارات من الكاميرات المادية المختلفة، ولكن على بعض التطبيقات المميزة فقط. في كثير من الأحيان، تظهر واحدة فقط من الكاميرات المادية لإطار العمل. يوضّح الرسم التوضيحي التالي موقف المطوّرين التابعين لجهات خارجية قبل استخدام Android 9:

الشكل 1. لا تتوفّر إمكانات الكاميرا عادةً إلا للتطبيقات المميّزة فقط.

بدءًا من نظام التشغيل Android 9، لن يُسمح بواجهات برمجة التطبيقات الخاصة في تطبيقات Android. مع تضمين دعم كاميرات متعددة في إطار العمل، تنصح أفضل الممارسات في Android بشدة بأن تعرض الشركات المصنّعة للهواتف كاميرا منطقية لجميع الكاميرات المادية التي تواجه الاتجاه نفسه. في ما يلي ما يجب أن يتوقّعه مطوّرو البرامج الخارجيون على الأجهزة التي تعمل بنظام التشغيل Android 9 والإصدارات الأحدث:

الشكل 2. إذن وصول كامل للمطوّرين إلى جميع أجهزة الكاميرا بدءًا من نظام التشغيل Android 9

إنّ ما توفّره الكاميرا المنطقية يعتمد تمامًا على تنفيذ المصنِّع الأصلي للجهاز لطبقة التحكّم في الكاميرا (HAL). على سبيل المثال، يستخدم جهاز مثل Pixel 3 الكاميرا المنطقية بحيث يختار إحدى الكاميرات الفعلية بناءً على البعد البؤري ومنطقة الاقتصاص المطلوب.

واجهة برمجة التطبيقات للكاميرات المتعددة

تضيف واجهة برمجة التطبيقات الجديدة الثوابت والفئات والطرق الجديدة التالية:

بسبب التغييرات التي طرأت على مستند تعريف التوافق مع Android (CDD)، تأتي أيضًا واجهة برمجة التطبيقات للكاميرات المتعددة توقّعات معيّنة من المطوّرين. كانت الأجهزة التي تحتوي على كاميرات مزدوجة متوفّرة قبل الإصدار 9 من Android، غير أنّ فتح أكثر من كاميرا واحدة تزامن مع التجربة والخطأ. في نظام التشغيل Android 9 والإصدارات الأحدث، تضع الكاميرات المتعددة مجموعة من القواعد لتحديد الحالات التي يمكن فيها فتح اثنتين من الكاميرات الفعلية التي تكون جزءًا من الكاميرا المنطقية نفسها.

في معظم الحالات، تعرض الأجهزة التي تعمل بنظام التشغيل Android 9 والإصدارات الأحدث جميع الكاميرات المادية (باستثناء أنواع أجهزة الاستشعار الأقل شيوعًا مثل الأشعة تحت الحمراء) بالإضافة إلى كاميرا منطقية سهلة الاستخدام. فلكل مجموعة من مجموعات البث المضمونة عملها، يمكن استبدال بث واحد خاص بكاميرا منطقية ببثَّين من الكاميرات الفعلية الأساسية.

أحداث بث متعددة في الوقت نفسه

ويشمل استخدام مجموعات بث متعددة الكاميرات في الوقت نفسه قواعد استخدام عدة مجموعات بث في الوقت نفسه باستخدام كاميرا واحدة. مع إضافة واحدة بارزة، تنطبق القواعد نفسها على عدة كاميرات. توضّح CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA كيفية استبدال بث YUV_420_888 منطقي أو بث أوّلي ببثَّين ماديَّين. وهذا يعني أنه يمكن استبدال كل بث من النوع YUV أو RAW ببثَّين من النوع والحجم المتطابقَين. يمكنك البدء ببث الكاميرا بالإعداد المضمون التالي للأجهزة ذات الكاميرا الواحدة:

  • مجموعة البث 1: نوع YUV، الحجم MAXIMUM من الكاميرا المنطقية id = 0

بعد ذلك، يسمح لك الجهاز المزوّد بكاميرات متعددة بإنشاء جلسة تستبدل تدفق YUV المنطقي بمجموعتين فعليتين من البث:

  • مجموعة البث 1: نوع YUV، الحجم MAXIMUM من الكاميرا الفعلية id = 1
  • مجموعة البث 2: نوع YUV، الحجم MAXIMUM من الكاميرا الفعلية id = 2

يمكنك استبدال بث YUV أو RAW بمجموعتَي بث مكافئَين إذا كانت هاتان الكاميرتان جزءًا من مجموعة كاميرات منطقية مدرَجة ضمن CameraCharacteristics.getPhysicalCameraIds().

والضمانات التي يوفّرها إطار العمل هي الحدّ الأدنى المطلوب للحصول على إطارات من أكثر من كاميرا مادية واحدة في الوقت نفسه. ويمكن استخدام مجموعات بث إضافية في معظم الأجهزة، وقد يسمح أحيانًا بفتح أجهزة كاميرا مادية متعددة بشكل مستقل. ونظرًا لأن إطار العمل هذا ليس ضمانًا قويًا، فإن القيام بذلك يتطلب إجراء اختبار لكل جهاز وضبطه باستخدام التجربة والخطأ.

إنشاء جلسة باستخدام عدة كاميرات فعلية

عند استخدام الكاميرات الفعلية على جهاز مزود بكاميرات متعددة، افتح CameraDevice (الكاميرا المنطقية) وتفاعَل معها خلال جلسة واحدة. أنشِئ جلسة واحدة باستخدام واجهة برمجة التطبيقات CameraDevice.createCaptureSession(SessionConfiguration config) التي تمت إضافتها في المستوى 28 من واجهة برمجة التطبيقات. تحتوي تهيئة الجلسة على عدد من إعدادات الإخراج، ولكل منها مجموعة من أهداف المخرجات، ورقم تعريف كاميرا فعلية مطلوب، إذا رغبت في ذلك.

الشكل 3. نموذج جلسة ضبط الإعدادات ونموذج الإخراج

ترتبط طلبات الالتقاط بأهداف مستهدَفة مرتبطة بها. ويحدّد إطار العمل الكاميرا الفعلية (أو المنطقية) التي يتم إرسال الطلبات إليها استنادًا إلى هدف الإخراج المرفق. إذا كان هدف الإخراج يتطابق مع أحد أهداف المخرجات التي تم إرسالها كإعدادات إخراج مع معرّف كاميرا فعلية، ستتلقّى هذه الكاميرا الفعلية الطلب وتعالجه.

استخدام زوج من الكاميرات المادية

إضافة أخرى إلى واجهات برمجة التطبيقات للكاميرا الخاصة بالكاميرات المتعددة هي القدرة على التعرّف على الكاميرات المنطقية والعثور على الكاميرات المادية خلفها. يمكنك تحديد دالة للمساعدة في تحديد الأزواج المحتملة من الكاميرات المادية التي يمكنك استخدامها لاستبدال أحد تدفقات الكاميرا المنطقية:

Kotlin

/**
     * Helper class used to encapsulate a logical camera and two underlying
     * physical cameras
     */
    data class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)

    fun findDualCameras(manager: CameraManager, facing: Int? = null): List {
        val dualCameras = MutableList()

        // Iterate over all the available camera characteristics
        manager.cameraIdList.map {
            Pair(manager.getCameraCharacteristics(it), it)
        }.filter {
            // Filter by cameras facing the requested direction
            facing == null || it.first.get(CameraCharacteristics.LENS_FACING) == facing
        }.filter {
            // Filter by logical cameras
            // CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA requires API >= 28
            it.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!.contains(
                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
        }.forEach {
            // All possible pairs from the list of physical cameras are valid results
            // NOTE: There could be N physical cameras as part of a logical camera grouping
            // getPhysicalCameraIds() requires API >= 28
            val physicalCameras = it.first.physicalCameraIds.toTypedArray()
            for (idx1 in 0 until physicalCameras.size) {
                for (idx2 in (idx1 + 1) until physicalCameras.size) {
                    dualCameras.add(DualCamera(
                        it.second, physicalCameras[idx1], physicalCameras[idx2]))
                }
            }
        }

        return dualCameras
    }

Java

/**
     * Helper class used to encapsulate a logical camera and two underlying
     * physical cameras
     */
    final class DualCamera {
        final String logicalId;
        final String physicalId1;
        final String physicalId2;

        DualCamera(String logicalId, String physicalId1, String physicalId2) {
            this.logicalId = logicalId;
            this.physicalId1 = physicalId1;
            this.physicalId2 = physicalId2;
        }
    }
    List findDualCameras(CameraManager manager, Integer facing) {
        List dualCameras = new ArrayList<>();

        List cameraIdList;
        try {
            cameraIdList = Arrays.asList(manager.getCameraIdList());
        } catch (CameraAccessException e) {
            e.printStackTrace();
            cameraIdList = new ArrayList<>();
        }

        // Iterate over all the available camera characteristics
        cameraIdList.stream()
                .map(id -> {
                    try {
                        CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
                        return new Pair<>(characteristics, id);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                        return null;
                    }
                })
                .filter(pair -> {
                    // Filter by cameras facing the requested direction
                    return (pair != null) &&
                            (facing == null || pair.first.get(CameraCharacteristics.LENS_FACING).equals(facing));
                })
                .filter(pair -> {
                    // Filter by logical cameras
                    // CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA requires API >= 28
                    IntPredicate logicalMultiCameraPred =
                            arg -> arg == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA;
                    return Arrays.stream(pair.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES))
                            .anyMatch(logicalMultiCameraPred);
                })
                .forEach(pair -> {
                    // All possible pairs from the list of physical cameras are valid results
                    // NOTE: There could be N physical cameras as part of a logical camera grouping
                    // getPhysicalCameraIds() requires API >= 28
                    String[] physicalCameras = pair.first.getPhysicalCameraIds().toArray(new String[0]);
                    for (int idx1 = 0; idx1 < physicalCameras.length; idx1++) {
                        for (int idx2 = idx1 + 1; idx2 < physicalCameras.length; idx2++) {
                            dualCameras.add(
                                    new DualCamera(pair.second, physicalCameras[idx1], physicalCameras[idx2]));
                        }
                    }
                });
return dualCameras;
}

وتتحكّم الكاميرا المنطقية في التعامل مع حالة الكاميرات الفعلية. لفتح "كاميرا مزدوجة"، افتح الكاميرا المنطقية المقابلة للكاميرات الفعلية:

Kotlin

fun openDualCamera(cameraManager: CameraManager,
                       dualCamera: DualCamera,
        // AsyncTask is deprecated beginning API 30
                       executor: Executor = AsyncTask.SERIAL_EXECUTOR,
                       callback: (CameraDevice) -> Unit) {

        // openCamera() requires API >= 28
        cameraManager.openCamera(
            dualCamera.logicalId, executor, object : CameraDevice.StateCallback() {
                override fun onOpened(device: CameraDevice) = callback(device)
                // Omitting for brevity...
                override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)
                override fun onDisconnected(device: CameraDevice) = device.close()
            })
    }

Java

void openDualCamera(CameraManager cameraManager,
                        DualCamera dualCamera,
                        Executor executor,
                        CameraDeviceCallback cameraDeviceCallback
    ) {

        // openCamera() requires API >= 28
        cameraManager.openCamera(dualCamera.logicalId, executor, new CameraDevice.StateCallback() {
            @Override
            public void onOpened(@NonNull CameraDevice cameraDevice) {
               cameraDeviceCallback.callback(cameraDevice);
            }

            @Override
            public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                cameraDevice.close();
            }

            @Override
            public void onError(@NonNull CameraDevice cameraDevice, int i) {
                onDisconnected(cameraDevice);
            }
        });
    }

إلى جانب اختيار الكاميرا المطلوب فتحها، تتشابه عملية فتح الكاميرا مع عملية فتح الكاميرا في إصدارات Android السابقة. إنّ إنشاء جلسة تسجيل باستخدام واجهة برمجة التطبيقات الجديدة لإعدادات الجلسة يطلب من إطار العمل ربط أهداف معيّنة بمعرّفات فعلية محدّدة للكاميرا:

Kotlin

/**
 * Helper type definition that encapsulates 3 sets of output targets:
 *
 *   1. Logical camera
 *   2. First physical camera
 *   3. Second physical camera
 */
typealias DualCameraOutputs =
        Triple?, MutableList?, MutableList?>

fun createDualCameraSession(cameraManager: CameraManager,
                            dualCamera: DualCamera,
                            targets: DualCameraOutputs,
                            // AsyncTask is deprecated beginning API 30
                            executor: Executor = AsyncTask.SERIAL_EXECUTOR,
                            callback: (CameraCaptureSession) -> Unit) {

    // Create 3 sets of output configurations: one for the logical camera, and
    // one for each of the physical cameras.
    val outputConfigsLogical = targets.first?.map { OutputConfiguration(it) }
    val outputConfigsPhysical1 = targets.second?.map {
        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId1) } }
    val outputConfigsPhysical2 = targets.third?.map {
        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId2) } }

    // Put all the output configurations into a single flat array
    val outputConfigsAll = arrayOf(
        outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2)
        .filterNotNull().flatMap { it }

    // Instantiate a session configuration that can be used to create a session
    val sessionConfiguration = SessionConfiguration(
        SessionConfiguration.SESSION_REGULAR,
        outputConfigsAll, executor, object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) = callback(session)
            // Omitting for brevity...
            override fun onConfigureFailed(session: CameraCaptureSession) = session.device.close()
        })

    // Open the logical camera using the previously defined function
    openDualCamera(cameraManager, dualCamera, executor = executor) {

        // Finally create the session and return via callback
        it.createCaptureSession(sessionConfiguration)
    }
}

Java

/**
 * Helper class definition that encapsulates 3 sets of output targets:
 * 

* 1. Logical camera * 2. First physical camera * 3. Second physical camera */ final class DualCameraOutputs { private final List logicalCamera; private final List firstPhysicalCamera; private final List secondPhysicalCamera; public DualCameraOutputs(List logicalCamera, List firstPhysicalCamera, List third) { this.logicalCamera = logicalCamera; this.firstPhysicalCamera = firstPhysicalCamera; this.secondPhysicalCamera = third; } public List getLogicalCamera() { return logicalCamera; } public List getFirstPhysicalCamera() { return firstPhysicalCamera; } public List getSecondPhysicalCamera() { return secondPhysicalCamera; } } interface CameraCaptureSessionCallback { void callback(CameraCaptureSession cameraCaptureSession); } void createDualCameraSession(CameraManager cameraManager, DualCamera dualCamera, DualCameraOutputs targets, Executor executor, CameraCaptureSessionCallback cameraCaptureSessionCallback) { // Create 3 sets of output configurations: one for the logical camera, and // one for each of the physical cameras. List outputConfigsLogical = targets.getLogicalCamera().stream() .map(OutputConfiguration::new) .collect(Collectors.toList()); List outputConfigsPhysical1 = targets.getFirstPhysicalCamera().stream() .map(s -> { OutputConfiguration outputConfiguration = new OutputConfiguration(s); outputConfiguration.setPhysicalCameraId(dualCamera.physicalId1); return outputConfiguration; }) .collect(Collectors.toList()); List outputConfigsPhysical2 = targets.getSecondPhysicalCamera().stream() .map(s -> { OutputConfiguration outputConfiguration = new OutputConfiguration(s); outputConfiguration.setPhysicalCameraId(dualCamera.physicalId2); return outputConfiguration; }) .collect(Collectors.toList()); // Put all the output configurations into a single flat array List outputConfigsAll = Stream.of( outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2 ) .filter(Objects::nonNull) .flatMap(Collection::stream) .collect(Collectors.toList()); // Instantiate a session configuration that can be used to create a session SessionConfiguration sessionConfiguration = new SessionConfiguration( SessionConfiguration.SESSION_REGULAR, outputConfigsAll, executor, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { cameraCaptureSessionCallback.callback(cameraCaptureSession); } // Omitting for brevity... @Override public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { cameraCaptureSession.getDevice().close(); } }); // Open the logical camera using the previously defined function openDualCamera(cameraManager, dualCamera, executor, (CameraDevice c) -> // Finally create the session and return via callback c.createCaptureSession(sessionConfiguration)); }

يمكنك الاطّلاع على createCaptureSession للحصول على معلومات حول مجموعة أحداث البث المتوافقة. ويكون دمج مجموعات البث مخصّصًا لعدّة مجموعات بث على كاميرا منطقية واحدة. يمتد التوافق ليشمل استخدام الإعدادات نفسها واستبدال أحد مجموعات البث هذه ببثَّين من كاميرتَين ماديتَين تشكّلان جزءًا من الكاميرا المنطقية نفسها.

عندما تكون جلسة الكاميرا جاهزة، يمكنك إرسال طلبات الالتقاط المطلوبة. يتلقّى كل هدف في طلب الالتقاط بياناته من الكاميرا المادية المرتبطة به، إذا كان أي منها قيد الاستخدام، أو يعود إلى الكاميرا المنطقية.

مثال على حالة استخدام Zoom

من الممكن استخدام دمج الكاميرات المادية في بث واحد لكي يتمكّن المستخدمون من التبديل بين الكاميرات المادية المختلفة لتجربة مجال رؤية مختلف، وبالتالي التقاط "مستوى تكبير/تصغير" مختلف بشكل فعّال.

الشكل 4. مثال على تبديل الكاميرات بحالة الاستخدام لمستوى التكبير أو التصغير (من إعلان Pixel 3)

ابدأ باختيار زوج من الكاميرات المادية للسماح للمستخدمين بالتبديل بينهما. للحصول على أقصى تأثير ممكن، يمكنك اختيار زوج من الكاميرات التي توفر الحد الأدنى والحد الأقصى للبعد البؤري المتاح.

Kotlin

fun findShortLongCameraPair(manager: CameraManager, facing: Int? = null): DualCamera? {

    return findDualCameras(manager, facing).map {
        val characteristics1 = manager.getCameraCharacteristics(it.physicalId1)
        val characteristics2 = manager.getCameraCharacteristics(it.physicalId2)

        // Query the focal lengths advertised by each physical camera
        val focalLengths1 = characteristics1.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)
        val focalLengths2 = characteristics2.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)

        // Compute the largest difference between min and max focal lengths between cameras
        val focalLengthsDiff1 = focalLengths2.maxOrNull()!! - focalLengths1.minOrNull()!!
        val focalLengthsDiff2 = focalLengths1.maxOrNull()!! - focalLengths2.minOrNull()!!

        // Return the pair of camera IDs and the difference between min and max focal lengths
        if (focalLengthsDiff1 < focalLengthsDiff2) {
            Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)
        } else {
            Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)
        }

        // Return only the pair with the largest difference, or null if no pairs are found
    }.maxByOrNull { it.second }?.first
}

Java

// Utility functions to find min/max value in float[]
    float findMax(float[] array) {
        float max = Float.NEGATIVE_INFINITY;
        for(float cur: array)
            max = Math.max(max, cur);
        return max;
    }
    float findMin(float[] array) {
        float min = Float.NEGATIVE_INFINITY;
        for(float cur: array)
            min = Math.min(min, cur);
        return min;
    }

DualCamera findShortLongCameraPair(CameraManager manager, Integer facing) {
        return findDualCameras(manager, facing).stream()
                .map(c -> {
                    CameraCharacteristics characteristics1;
                    CameraCharacteristics characteristics2;
                    try {
                        characteristics1 = manager.getCameraCharacteristics(c.physicalId1);
                        characteristics2 = manager.getCameraCharacteristics(c.physicalId2);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                        return null;
                    }

                    // Query the focal lengths advertised by each physical camera
                    float[] focalLengths1 = characteristics1.get(
                            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
                    float[] focalLengths2 = characteristics2.get(
                            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);

                    // Compute the largest difference between min and max focal lengths between cameras
                    Float focalLengthsDiff1 = findMax(focalLengths2) - findMin(focalLengths1);
                    Float focalLengthsDiff2 = findMax(focalLengths1) - findMin(focalLengths2);

                    // Return the pair of camera IDs and the difference between min and max focal lengths
                    if (focalLengthsDiff1 < focalLengthsDiff2) {
                        return new Pair<>(new DualCamera(c.logicalId, c.physicalId1, c.physicalId2), focalLengthsDiff1);
                    } else {
                        return new Pair<>(new DualCamera(c.logicalId, c.physicalId2, c.physicalId1), focalLengthsDiff2);
                    }

                }) // Return only the pair with the largest difference, or null if no pairs are found
                .max(Comparator.comparing(pair -> pair.second)).get().first;
    }

وقد يكون من المفيد تضمين وحدتي SurfaceViews في بنية معقولة لكل مصدر، ولكل مجموعة بيانات. يتم تبديل SurfaceViews هذه استنادًا إلى تفاعل المستخدم، بحيث لا يظهر سوى تفاعل واحد في أي وقت.

يعرض الرمز البرمجي التالي كيفية فتح الكاميرا المنطقية، وضبط مصادر إخراجات الكاميرا، وإنشاء جلسة كاميرا، وبدء بثَين للمعاينة:

Kotlin

val cameraManager: CameraManager = ...

// Get the two output targets from the activity / fragment
val surface1 = ...  // from SurfaceView
val surface2 = ...  // from SurfaceView

val dualCamera = findShortLongCameraPair(manager)!!
val outputTargets = DualCameraOutputs(
    null, mutableListOf(surface1), mutableListOf(surface2))

// Here you open the logical camera, configure the outputs and create a session
createDualCameraSession(manager, dualCamera, targets = outputTargets) { session ->

  // Create a single request which has one target for each physical camera
  // NOTE: Each target receive frames from only its associated physical camera
  val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
  val captureRequest = session.device.createCaptureRequest(requestTemplate).apply {
    arrayOf(surface1, surface2).forEach { addTarget(it) }
  }.build()

  // Set the sticky request for the session and you are done
  session.setRepeatingRequest(captureRequest, null, null)
}

Java

CameraManager manager = ...;

        // Get the two output targets from the activity / fragment
        Surface surface1 = ...;  // from SurfaceView
        Surface surface2 = ...;  // from SurfaceView

        DualCamera dualCamera = findShortLongCameraPair(manager, null);
                DualCameraOutputs outputTargets = new DualCameraOutputs(
                null, Collections.singletonList(surface1), Collections.singletonList(surface2));

        // Here you open the logical camera, configure the outputs and create a session
        createDualCameraSession(manager, dualCamera, outputTargets, null, (session) -> {
            // Create a single request which has one target for each physical camera
            // NOTE: Each target receive frames from only its associated physical camera
            CaptureRequest.Builder captureRequestBuilder;
            try {
                captureRequestBuilder = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                Arrays.asList(surface1, surface2).forEach(captureRequestBuilder::addTarget);

                // Set the sticky request for the session and you are done
                session.setRepeatingRequest(captureRequestBuilder.build(), null, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        });

ما عليك سوى توفير واجهة مستخدم تتيح للمستخدم التبديل بين السطحَين، مثل زر أو النقر مرّتين على SurfaceView. يمكنك حتى تنفيذ شكل من أشكال تحليل المشهد والتبديل بين البثين تلقائيًا.

تشوّه العدسة

تُنتج جميع العدسات مقدارًا معينًا من التشوه. في Android، يمكنك الاستعلام عن التشوّه الناتج عن العدسات باستخدام CameraCharacteristics.LENS_DISTORTION، الذي يحلّ محلّ CameraCharacteristics.LENS_RADIAL_DISTORTION التي تم إيقافها نهائيًا. بالنسبة إلى الكاميرات المنطقية، يكون التشوّه ضئيلاً ويمكن للتطبيق أن يستخدم الإطارات أكثر أو أقل عندما تأتي من الكاميرا. بالنسبة إلى الكاميرات الفعلية، قد تكون هناك إعدادات مختلفة للعدسات، خاصةً على العدسات العريضة.

قد تنفّذ بعض الأجهزة تصحيح التشوّه التلقائي من خلال CaptureRequest.DISTORTION_CORRECTION_MODE. يتم تلقائيًا تفعيل ميزة "تصحيح التشوّه" على معظم الأجهزة.

Kotlin

val cameraSession: CameraCaptureSession = ...

        // Use still capture template to build the capture request
        val captureRequest = cameraSession.device.createCaptureRequest(
            CameraDevice.TEMPLATE_STILL_CAPTURE
        )

        // Determine if this device supports distortion correction
        val characteristics: CameraCharacteristics = ...
        val supportsDistortionCorrection = characteristics.get(
            CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
        )?.contains(
            CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
        ) ?: false

        if (supportsDistortionCorrection) {
            captureRequest.set(
                CaptureRequest.DISTORTION_CORRECTION_MODE,
                CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
            )
        }

        // Add output target, set other capture request parameters...

        // Dispatch the capture request
        cameraSession.capture(captureRequest.build(), ...)

Java

CameraCaptureSession cameraSession = ...;

        // Use still capture template to build the capture request
        CaptureRequest.Builder captureRequestBuilder = null;
        try {
            captureRequestBuilder = cameraSession.getDevice().createCaptureRequest(
                    CameraDevice.TEMPLATE_STILL_CAPTURE
            );
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        // Determine if this device supports distortion correction
        CameraCharacteristics characteristics = ...;
        boolean supportsDistortionCorrection = Arrays.stream(
                        characteristics.get(
                                CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
                        ))
                .anyMatch(i -> i == CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY);
        if (supportsDistortionCorrection) {
            captureRequestBuilder.set(
                    CaptureRequest.DISTORTION_CORRECTION_MODE,
                    CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
            );
        }

        // Add output target, set other capture request parameters...

        // Dispatch the capture request
        cameraSession.capture(captureRequestBuilder.build(), ...);

قد يؤثر ضبط طلب الالتقاط في هذا الوضع في عدد اللقطات في الثانية الذي يمكن للكاميرا الحصول عليه. يمكنك اختيار ضبط تصحيح التشوّه على التقاطات الصور الثابتة فقط.