মাল্টি-ক্যামেরা API

মনে রাখবেন: এই পৃষ্ঠা Camera2 প্যাকেজকে রেফার করে। আপনার অ্যাপের Camera2 থেকে নির্দিষ্ট, লো-লেভেল ফিচারের প্রয়োজন না হলে, আমরা CameraX ব্যবহার করার সাজেশন দিই। CameraX এবং Camera2, দুটিই Android 5.0 (API লেভেল ২১) এবং তার পরবর্তী যেকোনও ভার্সনে কাজ করে।

Android 9 (API লেভেল ২৮)-এ মাল্টি-ক্যামেরা ফিচার প্রথমবার নিয়ে আসা হয়। এটি রিলিজ হওয়ার পর থেকে, API কাজ করে এমন ডিভাইস বাজারে এসেছে। মাল্টি-ক্যামেরা ব্যবহারের অনেক কেস নির্দিষ্ট হার্ডওয়্যার কনফিগারেশনের সাথে ঘনিষ্ঠভাবে যুক্ত। অন্যভাবে বলতে গেলে, সব ধরনের ব্যবহারের ক্ষেত্রে সব ডিভাইস মানানসই নয়,  যার ফলে মাল্টি-ক্যামেরা ফিচার Play ফিচার ডেলিভারি-এর জন্য উপযুক্ত।

কিছু সাধারণ ব্যবহারের ক্ষেত্রে অন্তর্ভুক্ত:

  • জুম: ক্রপ করা অঞ্চল বা বেছে নেওয়া ফোকাল দৈর্ঘ্যের উপর নির্ভর করে ক্যামেরার মধ্যে পরিবর্তন করা।
  • ডেপথ: ডেপথ ম্যাপ তৈরি করতে একাধিক ক্যামেরা ব্যবহার করা।
  • বোকে: DSLR-এর মতো সংকীর্ণ ফোকাস রেঞ্জ সিমুলেট করতে অনুমান করা গভীরতার তথ্য ব্যবহার করা।

লজিক্যাল ও ফিজিক্যাল ক্যামেরার মধ্যে পার্থক্য

মাল্টি-ক্যামেরা API বুঝতে হলে লজিক্যাল ও ফিজিক্যাল ক্যামেরার মধ্যে পার্থক্য বুঝতে হবে। রেফারেন্সের জন্য, তিনটি ব্যাক-ফেসিং ক্যামেরা সহ ডিভাইস বিবেচনা করুন। এই উদাহরণে, তিনটি ব্যাক ক্যামেরার প্রত্যেকটিকে ফিজিক্যাল ক্যামেরা হিসেবে বিবেচনা করা হয়। লজিক্যাল ক্যামেরা হল দুটি বা তার বেশি ফিজিক্যাল ক্যামেরার গ্রুপ। লজিক্যাল ক্যামেরার আউটপুট এমন একটি স্ট্রিম হতে পারে যা অন্তর্নিহিত ফিজিক্যাল ক্যামেরাগুলির মধ্যে একটি থেকে আসে, অথবা একাধিক অন্তর্নিহিত ফিজিক্যাল ক্যামেরা থেকে একসাথে আসা একটি ফিউজড স্ট্রিম। যেকোনও উপায়ে, স্ট্রিমটি ক্যামেরা হার্ডওয়্যার অ্যাবস্ট্রাকশন লেয়ার (HAL) দ্বারা ম্যানেজ করা হয়।

অনেক ফোন প্রস্তুতকারক ফার্স্ট-পার্টি ক্যামেরা অ্যাপ্লিকেশন তৈরি করে, যা সাধারণত তাদের ডিভাইসে আগে থেকে ইনস্টল করা থাকে। হার্ডওয়্যারের সব ক্ষমতা ব্যবহার করার জন্য, তারা ব্যক্তিগত বা লুকানো API ব্যবহার করতে পারে অথবা ড্রাইভার ইমপ্লিমেন্টেশন থেকে বিশেষ ট্রিটমেন্ট পেতে পারে যা অন্যান্য অ্যাপ্লিকেশনের অ্যাক্সেসে নেই। কিছু ডিভাইস বিভিন্ন ফিজিক্যাল ক্যামেরা থেকে ফ্রেমের ফিউজড স্ট্রিম প্রদান করে লজিক্যাল ক্যামেরার ধারণা প্রয়োগ করে, তবে শুধুমাত্র নির্দিষ্ট কিছু বিশেষাধিকারপ্রাপ্ত অ্যাপ্লিকেশনের জন্য। প্রায়ই, ফ্রেমওয়ার্কে শুধুমাত্র একটি ফিজিক্যাল ক্যামেরা দেখানো হয়। Android 9-এর আগে থার্ড-পার্টি ডেভেলপারদের পরিস্থিতি নিচের ডায়াগ্রামে দেখানো হয়েছে:

ক্যামেরার ক্ষমতা সাধারণত শুধুমাত্র বিশেষ সুবিধা প্রাপ্ত অ্যাপ্লিকেশনের জন্য উপলভ্য
ছবি ১. ক্যামেরার ক্ষমতা সাধারণত শুধুমাত্র বিশেষ সুবিধা প্রাপ্ত অ্যাপ্লিকেশনের জন্য উপলভ্য

Android 9 থেকে শুরু করে, Android অ্যাপে ব্যক্তিগত API আর অনুমোদিত নয়। ফ্রেমওয়ার্কে একাধিক ক্যামেরা ব্যবহারের সুবিধা অন্তর্ভুক্ত করার সাথে সাথে, Android-এর সেরা পদ্ধতিগুলি ফোন প্রস্তুতকারকদের একই দিকে মুখ করে থাকা সমস্ত ফিজিক্যাল ক্যামেরার জন্য একটি লজিক্যাল ক্যামেরা প্রকাশ করার জন্য দৃঢ়ভাবে সাজেস্ট করে। Android 9 এবং তার পরবর্তী ভার্সন চালু থাকা ডিভাইসে থার্ড-পার্টি ডেভেলপাররা কী দেখতে পাবেন তা নিচে দেওয়া হল:

Android 9 থেকে শুরু করে সব ক্যামেরা ডিভাইসে সম্পূর্ণ ডেভেলপার অ্যাক্সেস
ছবি ২. Android 9 থেকে শুরু করে সব ক্যামেরা ডিভাইসে ডেভেলপারের সম্পূর্ণ অ্যাক্সেস

লজিক্যাল ক্যামেরা কী প্রদান করে তা সম্পূর্ণভাবে ক্যামেরা HAL-এর OEM ইমপ্লিমেন্টেশনের উপর নির্ভর করে। যেমন, Pixel 3-এর মতো ডিভাইস তার লজিক্যাল ক্যামেরা এমনভাবে প্রয়োগ করে যাতে এটি অনুরোধ করা ফোকাল দৈর্ঘ্য এবং ক্রপ অঞ্চলের উপর ভিত্তি করে তার একটি ফিজিক্যাল ক্যামেরা বেছে নেয়।

মাল্টি-ক্যামেরা API

নতুন API নিম্নলিখিত নতুন কনস্ট্যান্ট, ক্লাস ও পদ্ধতি যোগ করে:

Android কম্প্যাটিবিলিটি ডেফিনিশন ডকুমেন্ট (CDD)-এ পরিবর্তন হওয়ার কারণে, মাল্টি-ক্যামেরা API-এর ক্ষেত্রেও ডেভেলপারদের থেকে কিছু প্রত্যাশা করা হয়। Android 9-এর আগে ডুয়াল ক্যামেরা সহ ডিভাইস ছিল, কিন্তু একাধিক ক্যামেরা একসাথে খোলার জন্য ট্রায়াল ও এরর পদ্ধতি ব্যবহার করতে হত। Android 9 ও তার পরবর্তী ভার্সনে, মাল্টি-ক্যামেরা এক সেট নিয়ম প্রদান করে যা নির্দিষ্ট করে যে একই লজিক্যাল ক্যামেরার অংশ এমন এক জোড়া ফিজিক্যাল ক্যামেরা কখন খোলা সম্ভব।

বেশিরভাগ ক্ষেত্রে, Android 9 এবং তার পরবর্তী যেকোনও ভার্সনে চলা ডিভাইসে সব ফিজিক্যাল ক্যামেরা (ইনফ্রারেডের মতো কম-সাধারণ সেন্সর টাইপ ছাড়া) এবং সহজে ব্যবহার করা যায় এমন লজিক্যাল ক্যামেরা দেখানো হয়। কাজ করবে বলে গ্যারান্টি দেওয়া হয়েছে এমন প্রতিটি স্ট্রিমের কম্বিনেশনের জন্য, লজিক্যাল ক্যামেরার একটি স্ট্রিমকে আসল ফিজিক্যাল ক্যামেরার দুটি স্ট্রিম দিয়ে পরিবর্তন করা যেতে পারে।

একসাথে একাধিক স্ট্রিম

একসাথে একাধিক ক্যামেরা স্ট্রিম ব্যবহার করা বিভাগে একটি ক্যামেরায় একসাথে একাধিক স্ট্রিম ব্যবহার করার নিয়ম কভার করা হয়েছে। একটি উল্লেখযোগ্য সংযোজন সহ, একাধিক ক্যামেরার জন্য একই নিয়ম প্রযোজ্য। CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA দুটি ফিজিক্যাল স্ট্রিমের মাধ্যমে কীভাবে লজিক্যাল YUV_420_888 বা র স্ট্রিম রিপ্লেস করতে হয় তা ব্যাখ্যা করে। অর্থাৎ, YUV বা RAW ধরনের প্রতিটি স্ট্রিম একই ধরন ও সাইজের দুটি স্ট্রিম দিয়ে প্রতিস্থাপন করা যেতে পারে। আপনি নিম্নলিখিত গ্যারান্টিযুক্ত কনফিগারেশন সহ সিঙ্গেল-ক্যামেরা ডিভাইসের ক্যামেরা স্ট্রিম দিয়ে শুরু করতে পারেন:

  • স্ট্রিম ১: YUV টাইপ, লজিক্যাল ক্যামেরা id = 0 থেকে MAXIMUM সাইজ

তারপর, একাধিক ক্যামেরা ব্যবহারের সুবিধা আছে এমন ডিভাইস আপনাকে একটি সেশন তৈরি করতে দেয় যা লজিক্যাল YUV স্ট্রিমকে দুটি ফিজিক্যাল স্ট্রিম দিয়ে প্রতিস্থাপন করে:

  • স্ট্রিম ১: YUV টাইপ, ফিজিক্যাল ক্যামেরা id = 1 থেকে MAXIMUM সাইজ
  • স্ট্রিম ২: YUV টাইপ, ফিজিক্যাল ক্যামেরা id = 2 থেকে MAXIMUM সাইজ

আপনি YUV বা RAW স্ট্রিমকে দুটি সমতুল্য স্ট্রিম দিয়ে তখনই প্রতিস্থাপন করতে পারবেন, যদি সেই দুটি ক্যামেরা একটি লজিক্যাল ক্যামেরা গ্রুপিংয়ের অংশ হয়,  যা CameraCharacteristics.getPhysicalCameraIds()-এর অধীনে তালিকাভুক্ত।

ফ্রেমওয়ার্কের মাধ্যমে দেওয়া গ্যারান্টি হল একাধিক ফিজিক্যাল ক্যামেরা থেকে একসাথে ফ্রেম পাওয়ার জন্য প্রয়োজনীয় ন্যূনতম গ্যারান্টি। অধিকাংশ ডিভাইসে অতিরিক্ত স্ট্রিম কাজ করে, কখনও কখনও একাধিক ফিজিক্যাল ক্যামেরা ডিভাইস আলাদাভাবে খোলার অনুমতিও দেয়। যেহেতু এটি ফ্রেমওয়ার্কের থেকে কোনও হার্ড গ্যারান্টি নয়, তাই এটি করার জন্য প্রতিটি ডিভাইসে টেস্টিং এবং ট্রায়াল ও এরর ব্যবহার করে টিউনিং করতে হবে।

একাধিক ফিজিক্যাল ক্যামেরা সহ সেশন তৈরি করা

একাধিক ক্যামেরা চালু থাকা ডিভাইসে ফিজিক্যাল ক্যামেরা ব্যবহার করার সময়, একটি CameraDevice (লজিক্যাল ক্যামেরা) খুলুন এবং একটি সেশনে এটির সাথে ইন্টার‍্যাক্ট করুন। API CameraDevice.createCaptureSession(SessionConfiguration config) ব্যবহার করে একক সেশন তৈরি করুন, যা API লেভেল ২৮-এ যোগ করা হয়েছে। সেশন কনফিগারেশনে একাধিক আউটপুট কনফিগারেশন থাকে, যার প্রতিটিতে আউটপুট টার্গেটের একটি সেট এবং ঐচ্ছিকভাবে, একটি বেছে নেওয়া ফিজিক্যাল ক্যামেরা আইডি থাকে।

SessionConfiguration এবং OutputConfiguration মডেল
ছবি ৩. SessionConfiguration এবং OutputConfiguration মডেল

ক্যাপচার অনুরোধের সাথে একটি আউটপুট টার্গেট যুক্ত থাকে। কোন আউটপুট টার্গেট অ্যাটাচ করা আছে তার উপর ভিত্তি করে ফ্রেমওয়ার্ক নির্ধারণ করে যে কোন ফিজিক্যাল (বা লজিক্যাল) ক্যামেরায় অনুরোধ পাঠানো হবে। আউটপুট টার্গেট যদি ফিজিক্যাল ক্যামেরা আইডির সাথে আউটপুট কনফিগারেশন হিসেবে পাঠানো আউটপুট টার্গেটের সাথে মিলে যায়, তাহলে সেই ফিজিক্যাল ক্যামেরা অনুরোধটি গ্রহণ করে প্রসেস করে।

এক জোড়া ফিজিক্যাল ক্যামেরা ব্যবহার করা

মাল্টি-ক্যামেরার জন্য ক্যামেরা API-তে আরেকটি সংযোজন হল লজিক্যাল ক্যামেরা শনাক্ত করার এবং তাদের পিছনে থাকা ফিজিক্যাল ক্যামেরা খুঁজে বের করার ক্ষমতা। আপনি একটি ফাংশন নির্ধারণ করতে পারেন যা আপনাকে ফিজিক্যাল ক্যামেরার সম্ভাব্য পেয়ার শনাক্ত করতে সাহায্য করবে, যা আপনি লজিক্যাল ক্যামেরা স্ট্রিমের একটিকে প্রতিস্থাপন করতে ব্যবহার করতে পারেন:

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<DualCamera> {
    val dualCameras = MutableList<DualCamera>()

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

জাভা

/**
 * 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<DualCamera> findDualCameras(CameraManager manager, Integer facing) {
    List<DualCamera> dualCameras = new ArrayList<>();

    List<String> 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()
            })
    }

জাভা

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-এর আগের ভার্সনে ক্যামেরা খোলার প্রসেস একই রকম। নতুন সেশন কনফিগারেশন API ব্যবহার করে ক্যাপচার সেশন তৈরি করলে, ফ্রেমওয়ার্ক নির্দিষ্ট ফিজিক্যাল ক্যামেরা আইডির সাথে নির্দিষ্ট টার্গেটকে অ্যাসোসিয়েট করে:

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<Surface>?, MutableList<Surface>?, MutableList<Surface>?>

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 using the callback
        it.createCaptureSession(sessionConfiguration)
    }
}

জাভা

/**
 * Helper class definition that encapsulates 3 sets of output targets:
 * <p>
 * 1. Logical camera
 * 2. First physical camera
 * 3. Second physical camera
 */
final class DualCameraOutputs {
    private final List<Surface> logicalCamera;
    private final List<Surface> firstPhysicalCamera;
    private final List<Surface> secondPhysicalCamera;

    public DualCameraOutputs(List<Surface> logicalCamera, List<Surface> firstPhysicalCamera, List<Surface> third) {
        this.logicalCamera = logicalCamera;
        this.firstPhysicalCamera = firstPhysicalCamera;
        this.secondPhysicalCamera = third;
    }

    public List<Surface> getLogicalCamera() {
        return logicalCamera;
    }

    public List<Surface> getFirstPhysicalCamera() {
        return firstPhysicalCamera;
    }

    public List<Surface> 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<OutputConfiguration> outputConfigsLogical = targets.getLogicalCamera().stream()
            .map(OutputConfiguration::new)
            .collect(Collectors.toList());
    List<OutputConfiguration> outputConfigsPhysical1 = targets.getFirstPhysicalCamera().stream()
            .map(s -> {
                OutputConfiguration outputConfiguration = new OutputConfiguration(s);
                outputConfiguration.setPhysicalCameraId(dualCamera.physicalId1);
                return outputConfiguration;
            })
            .collect(Collectors.toList());
    List<OutputConfiguration> 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<OutputConfiguration> 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 using the callback
            c.createCaptureSession(sessionConfiguration));
}

কোন কোন স্ট্রিমের কম্বিনেশন কাজ করে সেই সম্পর্কে তথ্য পেতে createCaptureSession দেখুন। একটি লজিক্যাল ক্যামেরায় একাধিক স্ট্রিমের জন্য স্ট্রিম একত্রিত করা হয়। একই কনফিগারেশন ব্যবহার করা এবং একই লজিক্যাল ক্যামেরার অংশ এমন দুটি ফিজিক্যাল ক্যামেরা থেকে দুটি স্ট্রিম দিয়ে সেই স্ট্রিমগুলির একটিকে প্রতিস্থাপন করার ক্ষেত্রেও কম্প্যাটিবিলিটি প্রযোজ্য।

ক্যামেরা সেশন রেডি হয়ে গেলে, বেছে নেওয়া ক্যাপচার করার অনুরোধ পাঠান। ক্যাপচার অনুরোধের প্রতিটি টার্গেট, এর সাথে যুক্ত ফিজিক্যাল ক্যামেরা থেকে ডেটা পায়, যদি কোনওটি ব্যবহার করা হয়, অথবা লজিক্যাল ক্যামেরায় ফিরে যায়।

Zoom-এর ব্যবহারের উদাহরণ

একটি স্ট্রিমে একাধিক ফিজিক্যাল ক্যামেরা মার্জ করা সম্ভব যাতে ব্যবহারকারীরা বিভিন্ন ফিজিক্যাল ক্যামেরার মধ্যে পরিবর্তন করে আলাদা ফিল্ড-অফ-ভিউয়ের অভিজ্ঞতা পেতে পারেন, কার্যকরভাবে আলাদা "জুম লেভেল" ক্যাপচার করতে পারেন।

জুম লেভেল ব্যবহারের ক্ষেত্রে ক্যামেরা সোয়্যাপ করার উদাহরণ
ছবি ৪. জুম লেভেল ব্যবহারের ক্ষেত্রে ক্যামেরা অদলবদল করার উদাহরণ (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
}

জাভা

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

জাভা

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

এখন শুধু ব্যবহারকারীকে দুটি সারফেসের মধ্যে পরিবর্তন করার জন্য UI প্রদান করতে হবে, যেমন একটি বোতাম বা 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(), ...)

জাভা

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(), ...);

এই মোডে ক্যাপচার করার অনুরোধ সেট করলে, ক্যামেরা থেকে তৈরি হওয়া ফ্রেম রেটের উপর প্রভাব পড়তে পারে। আপনি শুধুমাত্র স্থির ছবি ক্যাপচারের জন্য বিকৃতি সংশোধন সেট করার বিকল্প বেছে নিতে পারেন।