หมายเหตุ: หน้านี้อ้างอิงถึงแพ็กเกจ Camera2 เราขอแนะนำให้ใช้ CameraX เว้นแต่ว่าแอปของคุณต้องใช้ฟีเจอร์ระดับต่ำที่เฉพาะเจาะจงจาก Camera2 ทั้ง CameraX และ Camera2 รองรับ Android 5.0 (API ระดับ 21) ขึ้นไป
กล้องหลายตัวเปิดตัวพร้อมกับ Android 9 (API ระดับ 28) นับตั้งแต่เปิดตัว มีอุปกรณ์ที่รองรับ API นี้ออกสู่ตลาด กรณีการใช้งานกล้องหลายตัวจำนวนมาก เชื่อมต่อกับการกำหนดค่าฮาร์ดแวร์ที่เฉพาะเจาะจงอย่างแน่นหนา กล่าวคือ กรณีการใช้งานบางกรณีอาจใช้ร่วมกับอุปกรณ์บางเครื่องไม่ได้ ซึ่งทำให้ฟีเจอร์กล้องหลายตัวเหมาะที่จะใช้การนำส่งฟีเจอร์ Play
กรณีการใช้งานทั่วไปมีดังนี้
- ซูม: สลับไปมาระหว่างกล้องโดยขึ้นอยู่กับขอบเขตการครอบตัดหรือความยาวโฟกัสที่เลือก
- ความลึก: ใช้กล้องหลายตัวเพื่อสร้างแผนที่ความลึก
- โบเก้: ใช้ข้อมูลความลึกที่อนุมานเพื่อจำลองช่วงโฟกัสแคบๆ แบบกล้อง DSLR
ความแตกต่างระหว่างกล้องเชิงตรรกะและกล้องจริง
การทำความเข้าใจ API แบบหลายกล้องต้องอาศัยความเข้าใจความแตกต่างระหว่างกล้องเชิงตรรกะและกล้องจริง ตัวอย่างเช่น ลองพิจารณาอุปกรณ์ที่มีกล้องหลัง 3 ตัว ในตัวอย่างนี้ กล้องหลังทั้ง 3 ตัวจะถือว่าเป็นกล้องจริง กล้องแบบตรรกะคือการจัดกลุ่มกล้องจริงตั้งแต่ 2 ตัวขึ้นไป เอาต์พุตของกล้องเชิงตรรกะอาจเป็นสตรีมที่มาจากกล้องจริงตัวใดตัวหนึ่งที่อยู่เบื้องหลัง หรือสตรีมแบบรวมที่มาจากกล้องจริงที่อยู่เบื้องหลังมากกว่า 1 ตัวพร้อมกัน ไม่ว่าจะด้วยวิธีใด สตรีมจะได้รับการจัดการโดยระดับชั้นการจัดการฮาร์ดแวร์โดยตรง (HAL) ของกล้อง
ผู้ผลิตโทรศัพท์หลายรายพัฒนาแอปพลิเคชันกล้องของบุคคลที่หนึ่ง ซึ่งมักจะติดตั้งไว้ล่วงหน้าในอุปกรณ์ แอปเหล่านี้อาจใช้ API ส่วนตัวหรือ API ที่ซ่อนอยู่ หรือได้รับการดูแลเป็นพิเศษจากการติดตั้งใช้งานไดรเวอร์ที่แอปพลิเคชันอื่นๆ ไม่มีสิทธิ์เข้าถึง เพื่อใช้ความสามารถทั้งหมดของฮาร์ดแวร์ อุปกรณ์บางเครื่องใช้แนวคิดของกล้องเชิงตรรกะโดยการจัดเตรียมสตรีมเฟรมที่รวมจากกล้องจริงที่แตกต่างกัน แต่จะใช้ได้กับแอปพลิเคชันที่มีสิทธิ์บางแอปพลิเคชันเท่านั้น บ่อยครั้งที่เฟรมเวิร์กจะแสดงกล้องจริงเพียงตัวเดียวเท่านั้น สถานการณ์สำหรับนักพัฒนาแอปบุคคลที่สามก่อน Android 9 แสดงอยู่ในแผนภาพต่อไปนี้
ตั้งแต่ Android 9 เป็นต้นไป ระบบจะไม่อนุญาตให้ใช้ API ส่วนตัวในแอป Android อีกต่อไป เมื่อมีการรองรับกล้องหลายตัวในเฟรมเวิร์ก แนวทางปฏิบัติแนะนำของ Android ขอแนะนำอย่างยิ่งให้ผู้ผลิตโทรศัพท์แสดงกล้องเชิงตรรกะสำหรับกล้องจริงทั้งหมดที่หันไปในทิศทางเดียวกัน สิ่งที่นักพัฒนาแอปบุคคลที่สามควรคาดหวังว่าจะเห็นในอุปกรณ์ที่ใช้ Android 9 ขึ้นไปมีดังนี้
สิ่งที่กล้องเชิงตรรกะมีให้จะขึ้นอยู่กับการติดตั้งใช้งาน HAL ของกล้องโดย OEM ทั้งหมด ตัวอย่างเช่น อุปกรณ์อย่าง Pixel 3 ใช้กล้องเชิงตรรกะในลักษณะที่เลือกกล้องจริงตัวใดตัวหนึ่งโดยอิงตามความยาวโฟกัสและขอบเขตการครอบตัดที่ขอ
Multi-camera API
API ใหม่จะเพิ่มค่าคงที่ คลาส และเมธอดใหม่ต่อไปนี้
CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERACameraCharacteristics.getPhysicalCameraIds()CameraCharacteristics.getAvailablePhysicalCameraRequestKeys()CameraDevice.createCaptureSession(SessionConfiguration config)CameraCharacteritics.LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPEOutputConfigurationและSessionConfiguration
เนื่องจากการเปลี่ยนแปลงในเอกสารนิยามความเข้ากันได้ (CDD) ของ Android นักพัฒนาแอปจึงมีความคาดหวังบางอย่างเกี่ยวกับ Multi-Camera API ด้วย อุปกรณ์ที่มีกล้องคู่มีอยู่ก่อน Android 9 แต่การเปิดกล้องมากกว่า 1 ตัวพร้อมกันนั้นต้องอาศัยการลองผิดลองถูก ใน Android 9 ขึ้นไป กล้องหลายตัวจะมีชุดกฎเพื่อระบุว่าเมื่อใดที่สามารถเปิดกล้องจริงคู่หนึ่งซึ่งเป็นส่วนหนึ่งของกล้องเชิงตรรกะเดียวกันได้
ในกรณีส่วนใหญ่ อุปกรณ์ที่ใช้ Android 9 ขึ้นไปจะแสดงกล้องจริงทั้งหมด (ยกเว้นประเภทเซ็นเซอร์ที่พบได้น้อย เช่น อินฟราเรด) พร้อมกับกล้องเชิงตรรกะที่ใช้งานง่ายกว่า สำหรับสตรีมทุกชุดที่รับประกันว่าจะทำงานได้ สตรีม 1 รายการที่มาจากกล้องเชิงตรรกะจะแทนที่ด้วยสตรีม 2 รายการจากกล้องจริงที่เกี่ยวข้องได้
สตรีมหลายรายการพร้อมกัน
การใช้สตรีมจากกล้องหลายรายการพร้อมกันครอบคลุมกฎสำหรับการใช้สตรีมหลายรายการพร้อมกันในกล้องตัวเดียว กฎเดียวกันนี้ใช้กับกล้องหลายตัวได้ โดยมีข้อยกเว้นที่สำคัญอย่างหนึ่ง
CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
อธิบายวิธีแทนที่สตรีม YUV_420_888 เชิงตรรกะหรือสตรีมข้อมูลดิบด้วยสตรีมจริง 2 รายการ กล่าวคือ สตรีมแต่ละรายการประเภท YUV หรือ RAW สามารถแทนที่ด้วยสตรีม 2 รายการที่มีประเภทและขนาดเหมือนกันได้ คุณสามารถเริ่มต้นด้วยสตรีมจากกล้องที่มีการกำหนดค่าที่รับประกันต่อไปนี้สำหรับอุปกรณ์ที่มีกล้องตัวเดียว
- สตรีม 1: ประเภท YUV, ขนาด
MAXIMUMจากกล้องเชิงตรรกะid = 0
จากนั้นอุปกรณ์ที่รองรับกล้องหลายตัวจะช่วยให้คุณสร้างเซสชันที่แทนที่สตรีม YUV เชิงตรรกะนั้นด้วยสตรีมจริง 2 รายการได้ ดังนี้
- สตรีม 1: ประเภท YUV, ขนาด
MAXIMUMจากกล้องจริงid = 1 - สตรีม 2: ประเภท YUV, ขนาด
MAXIMUMจากกล้องจริงid = 2
คุณสามารถแทนที่สตรีม YUV หรือ RAW ด้วยสตรีมที่เทียบเท่า 2 รายการได้ก็ต่อเมื่อกล้อง 2 ตัวนั้นเป็นส่วนหนึ่งของการจัดกลุ่มกล้องเชิงตรรกะ ซึ่งแสดงอยู่ใน CameraCharacteristics.getPhysicalCameraIds()
การรับประกันที่เฟรมเวิร์กให้ไว้เป็นเพียงข้อกำหนดขั้นต่ำที่จำเป็นในการรับเฟรมจากกล้องจริงมากกว่า 1 ตัวพร้อมกัน อุปกรณ์ส่วนใหญ่รองรับสตรีมเพิ่มเติม บางครั้งยังอนุญาตให้เปิดอุปกรณ์กล้องจริงหลายเครื่องแยกกันได้ด้วย เนื่องจากเฟรมเวิร์กไม่ได้ให้การรับประกันที่แน่นอน การดำเนินการดังกล่าวจึงต้องทำการทดสอบและปรับแต่งตามอุปกรณ์โดยใช้การลองผิดลองถูก
การสร้างเซสชันที่มีกล้องจริงหลายตัว
เมื่อใช้กล้องจริงในอุปกรณ์ที่เปิดใช้กล้องหลายตัว ให้เปิด CameraDevice (กล้องเชิงตรรกะ) ตัวเดียวและโต้ตอบกับกล้องนั้นภายในเซสชันเดียว สร้างเซสชันเดียวโดยใช้ API CameraDevice.createCaptureSession(SessionConfiguration config) ซึ่งเพิ่มเข้ามาในระดับ API 28 การกำหนดค่าเซสชันมีการกำหนดค่าเอาต์พุตจำนวนหนึ่ง ซึ่งแต่ละรายการมีชุดเป้าหมายเอาต์พุต และรหัสกล้องจริงที่เลือก (ไม่บังคับ)
คำขอการจับภาพมีเป้าหมายเอาต์พุตที่เชื่อมโยงอยู่ เฟรมเวิร์กจะกำหนดว่าจะส่งคำขอไปยังกล้องจริง (หรือกล้องเชิงตรรกะ) ตัวใดโดยอิงตามเป้าหมายเอาต์พุตที่แนบมา หากเป้าหมายเอาต์พุตสอดคล้องกับเป้าหมายเอาต์พุตรายการใดรายการหนึ่งที่ส่งเป็นการกำหนดค่าเอาต์พุตพร้อมกับรหัสกล้องจริง กล้องจริงนั้นจะรับและประมวลผลคำขอ
การใช้กล้องจริง 2 ตัว
สิ่งที่เพิ่มเข้ามาใน 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
}
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<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;
}
การจัดการสถานะของกล้องจริงจะควบคุมโดยกล้องเชิงตรรกะ หากต้องการเปิด "Dual Camera" ให้เปิดกล้องเชิงตรรกะที่สอดคล้องกับกล้องจริง ดังนี้
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 เวอร์ชันที่ผ่านมา การสร้างเซสชันการจับภาพโดยใช้ 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)
}
}
Java
/**
* 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 การรวมสตรีมมีไว้สำหรับสตรีมหลายรายการในกล้องเชิงตรรกะเดียว ความเข้ากันได้นี้ครอบคลุมถึงการใช้การกำหนดค่าเดียวกันและการแทนที่สตรีมรายการใดรายการหนึ่งด้วยสตรีม 2 รายการจากกล้องจริง 2 ตัวที่เป็นส่วนหนึ่งของกล้องเชิงตรรกะเดียวกัน
เมื่อเซสชันกล้องพร้อมแล้ว ให้ส่งคำขอจับภาพที่เลือก เป้าหมายแต่ละรายการของคำขอจับภาพจะได้รับข้อมูลจากกล้องจริงที่เชื่อมโยง (หากมีการใช้งาน) หรือกลับไปใช้กล้องเชิงตรรกะ
ตัวอย่างกรณีการใช้งาน Zoom
คุณสามารถรวมกล้องจริงเข้าเป็นสตรีมเดียวเพื่อให้ผู้ใช้สลับระหว่างกล้องจริงต่างๆ เพื่อสัมผัสขอบเขตการมองเห็นที่แตกต่างกัน ซึ่งจะจับภาพ "ระดับการซูม" ที่แตกต่างกันได้อย่างมีประสิทธิภาพ
เริ่มต้นด้วยการเลือกคู่กล้องจริงเพื่อให้ผู้ใช้สลับไปมาได้ เพื่อให้ได้ผลลัพธ์สูงสุด คุณสามารถเลือกคู่กล้องที่ให้ความยาวโฟกัสต่ำสุดและสูงสุดที่มีอยู่
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 2 รายการ โดยมี 1 รายการสำหรับแต่ละสตรีม SurfaceViewsเหล่านี้จะสลับกันตามการโต้ตอบของผู้ใช้เพื่อให้มองเห็นได้เพียงรายการเดียวในแต่ละครั้ง
โค้ดต่อไปนี้แสดงวิธีเปิดกล้องเชิงตรรกะ กำหนดค่าเอาต์พุตของกล้อง สร้างเซสชันกล้อง และเริ่มสตรีมตัวอย่าง 2 รายการ
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();
}
});
สิ่งที่เหลืออยู่คือการจัดเตรียม UI ให้ผู้ใช้สลับระหว่าง 2 พื้นผิว เช่น ปุ่มหรือการแตะ 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(), ...);
การตั้งค่าคำขอจับภาพในโหมดนี้อาจส่งผลต่ออัตราเฟรมที่กล้องสร้างได้ คุณอาจเลือกตั้งค่าการแก้ไขการบิดเบือนเฉพาะการจับภาพนิ่งเท่านั้น