여러 카메라 스트림 동시 사용

참고: 이 페이지에서는 Camera2 패키지에 관해 다룹니다. 앱에 Camera2의 특정 하위 수준 기능이 필요한 경우가 아니라면 CameraX를 사용하는 것이 좋습니다. CameraX와 Camera2는 모두 Android 5.0(API 수준 21) 이상을 지원합니다.

카메라 애플리케이션은 둘 이상의 프레임 스트림을 동시에 사용할 수 있습니다. 스트림마다 다른 프레임 해상도나 픽셀 형식이 필요한 경우도 있습니다. 몇 가지 일반적인 사용 사례는 다음과 같습니다.

  • 동영상 녹화: 미리보기용 스트림과 인코딩되어 파일에 저장되는 스트림.
  • 바코드 스캔: 미리보기용 스트림과 바코드 감지용 스트림.
  • 컴퓨팅 사진: 미리보기용 스트림과 얼굴/장면 감지용 스트림

프레임을 처리하는 데는 적지 않은 성능 비용이 발생하며, 병렬 스트림 또는 파이프라인 처리를 수행할 때는 이 비용이 곱해집니다.

CPU, GPU, DSP와 같은 리소스는 프레임워크의 재처리 기능을 활용할 수 있지만 메모리와 같은 리소스는 선형적으로 증가합니다.

요청당 여러 대상

여러 카메라 스트림을 단일 CameraCaptureRequest로 결합할 수 있습니다. 다음 코드 스니펫은 카메라 미리보기용 스트림과 이미지 처리를 위한 다른 스트림으로 카메라 세션을 설정하는 방법을 보여줍니다.

Kotlin

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)

Java

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

타겟 노출 영역을 올바르게 구성하면 이 코드는 StreamComfigurationMap.GetOutputMinFrameDuration(int, Size)StreamComfigurationMap.GetOutputStallDuration(int, Size)에 의해 결정된 최소 FPS를 충족하는 스트림만 생성합니다. 실제 성능은 기기마다 다르지만 Android는 출력 유형, 출력 크기하드웨어 수준의 세 가지 변수에 따라 특정 조합을 지원하기 위한 몇 가지 보증을 제공합니다.

지원되지 않는 변수 조합을 사용하면 낮은 프레임 속도에서 작동할 수 있습니다. 그렇지 않으면 실패 콜백 중 하나를 트리거합니다. createCaptureSession 문서에서는 보장되는 방식을 설명합니다.

출력 유형

출력 유형은 프레임이 인코딩되는 형식을 나타냅니다. 가능한 값은 PRIV, YUV, JPEG, RAW입니다. createCaptureSession 문서에서 이를 설명합니다.

애플리케이션의 출력 유형을 선택할 때 호환성 극대화가 목표라면 프레임 분석에는 ImageFormat.YUV_420_888를 사용하고 스틸 이미지에는 ImageFormat.JPEG를 사용하세요. 미리보기 및 녹화 시나리오의 경우 SurfaceView, TextureView, MediaRecorder, MediaCodec, RenderScript.Allocation를 사용할 가능성이 높습니다. 이러한 경우 이미지 형식을 지정하지 마세요. 호환성을 위해 내부적으로 사용되는 실제 형식과 관계없이 ImageFormat.PRIVATE로 계산됩니다. CameraCharacteristics가 지정된 기기에서 지원하는 형식을 쿼리하려면 다음 코드를 사용하세요.

Kotlin

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

Java

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

출력 크기

사용 가능한 모든 출력 크기StreamConfigurationMap.getOutputSizes()에 의해 나열되지만 호환성과 관련된 것은 PREVIEWMAXIMUM 두 가지뿐입니다. 크기는 상한값으로 기능합니다. PREVIEW 크기의 항목이 작동하면 PREVIEW보다 작은 크기도 작동합니다. 이는 MAXIMUM의 경우에도 마찬가지입니다. CameraDevice 문서에서 이러한 크기를 설명합니다.

사용 가능한 출력 크기는 형식 선택에 따라 다릅니다. CameraCharacteristics와 형식이 주어지면 다음과 같이 사용 가능한 출력 크기를 쿼리할 수 있습니다.

Kotlin

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

Java

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

카메라 미리보기 및 녹화 사용 사례에서 타겟 클래스를 사용하여 지원되는 크기를 결정합니다. 형식은 카메라 프레임워크 자체에서 처리합니다.

Kotlin

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

Java

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

MAXIMUM 크기를 가져오려면 출력 크기를 영역으로 정렬하고 가장 큰 크기를 반환합니다.

Kotlin

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

Java

 @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를 정의합니다.

Kotlin

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

Java

/** 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 객체를 사용하면 단일 구문으로 하드웨어 수준을 가져올 수 있습니다.

Kotlin

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)

Java

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

모든 조각 맞추기

출력 유형, 출력 크기 및 하드웨어 수준을 사용하여 유효한 스트림 조합을 확인할 수 있습니다. 다음 차트는 LEGACY 하드웨어 수준의 CameraDevice에서 지원되는 구성의 스냅샷입니다.

타겟 1 타겟 2 타겟 3 샘플 사용 사례
유형 최대 크기 유형 최대 크기 유형 최대 크기
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 미리보기 및 인앱 처리
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM 스틸 캡처 및 인앱 처리

LEGACY은 가장 낮은 하드웨어 수준입니다. 이 표는 Camera2 (API 수준 21 이상)를 지원하는 모든 기기가 올바른 구성을 사용하여 최대 3개의 동시 스트림을 출력할 수 있으며 메모리, CPU 또는 열 제약 조건과 같은 오버헤드 제한 성능이 너무 많지 않은 경우 이를 보여줍니다.

앱은 출력 버퍼 타겟팅도 구성해야 합니다. 예를 들어 LEGACY 하드웨어 수준의 기기를 타겟팅하려면 두 개의 타겟 출력 표면을 설정할 수 있습니다. 하나는 ImageFormat.PRIVATE을 사용하고 다른 하나는 ImageFormat.YUV_420_888를 사용합니다. 이는 PREVIEW 크기를 사용하는 동안 지원되는 조합입니다. 이 주제의 앞부분에서 정의한 함수를 사용하여 카메라 ID에 필요한 미리보기 크기를 가져오려면 다음 코드가 필요합니다.

Kotlin

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)

Java

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가 준비될 때까지 기다려야 합니다.

Kotlin

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

Java

SurfaceView surfaceView = findViewById <SurfaceView>(...);

surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
                // You do not need to specify image format, and it will be considered of type PRIV
                // Surface is now ready and you could use it as an output target for CameraSession
            }
            ...
        });

SurfaceHolder.setFixedSize()를 호출하여 SurfaceView를 강제로 카메라 출력 크기와 일치시킬 수도 있고, GitHub의 카메라 샘플에 있는 일반 모듈AutoFitSurfaceView과 유사한 접근 방식을 취할 수도 있습니다. 이 모듈은 가로세로 비율과 사용 가능한 공간을 모두 고려하여 절대 크기를 설정하고 활동 변경이 트리거될 때 자동으로 조정됩니다.

ImageReader의 다른 노출 영역을 원하는 형식으로 설정하는 것이 더 쉽습니다. 기다릴 콜백이 없기 때문입니다.

Kotlin

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)

Java

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와 같은 차단 타겟 버퍼를 사용할 때는 사용 후 프레임을 삭제합니다.

Kotlin

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

Java

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

LEGACY 하드웨어 수준은 가장 낮은 공통분모 기기를 타겟팅합니다. 조건부 분기를 추가하고 하드웨어 수준이 LIMITED인 기기의 출력 타겟 노출 영역 중 하나에 RECORD 크기를 사용하거나 하드웨어 수준이 FULL인 기기의 경우 MAXIMUM 크기로 늘릴 수도 있습니다.