카메라 캡처 세션 및 요청

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

Android 지원 기기 하나에 여러 대의 카메라가 있을 수 있습니다. 각 카메라는 CameraDevice이며 CameraDevice는 둘 이상의 스트림을 동시에 출력할 수 있습니다.

이렇게 하는 이유는 한 스트림, 즉 CameraDevice에서 수신되는 순차 카메라 프레임은 뷰파인더 표시와 같은 특정 작업에 최적화되어 있고, 다른 스트림은 사진을 찍거나 동영상 녹화를 하는 데 사용될 수 있기 때문입니다. 스트림은 카메라에서 나오는 원시 프레임을 한 번에 한 프레임씩 처리하는 병렬 파이프라인 역할을 합니다.

그림 1. 범용 카메라 앱 빌드 이미지 (Google I/O '18)

병렬 처리는 CPU, GPU 또는 기타 프로세서에서 사용할 수 있는 처리 능력에 따라 성능 제한이 있을 수 있음을 나타냅니다. 파이프라인이 수신 프레임을 따라잡지 못하면 드롭을 시작합니다.

각 파이프라인에는 고유한 출력 형식이 있습니다. 들어오는 원시 데이터는 각 파이프라인과 관련된 암시적 로직에 의해 적절한 출력 형식으로 자동 변환됩니다. 이 페이지의 코드 샘플 전반에 사용된 CameraDevice은 구체적이지 않으므로 계속 진행하기 전에 먼저 사용 가능한 모든 카메라를 열거형합니다.

CameraDevice를 사용하여 CameraDevice에만 해당하는 CameraCaptureSession를 만들 수 있습니다. CameraDeviceCameraCaptureSession를 사용하여 각 원시 프레임의 프레임 구성을 수신해야 합니다. 구성은 자동 초점, 조리개, 효과, 노출과 같은 카메라 속성을 지정합니다. 하드웨어 제약으로 인해 카메라 센서에서 항상 단일 구성만 활성화되며, 이를 활성 구성이라고 합니다.

그러나 스트림 사용 사례는 기존에 CameraDevice를 사용하여 캡처 세션을 스트리밍하는 방식을 개선하고 확장합니다. 따라서 특정 사용 사례에 맞게 카메라 스트림을 최적화할 수 있습니다. 예를 들어 영상 통화를 최적화할 때 배터리 수명을 개선할 수 있습니다.

CameraCaptureSessionCameraDevice에 바인딩된 가능한 모든 파이프라인을 설명합니다. 세션이 생성되면 파이프라인을 추가하거나 제거할 수 없습니다. CameraCaptureSession는 활성 구성이 되는 CaptureRequest의 큐를 유지합니다.

CaptureRequest는 큐에 구성을 추가하고 사용 가능한 파이프라인을 하나, 하나 이상 또는 모두 선택하여 CameraDevice에서 프레임을 수신합니다. 캡처 세션의 수명 동안 캡처 요청을 여러 번 보낼 수 있습니다. 각 요청은 활성 구성과 원시 이미지를 수신하는 출력 파이프라인 집합을 변경할 수 있습니다.

성능 향상을 위한 스트림 사용 사례 사용

스트림 사용 사례는 Camera2 캡처 세션의 성능을 개선하는 방법입니다. 하드웨어 기기에 매개변수 조정을 위한 더 많은 정보를 제공하여 특정 작업에 더 나은 카메라 환경을 제공합니다.

이렇게 하면 카메라 기기에서 각 스트림의 사용자 시나리오를 기반으로 카메라 하드웨어 및 소프트웨어 파이프라인을 최적화할 수 있습니다. 스트림 사용 사례에 관한 자세한 내용은 setStreamUseCase를 참고하세요.

스트림 사용 사례를 사용하면 CameraDevice.createCaptureRequest()에서 템플릿을 설정하는 것 외에도 특정 카메라 스트림이 사용되는 방법을 더 자세히 지정할 수 있습니다. 이를 통해 카메라 하드웨어는 특정 사용 사례에 적합한 품질 또는 지연 시간 절충점을 기반으로 미세 조정, 센서 모드 또는 카메라 센서 설정과 같은 매개변수를 최적화할 수 있습니다.

스트림 사용 사례에는 다음이 포함됩니다.

  • DEFAULT: 모든 기존 애플리케이션 동작을 다룹니다. 스트림 사용 사례를 설정하지 않는 것과 같습니다.

  • PREVIEW: 뷰파인더 또는 인앱 이미지 분석에 권장됩니다.

  • STILL_CAPTURE: 고화질 고해상도 캡처에 최적화되어 있으며 미리보기와 같은 프레임 속도를 유지할 수 없습니다.

  • VIDEO_RECORD: 기기에서 지원하고 애플리케이션에서 사용 설정한 경우 고화질 이미지 흔들림 보정을 포함한 고화질 동영상 캡처에 최적화되어 있습니다. 이 옵션은 최고 품질의 안정화 또는 기타 처리를 위해 실시간과 상당한 지연이 있는 출력 프레임을 생성할 수 있습니다.

  • VIDEO_CALL: 전력 소모가 우려되는 장시간 실행되는 카메라에 권장됩니다.

  • PREVIEW_VIDEO_STILL: 소셜 미디어 앱 또는 단일 스트림 사용 사례에 권장됩니다. 다목적 스트림입니다.

  • VENDOR_START: OEM 정의 사용 사례에 사용됩니다.

CameraCaptureSession 만들기

카메라 세션을 만들려면 앱이 출력 프레임을 쓸 수 있는 하나 이상의 출력 버퍼를 제공합니다. 각 버퍼는 파이프라인을 나타냅니다. 카메라 사용을 시작하기 전에 이 작업을 실행해야 합니다. 그래야 프레임워크가 기기의 내부 파이프라인을 구성하고 필요한 출력 타겟에 프레임을 전송하기 위한 메모리 버퍼를 할당할 수 있습니다.

다음 코드 스니펫은 SurfaceView에 속하고 ImageReader에 속하는 출력 버퍼 두 개를 사용하여 카메라 세션을 준비하는 방법을 보여줍니다. PREVIEW 스트림 사용 사례를 previewSurface에 추가하고 STILL_CAPTURE 스트림 사용 사례를 imReaderSurface에 추가하면 기기 하드웨어가 이러한 스트림을 더욱 최적화할 수 있습니다.

Kotlin


// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
// analysis
// 3. OpenGL Texture or TextureView, although discouraged for maintainability
      reasons
// 4. RenderScript.Allocation, if you want to do parallel processing
val surfaceView = findViewById<SurfaceView>(...)
val imageReader = ImageReader.newInstance(...)

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated()
val previewSurface = surfaceView.holder.surface
val imReaderSurface = imageReader.surface
val targets = listOf(previewSurface, imReaderSurface)

// Create a capture session using the predefined targets; this also involves
// defining the session state callback to be notified of when the session is
// ready
// Setup Stream Use Case while setting up your Output Configuration.
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun configureSession(device: CameraDevice, targets: List<Surface>){
    val configs = mutableListOf<OutputConfiguration>()
    val streamUseCase = CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    targets.forEach {
        val config = OutputConfiguration(it)
        config.streamUseCase = streamUseCase.toLong()
        configs.add(config)
    }
    ...
    device.createCaptureSession(session)
}

Java


// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
      analysis
// 3. RenderScript.Allocation, if you want to do parallel processing
// 4. OpenGL Texture or TextureView, although discouraged for maintainability
      reasons
Surface surfaceView = findViewById<SurfaceView>(...);
ImageReader imageReader = ImageReader.newInstance(...);

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated()
Surface previewSurface = surfaceView.getHolder().getSurface();
Surface imageSurface = imageReader.getSurface();
List<Surface> targets = Arrays.asList(previewSurface, imageSurface);

// Create a capture session using the predefined targets; this also involves defining the
// session state callback to be notified of when the session is ready
private void configureSession(CameraDevice device, List<Surface> targets){
    ArrayList<OutputConfiguration> configs= new ArrayList()
    String streamUseCase=  CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    for(Surface s : targets){
        OutputConfiguration config = new OutputConfiguration(s)
        config.setStreamUseCase(String.toLong(streamUseCase))
        configs.add(config)
}

device.createCaptureSession(session)
}

아직은 카메라의 활성 구성을 정의하지 않았습니다. 세션이 구성되면 캡처 요청을 만들고 전달하여 이를 수행할 수 있습니다.

버퍼에 기록될 때 입력에 적용되는 변환은 각 타겟의 유형(Surface이어야 함)에 따라 결정됩니다. Android 프레임워크는 활성 구성의 원시 이미지를 각 타겟에 적합한 형식으로 변환하는 방법을 알고 있습니다. 변환은 특정 Surface의 픽셀 형식과 크기에 따라 제어됩니다.

프레임워크는 최선을 다하려고 하지만 일부 Surface 구성 조합이 작동하지 않아 세션이 생성되지 않거나, 요청을 전달할 때 런타임 오류가 발생하거나, 성능이 저하되는 등의 문제가 발생할 수 있습니다. 프레임워크는 기기, 노출 영역, 요청 매개변수의 특정 조합을 보장합니다. 자세한 내용은 createCaptureSession() 문서를 참고하세요.

단일 CaptureRequests

각 프레임에 사용되는 구성은 카메라로 전송되는 CaptureRequest에 인코딩됩니다. 캡처 요청을 만들려면 사전 정의된 템플릿 중 하나를 사용하거나 TEMPLATE_MANUAL을 사용하여 전체 제어 권한을 지정하면 됩니다. 템플릿을 선택할 때는 요청에 사용할 출력 버퍼를 하나 이상 제공해야 합니다. 사용하려는 캡처 세션에 이미 정의된 버퍼만 사용할 수 있습니다.

캡처 요청은 빌더 패턴을 사용하여 개발자가 자동 노출, 자동 초점, 렌즈 조리개다양한 옵션을 설정할 수 있는 기회를 제공합니다. 필드를 설정하기 전에 CameraCharacteristics.getAvailableCaptureRequestKeys()를 호출하여 기기에서 특정 옵션을 사용할 수 있는지 그리고 사용 가능한 자동 노출 모드와 같은 적절한 카메라 특성을 확인하여 원하는 값이 지원되는지 확인하세요.

수정 없이 미리보기용으로 설계된 템플릿을 사용하여 SurfaceView의 캡처 요청을 만들려면 CameraDevice.TEMPLATE_PREVIEW를 사용합니다.

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequest.addTarget(previewSurface)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest.Builder captureRequest =
    session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
captureRequest.addTarget(previewSurface);

캡처 요청이 정의되었으면 이제 카메라 세션에 전달할 수 있습니다.

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest = ...  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null
session.capture(captureRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest captureRequest = ...;  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed
// capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null
session.capture(captureRequest.build(), null, null);

출력 프레임이 특정 버퍼에 들어가면 캡처 콜백이 트리거됩니다. 대부분의 경우 ImageReader.OnImageAvailableListener와 같은 추가 콜백은 콜백에 포함된 프레임이 처리될 때 트리거됩니다. 이 시점에서 지정된 버퍼에서 이미지 데이터를 가져올 수 있습니다.

CaptureRequest 반복

단일 카메라 요청은 간단하지만 실시간 미리보기나 동영상을 표시하는 데는 그리 유용하지 않습니다. 이 경우 단일 프레임이 아닌 연속된 프레임 스트림을 수신해야 합니다. 다음 코드 스니펫은 세션에 반복 요청을 추가하는 방법을 보여줍니다.

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest = ...  // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until
// the
// session is torn down or session.stopRepeating() is called
// session.setRepeatingRequest(captureRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest captureRequest = ...;  // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until the
// session is torn down or session.stopRepeating() is called
// session.setRepeatingRequest(captureRequest.build(), null, null);

반복 캡처 요청을 사용하면 카메라 기기가 제공된 CaptureRequest의 설정을 사용하여 이미지를 지속적으로 캡처합니다. 또한 Camera2 API를 사용하면 GitHub의 이 Camera2 샘플 저장소와 같이 사용자가 반복되는 CaptureRequests를 전송하여 카메라에서 동영상을 캡처할 수 있습니다. 또한 GitHub의 Camera2 슬로 모션 동영상 샘플 앱에서 볼 수 있듯이 반복 버스트 CaptureRequests을 사용하여 고속 (슬로 모션) 동영상을 캡처하여 슬로 모션 동영상을 렌더링할 수 있습니다.

CaptureRequests 인터리브

반복 캡처 요청이 활성화된 상태에서 뷰파인더를 표시하고 사용자가 사진을 캡처할 수 있도록 두 번째 캡처 요청을 전송하기 위해, 진행 중인 반복 요청을 중지하지 않아도 됩니다. 대신 반복 요청이 계속 실행되는 동안 반복되지 않는 캡처 요청을 실행합니다.

사용된 모든 출력 버퍼는 세션이 처음 생성될 때 카메라 세션의 일부로 구성되어야 합니다. 반복 요청은 단일 프레임 또는 버스트 요청보다 우선순위가 낮으므로 다음 샘플이 작동합니다.

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it
val repeatingRequest = session.device.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW)
repeatingRequest.addTarget(previewSurface)
session.setRepeatingRequest(repeatingRequest.build(), null, null)

// Some time later...

// Create the single request and dispatch it
// NOTE: This can disrupt the ongoing repeating request momentarily
val singleRequest = session.device.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE)
singleRequest.addTarget(imReaderSurface)
session.capture(singleRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it
CaptureRequest.Builder repeatingRequest =
session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
repeatingRequest.addTarget(previewSurface);
session.setRepeatingRequest(repeatingRequest.build(), null, null);

// Some time later...

// Create the single request and dispatch it
// NOTE: This can disrupt the ongoing repeating request momentarily
CaptureRequest.Builder singleRequest =
session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
singleRequest.addTarget(imReaderSurface);
session.capture(singleRequest.build(), null, null);

그러나 이 방법에는 단일 요청이 발생하는 정확한 시점을 알 수 없다는 단점이 있습니다. 다음 그림에서 A가 반복 캡처 요청이고 B가 단일 프레임 캡처 요청이면 세션에서는 다음과 같은 방식으로 요청 대기열을 처리합니다.

그림 2. 진행 중인 카메라 세션의 요청 큐 이미지

요청 B가 활성화되기 전 A의 마지막 반복 요청과 다음번에 A가 다시 사용될 시점 사이의 지연 시간이 보장되지 않으므로 일부 건너뛴 프레임이 있을 수 있습니다. 이 문제를 완화하기 위해 취할 수 있는 몇 가지 조치가 있습니다.

  • 요청 A의 출력 타겟을 추가하여 B를 요청합니다. 이렇게 하면 B의 프레임이 준비되면 A의 출력 타겟에 복사됩니다. 예를 들어 안정적인 프레임 속도를 유지하기 위해 동영상 스냅샷을 실행할 때 이 기능은 매우 중요합니다. 위 코드에서는 요청을 빌드하기 전에 singleRequest.addTarget(previewSurface)를 추가합니다.

  • 제로 셔터 랙과 같이 이러한 특정 시나리오에 맞게 설계된 템플릿 조합을 사용하세요.