안드로이드 카메라 스트림 동시에 여러개 사용하기

이 포스팅은 다음과 같은 내용을 포함합니다.

  • 하나의 카메라로 여러개의 스트림을 동시에 사용하는것
  • 하나의 캡쳐 리퀘스트로 다른 속성을 가진 타겟들을 결합하는것
  • 출력타입, 출력사이즈, 하드웨어 수준을 선택하고 조회하는 방법
  • SurfaceView와 ImageReader의 Surface를 셋팅하고 사용하는 방법

여러개의 카메라 스트림을 사용하는 사례

카메라를 사용하는 앱은 두개이상의 스트림을 동시에 사용할 때가 있습니다. 각각의 스트림이 다른 해상도 또는 픽셀 포맷을 사용해야하는 경우도 있습니다. 몇 가지 일반적인 사용 사례는 다음과 같습니다.

  • 비디오 녹화 : 스트림 하나는 미리보기용,  또 다른 스트림은 인코딩되어 파일로 저장되는용
  • 바코드 스캐닝 : 스트림 하나는 미리보기용, 또 다른 스트림은 바코드 감지용
  • 계산 사진학 기술(computational photography) : 하나는 미리보기용 스트림, 얼굴 / 장면 탐지용 스트림

프레임을 처리 할 때 성능 비용이 거의 들지 않으며, 병렬 스트림 / 파이프 라인 처리를 수행 할 때 비용이 배가됩니다. CPU, GPU 및 DSP와 같은 리소스는 프레임 워크의 재처리(reprocessing) 기능을 활용할 수 있지만 메모리와 같은 리소스는 선형적으로 증가합니다.

여러개의 타겟을 갖는 요청(request)

다소 관료적인 절차를 수행하면 여러 카메라 스트림을 하나의 CameraCaptureRequest로 결합 할 수 있습니다. 이 코드 스니펫은 카메라 미리보기를 위해 하나의 스트림을 사용하고 이미지 처리를 위해 다른 스트림을 사용하여 카메라 세션을 설정하는 방법을 보여줍니다.

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

// 여러 스트림들을 사용하기 때문에 프리뷰 캡쳐 템플릿을 이용할겁니다. 왜냐하면
// 낮은 지연속도에 최적화 되있기 때문입니다. 높은 이미지 품질을 위해서는
// TEMPLATE_STILL_CAPTURE를 이용하고, 안정된 프레임을 얻기 위해서는
// TEMPLATE_RECORD을 이용하세요.

val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
val combinedRequest = session.device.createCaptureRequest(requestTemplate)

// Surface타겟들을 리퀘스트에 추가합니다.
combinedRequest.addTarget(previewSurface)
combinedRequest.addTarget(imReaderSurface)

// 간단한 경우에는 SurfaceView가 자동으로 갱신됩니다. 이미지 리더는 반드시
// 프레임에 대한 콜백을 가져야하며, 프레임을 가져올수 있도록 합니다.
// 그래서 캡쳐리퀘스트에 대한 콜백을 따로 지정하지 않도록 합니다
session.setRepeatingRequest(combinedRequest.build(), null, null)

타겟 Surface를 올바르게 구성하면, 이 코드 StreamComfigurationMap.GetOutputMinFrameDuration (int, Size) 및 StreamComfigurationMap.GetOutputStallDuration (int, Size)에 의해 결정되는 최소 FPS를 충족하는 스트림 만 생성합니다. 실제 성능은 기기마다 다르지만 Android는 출력유형(Output type), 출력 크기(Output size) 및 하드웨어 수준(Hardware level) 세 가지 변수에 따라 특정 조합을 지원할 수있는 몇 가지 보장을 제공합니다. 지원되지 않는 매개 변수 조합을 사용하면 낮은 프레임 속도로 작동 할 수 있습니다. 또는 전혀 작동하지 않아 고장 콜백 중 하나를 트리거 할 수 있습니다.  CameraDevice문서를 참조하시면 좀 더 자세한정보를 얻을 수 있으니, 꼭 꼼꼼히 전부 읽어보시기 바랍니다.

Output type(출력유형)

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

PRIV는 애플리케이션에 직접표시되지 않는 형식과 함께 StreamConfigurationMap.getOutputSizes (Class)를 사용하여 사용 가능한 사이즈를 가진 타겟을 참조합니다.

YUV는 ImageFormat.YUV_420_888포맷을 사용하는 Surface 타겟을 참조합니다.

JPEG는 ImageFormat.JPEG 포맷을 참조합니다.

RAW는 ImageFormat.RAW_SENSOR 포맷을 참조합니다.

애플리케이션의 출력 유형을 선택할 때 호환성을 최대화하는 것이 목표라면 프레임 분석에는 ImageFormat.YUV_420_888을 사용하고 스틸 이미지에는 ImageFormat.JPEG를 사용하는 것이 좋습니다. 미리보기 및 녹화를 하는 시나리오의 경우 SurfaceView, TextureView, MediaRecorder, MediaCodec 또는 RenderScript.Allocation을 사용하게됩니다. 이 경우 이미지 포맷을 지정하지 않고 호환성을 위해사용되는 실제 형식과 상관없이 ImageFormat.PRIVATE로 포함되게 됩니다.

기기에서 지원하는 CameraCharacteristics형식을 쿼리하려면 다음 코드를 사용하십시오.

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

Output size(출력 크기)

StreamConfigurationMap.getOutputSizes()를 호출하면 사용 가능한 모든 출력 크기가 나열되지만 호환성을 고려하는한 PREVIEW 와 MAXIMUM 두 가지를 고민하면됩니다. 우리는 이 크기를 상한선으로 생각할 수 있습니다. 공식문서에 어떤 사이즈의 PREVIEW가 작동한다고하면 그 크기보다 작은 크기의 것이 모두 작동합니다. MAXIMUM에도 동일하게 적용됩니다. 다음은 공식문서에서 발췌 한 내용입니다.

For the maximum size column, PREVIEW refers to the best size match to the device’s screen resolution, or to 1080p (1920×1080), whichever is smaller. RECORD refers to the camera device’s maximum supported recording resolution, as determined by CamcorderProfile. And MAXIMUM refers to the camera device’s maximum output resolution for that format or target from StreamConfigurationMap.getOutputSizes(int).

최대 크기 열의 경우 PREVIEW는 기기의 화면 해상도에 가장 적합한 크기 또는 1080p (1920×1080) 중 작은 쪽을 참조합니다. RECORD는 CamcorderProfile에 의해 결정된 카메라 장치의 최대 지원 해상도를 나타냅니다. 그리고 MAXIMUM은 StreamConfigurationMap.getOutputSizes (int)에서 해당 형식 또는 대상에 대한 카메라 장치의 최대 출력 해상도를 나타냅니다.

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

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

카메라 미리보기 및 녹화하는 경우는 카메라 프레임 워크 자체에서 형식을 처리하므로 대상 클래스를 사용하여 지원되는 크기를 결정해야합니다.

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

MAXIMUM사이즈를 구하는것은 쉽습니다. 출력크기를 정렬하여서 가장 큰것을 골라내면 됩니다.

fun <T>getMaximumOutputSize(
        characteristics: CameraCharacteristics, targetClass: Class<T>, format: Int? = null):
        Size {
    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)

    // 만약 이미지 포맷이 주어진다면 이미지포맷을 이용하여 사이즈를 결정하세요. 
    // 그렇지 않다면 타겟 클래스를 사용하세요.
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)
    return allSizes.sortedWith(compareBy { it.height * it.width }).reversed()[0]
}

PREVIEW 크기를 얻으려면 조금 더 생각해야합니다. PREVIEW는 기기의 화면 해상도에 가장 적합한 크기 일치 또는 1080p (1920×1080) 중 작은 쪽을 참조합니다. 종횡비가 화면의 종횡비와 정확하게 일치하지 않을 수 있으므로 전체 화면 모드로 표시하려는 경우 레터박스 또는 화면자르기(cropping)를 스트림에 적용해야 할 수도 있습니다. 올바른 PREVIEW 크기를 얻으려면 디스플레이가 회전 될 수 있다는 점을 고려하면서 사용 가능한 출력 크기와 디스플레이 크기를 비교해야합니다. 이 코드에서는 크기 비교를 좀 더 쉽게 해주는 SmartSize 클래스를 만들었습니다.

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

fun getDisplaySmartSize(context: Context): SmartSize {
    val windowManager = context.getSystemService(
            Context.WINDOW_SERVICE) as WindowManager
    val outPoint = Point()
    windowManager.defaultDisplay.getRealSize(outPoint)
    return SmartSize(outPoint.x, outPoint.y)
}

fun <T>getPreviewOutputSize(
        context: Context, characteristics: CameraCharacteristics, targetClass: Class<T>,
        format: Int? = null): Size {

    // 어떤게 더 작은 지 찾음 : 화면크기 또는 1080p
    val hdSize = SmartSize(1080, 720)
    val screenSize = getDisplaySmartSize(context)
    val hdScreen = screenSize.long >= hdSize.long || screenSize.short >= hdSize.short
    val maxSize = if (hdScreen) screenSize else hdSize

    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)

    // 얻을수 있는 크기들을 구하고 그것들을 정렬한다
    val validSizes = allSizes
            .sortedWith(compareBy { it.height * it.width })
            .map { SmartSize(it.width, it.height) }.reversed()

    // maxSize와 같거나 작은 가장 큰 출력크기를 구한다
    return validSizes.filter {
        it.long <= maxSize.long && it.short <= maxSize.short }[0].size
}

 

Hardware level(하드웨어 수준)

런타임에 사용 가능한 기능을 결정하기 위해 카메라 애플리케이션에 필요한 가장 중요한 정보는 지원되는 하드웨어 수준입니다. 다시 한번 강조하지만 공식문서에 잘나와있으니 꼭 읽어보시기 바랍니다.

The supported hardware level is a high-level description of the camera device’s capabilities, summarizing several capabilities into one field. Each level adds additional features to the previous one, and is always a strict superset of the previous level. The ordering is LEGACY < LIMITED < FULL < LEVEL_3.

지원되는 하드웨어 수준은 카메라 장치의 기능에 대한 상위 수준의 설명(high-level description)으로 여러 기능을 하나의 필드로 요약합니다. 각 레벨은 이전 레벨에 추가 기능을 추가하며 항상 이전 레벨의 엄격한 수퍼셋(확대집합)입니다. 순서는 LEGACY <LIMITED <FULL <LEVEL_3입니다.

CameraCharacteristics 객체를 사용하면 다음과 같은 단일 명령문으로 하드웨어 레벨을 검색 할 수 있습니다.

val characteristics: CameraCharacteristics = ...

// 하드웨어 수준은 아래의 주석된 리스트중 하나 입니다.
// - 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)

3개의 변수를 하나로 모아봅시다.

출력 유형, 출력 크기 및 하드웨어 수준을 이해하면 유효한 스트림 조합을 결정할 수 있습니다. 

Legacy 하드웨어 수준

LEGACY-level guaranteed configurations
Target 1 Target 2 Target 3 사례
Type Max size Type Max size Type Max size
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 JPEG MAXIMUM 미리보기와 프로세싱도 하면서 사진도 캡쳐하고 싶을때

Limited 하드웨어 수준

LIMITED-level additional guaranteed configurations
Target 1 Target 2 Target 3 사례
Type Max size Type Max size Type Max size
PRIV PREVIEW PRIV RECORD 고해상도 비디오 녹화와 미리보기
PRIV PREVIEW YUV RECORD 고해상도의 비디오 프로세싱과 미리보기
YUV PREVIEW YUV RECORD 두개의 비디오 입력받아 프로세싱하기
PRIV PREVIEW PRIV RECORD JPEG RECORD 고해상도의 녹화와 함께 스냅샷 찍기
PRIV PREVIEW YUV RECORD JPEG RECORD 고해상도의 비디오와 스냅샷 프로세싱하기
YUV PREVIEW YUV PREVIEW JPEG MAXIMUM 두개의 프로세싱과 함께 캡쳐하기

Full 하드웨어 수준

FULL-level additional guaranteed configurations
Target 1 Target 2 Target 3 사례
Type Max size Type Max size Type Max size
PRIV PREVIEW PRIV MAXIMUM 최고 해상도 수준의 GPU 프로세싱과 미리보기
PRIV PREVIEW YUV MAXIMUM 최고 해상도로 앱내에서 미리보기와 함께 프로세싱하기
YUV PREVIEW YUV MAXIMUM 최대해상도의 두개의 비디오 입력을 받아 프로세싱하기
PRIV PREVIEW PRIV PREVIEW JPEG MAXIMUM 비디오 녹화와 함께 최대해상도의 스냅샷 찍기
YUV 640x480 PRIV PREVIEW YUV MAXIMUM 일반적인 비디오 녹화와 추가로 최대해상도의 프로세싱하기
YUV 640x480 YUV PREVIEW YUV MAXIMUM 미리보기와 함께 두개의 입력에 대한 최대 해상도 처리

LEGACY가 가장 낮은 하드웨어 수준이므로 위의 표에서 Camera2를 지원하는 모든 기기 (예 : API 레벨 21 이상)가 올바른 구성을 사용하여 최대 3개의 동시 스트림을 출력 할 수 있음을 알 수 있습니다. 그러나 메모리, CPU 및 발열과 같은 성능을 제한하는 다른 제약 조건을 유발하는 오버 헤드가 발생할 경우가 있기 때문에 많은 장치에서 최대 가용 처리를 못하는 경우도 있습니다.

이제 프레임 워크가 지원하는 두 개의 동시 스트림을 설정하는 데 필요한 지식을 얻었으므로 타겟의 출력 버퍼의 구성을 조금 더 자세히 파헤칠 수 있습니다. 예를 들어, LEGACY 하드웨어 레벨의 장치를 대상으로한다면, 두 개의 타겟 출력 Surface를 설정할 수 있습니다. 하나는 ImageFormat.PRIVATE를 사용하고 다른 하나는 ImageFormat.YUV_420_888을 사용하면 되는것이지요. 미리보기 크기를 사용하는 경우 위 표와 같이 지원되는 조합이어야 합니다. 아래에 정의된 코드를 사용하면 카메라 ID에 필요한 미리보기 크기를 얻는 것이 매우 간단합니다.

val characteristics: CameraCharacteristics = ...
val context = this as Context  // assuming we 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)

반드시 주어진 콜백을 이용하여서 SurfaceView가 준비가 될때까지 기다려야합니다.

val surfaceView = findViewById<SurfaceView>(...)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        // We do not need to specify image format, and it will be considered of type PRIV
        // Surface is now ready and we could use it as an output target for CameraSession
    }
    ...
})

SurfaceHolder.setFixedSize()를 호출하여 SurfaceView를 카메라 출력 크기와 일치 시키도록 할 수도 있지만, GitHub의 HDR 뷰 파인더 샘플에서 FixedAspectSurfaceView와 비슷한 접근 방식을 취하는 것이 UI의 관점에서 더 좋을 수 있습니다. 종횡비와 사용 가능한 공간을 고려하면서 활동 변경이 트리거되는시기를 자동으로 조정합니다.

원하는 포맷의 ImageReader로부터 다른 Surface를 설정하는것은 심지어 더 쉽습니다. 콜백을 기다릴 필요가 없기 때문이죠.

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)

ImageReader와 같은 blocking 타겟 버퍼를 사용할 때는 들어오는 프레임을 처리해줘야합니다.

imageReader.setOnImageAvailableListener({
        val frame =  it.acquireNextImage()
        // frame처리는 이곳에서 합니다
        it.close()
}, null)

LEGACY 하드웨어 수준의 장치는 가장 낮은 공통 분모를 목표로하고 있음을 명심해야합니다. 조건부 브랜칭을 추가하고 하드웨어 수준이 LIMITED 인 장치에서 출력 대상 표면 중 하나에 RECORD 크기를 사용하거나 하드웨어 수준이 FULL 인 장치의 경우 MAXIMUM 크기까지 사용 할 수 있습니다.

카테고리: GraphicsKotlin

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.