9 minute read

해당 포스트는 이전 포스트와 연결되므로 이전 포스트를 먼저 보고 오시기를 추천드립니다.

Preview

CameraX의 UseCase중 하나인 Preview는 카메라 센서로부터 이미지 데이터를 끊임없이 받아오는 데이터 생산자로 세션이 유지되는 동안 데이터의 흐름이 지속됩니다.

이때 Preview는 UI에 그림을 직접적으로 그리는것이 아니라 세션이 유지되는 동안 센서로부터 이미지 프레임을 끊임없이 받아오는 파이프라인입니다. 즉, Preview는 데이터를 전달하기만 할 뿐 최종적으로 UI에 어떻게 그려질지는 알지 못합니다. 실제적으로 UI에 그리는 역할은 SurfaceProvider 인터페이스를 구현한 객체가 수행합니다.

이러한 설계를 통해 이미지 프레임을 받아내는 것과 UI 그리기를 분리할 수 있게 되며, 이는 곧 UI 그리기 뿐만 아니라 OpenGL 처리 또는 스트리밍 서버 전송 등과 같은 다양한 방식으로 유연하게 확장하는 구조를 만들어 나갈 수 있게 됩니다.

이러한 설계 덕분에 개발자는 미리보기 데이터를 단순히 화면에 띄우는 것 외에도, 실시간으로 OpenGL 처리를 하거나 스트리밍 서버로 전송하는 등 유연하게 확장할 수 있습니다.

Preview 구성 전략

Preview를 연동 및 활용하는 방식을 알아가기 전, 가장 먼저 Preview를 구성하는 방식에 대해 알아야 합니다.

CameraX의 UseCase로 Preview를 추가할 때, 개발자는 여러가지 옵션을 지정하여 구성할 수 있으며, 이때 손쉽게 구성할 수 있도록 Builder 패턴을 제공하고 있습니다.

아래에서는 PreviewBuilder 패턴을 활용하여 적용할 수 있는 Preview 구성 전략들에 대해 다루겠습니다.

ResolutionSelector

ResolutionSelector는 해상도를 지정하기 위해 사용되는 옵션입니다. CameraX 1.3.0 이전 버전에서는 setTargetResolution이나 setTargetAspectRatio를 통해 해상도를 직접 지정했지만, 여러 기기 스펙에 대응하기 어려운 문제가 있어 ResolutionSelector가 도입되었습니다.

ResolutionSelector는 해상도에 대한 요구사항과 우선순위를 정의하여 특정 해상도를 강제하지 않으면서 특정 메커니즘에 따라 해상도를 결정합니다. CameraX가 최종 해상도를 결정하는 과정은 다음 3단계를 거칩니다.

  1. 수집: 디바이스 카메라가 지원하는 모든 출력 크기 목록을 가져와 후보 리스트를 만듭니다.

  2. 필터링 및 정렬: 개발자가 ResolutionSelector에 설정한 종횡비(Aspect Ratio)해상도(Resolution) 전략에 맞춰 후보 리스트를 필터링하고 우선순위대로 정렬합니다.

  3. 최종 선택: 바인딩된 다른 UseCase들과의 호환성, 하드웨어 제약 사항 등을 고려하여 최종적으로 사용할 해상도를 결정합니다. 이때 UseCase의 getResolutionInfo() 기능을 사용하여 어떤 해상도가 최종적으로 선택되는지 확인할 수 있습니다.


앞서 2번 과정에서 언급된 전략에 대해 조금 더 자세하게 알아보겠습니다. ResolutionSelector는 우선 순위를 정하기 위한 전략을 보유하고 있습니다. 전략은 아래와 같습니다.

AspectRatioStrategy(종횡비 전략)

16:9 또는 4:3 와 같이 종횡비를 결정하는 전략입니다. 해상도 후보를 정렬할 때 가장 먼저 고려되는 기준으로 AspectRatioStrategy는 이후에 설명할 ResolutionStrategy 보다 우선순위가 높습니다.

AspectRatioStrategy를 생성하기 위해서는 mPreferredAspectRatiomFallbackRule을 지정해야합니다. mPreferredAspectRatio는 종횡비를 의미하며, AspectRatio.Ratio 타입의 상수를 사용하여 16:9 또는 4:3 비율의 종횡비를 지정할 수 있습니다.

또 하나의 필드인 mFallbackRule지정한 종횡비를 사용할 수 없을 경우에 대한 대안 규칙을 의미하며, AspectRatioFallbackRule 타입의 상수를 사용하여 규칙을 지정할 수 있습니다. AspectRatioFallbackRule 타입 중 FALLBACK_RULE_NONE 의 경우 지정된 종횡비를 사용할 수 없을 경우 예외를 발생시키는 엄격한 규칙이며, FALLBACK_RULE_AUTO 는 지정된 종횡비를 사용할 수 없을 경우 차선의 종횡비를 선택하도록 하는 유연한 규칙입니다.

아래 이미지를 통해 AspectRatio.RatioAspectRatioFallbackRule 타입에 속해있는 상수들을 확인하실 수 있습니다.


아래의 코드는 AspectRatioStrategy의 정의 일부 입니다.

해당코드를 자세히 살펴보면, AspectRatioStrategy는 생성자를 통해 원하는 값을 주입하도록 구성할 수 있지만 한편으로 RATIO_4_3_FALLBACK_AUTO_STRATEGYRATIO_16_9_FALLBACK_AUTO_STRATEGY 같이 즉각적으로 사용할 수 있는 상수를 제공하고 있다는 사실을 알 수 있습니다.

public static final @NonNull AspectRatioStrategy RATIO_4_3_FALLBACK_AUTO_STRATEGY =
            new AspectRatioStrategy(RATIO_4_3, FALLBACK_RULE_AUTO);
            
public static final @NonNull AspectRatioStrategy RATIO_16_9_FALLBACK_AUTO_STRATEGY =
            new AspectRatioStrategy(RATIO_16_9, FALLBACK_RULE_AUTO);
            
...

@AspectRatio.Ratio
private final int mPreferredAspectRatio;
@AspectRatioFallbackRule
private final int mFallbackRule;

// AspectRatioStrategy 생성자를 통해,
// mPreferredAspectRatio,mFallbackRule를 정의합니다

public AspectRatioStrategy(@AspectRatio.Ratio int preferredAspectRatio,
        @AspectRatioFallbackRule int fallbackRule) {
    mPreferredAspectRatio = preferredAspectRatio;
    mFallbackRule = fallbackRule;
}

AspectRatioStrategy가 제공하는 각 상수의 의미는 아래와 같습니다.

RATIO_16_9_FALLBACK_AUTO_STRATEGY

16:9 비율을 최우선으로 찾습니다. 만약 기기가 16:9를 지원하지 않을 경우, 4:3 등 다른 비율 중 가장 적절한 것으로 Fallback 합니다.

RATIO_4_3_FALLBACK_AUTO_STRATEGY

4:3 비율을 최우선으로 찾습니다. 사진 촬영 앱에 적합합니다.

ResolutionStrategy(해상도 전략)

앞서 소개한 AspectRatioStrategy가 화면의 비율을 결정하였다면, ResolutionStrategy는 그 비율 안에서 구체적인 픽셀 수를 결정합니다. 해당 전략은 Bound SizeFallback Rule 두 가지 요소로 구성되며, 각각 mBoundSizemFallbackRule 필드로 대응됩니다. mBoundSize 는 android의 Size 클래스의 객체를 의미하며, mFallbackRule 의 경우 원하는 Bound Size가 정확히 지원하지 않을 때 대안을 찾는 규칙입니다. mFallbackRule 또한 앞서 소개한 AspectRatioFallbackRule 처럼 ResolutionFallbackRule 타입으로 5가지의 규칙이 존재합니다.

1. FALLBACK_RULE_NONE

원하는 크기가 없을 때, 대체 크기를 선택하지 않고, 예외를 발생시킵니다.

2. FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER

원하는 크기가 없을 때, 더 큰 고해상도를 먼저 찾고 없을 경우 저해상도로 내려갑니다.

3. FALLBACK_RULE_CLOSEST_HIGHER

원하는 크기가 없을 때, 가장 가까운 고해상도를 찾고, 고해상도가 없으면 저해상도로 내려가지 않고 예외를 발생시킵니다.

4. FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER

반대로 원하는 크기가 없을 때, 가장 가까운 저해상도를 먼저 찾고, 없으면 고해상도로 올라갑니다.

5. FALLBACK_RULE_CLOSEST_LOWER

가장 가까운 저해상도를 찾고, 저해상도가 없으면 예외를 발생시킵니다.

AspectRatioStrategy와 마찬가지로 ResolutionStrategy 또한 자주 쓰이는 상수를 별도로 정의하고 있습니다. 매번 Bound Size와 Fallback Rule을 조합하기 번거롭다면, CameraX가 미리 정의해 둔 상수를 사용할 수 있습니다.

ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY

비율에 맞는 해상도 중 가장 큰 크기를 무조건 선택합니다.

아래 ResolutionStrategy의 코드 일부를 보면 mBoundSize의 기본값이 null 이며, 상수를 사용하지 않으면 Size를 null로 적용할 수 없음을 알 수 있습니다. 이러한 메커니즘으로 인해 ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY 를 적용한 인스턴스는 유일해지고 내부에서 별도의 로직을 타게 만들 수 있습니다.

public final class ResolutionStrategy {
   
    // camerax가 제공하는 해상도 전략 설정 특수 상수 값
    public static final @NonNull ResolutionStrategy HIGHEST_AVAILABLE_STRATEGY =
            new ResolutionStrategy();
            
    ...
    
    private @Nullable Size mBoundSize = null;
    private int mFallbackRule = ResolutionStrategy.FALLBACK_RULE_NONE;
    
    ...
    
    private ResolutionStrategy() {}
    
    ...
    
    //생성자
    public ResolutionStrategy(@NonNull Size boundSize, @ResolutionFallbackRule int fallbackRule) {
        mBoundSize = boundSize;
        mFallbackRule = fallbackRule;
    }
    
    ...
    
}
            

ResolutionFilter(해상도 필터)

앞서 나열된 AspectRatioStrategy와 ResolutionStrategy 만으로는 개발자가 원하는 스펙을 지정할 수 없는 경우가 존재할 수 있습니다. 이에 대하여 특별한 로직이 필요한 경우 ResolutionFilter을 사용하여 해결할 수 있습니다.

ResolutionFilter는 앞서 전략을 통해 1차적으로 필터링한 후보 해상도 목록 중 마음에 안 드는 것을 제거하거나 선호하는 순서대로 리스트를 재정렬하여 반환할 수 있습니다. 개발자는 ResolutionFilter를 사용하고자 할 때 해당 인터페이스의 filter() 메서드를 구현해야 합니다.

filter의 인자 중 supportedSizes는 앞선 전략에 의해 1차적으로 걸러진 해상도 목록을 의미하며, rotationDegrees는 현재 Preview의 목표 회전 각도를 의미합니다.

AllowedResolutionMode(허용 해상도 모드)

AllowedResolutionMode는 ResolutionSelector의 마지막 옵션으로 허용할 수 있는 해상도의 범위를 지정합니다.

최신 스마트폰에 탑재된 카메라들은 200MP 규모의 초고해상도 센서를 탑재하는 경우가 많지만, 실제로 200MP 규모를 전부 처리하려면 매우 긴 처리시간이 소요될 수 있습니다. 한편으로 실제 간단한 카메라 기능에서는 고해상도의 이미지보다는 처리속도가 중요한 요인으로 작용할 수 있습니다. AllowedResolutionMode는 이러한 상황을 처리할 수 있는 수단으로 고해상도를 해상도 후보 목록에 포함시킬지를 결정하는 옵션입니다.

MP 1MP는 MegaPixel의 약자로, 1MP는 100만 화소(Pixel)와 동일합니다. 즉, 12MP는 1200만개, 200MP는 2억개의 픽셀로 이루어짐을 의미합니다. MP가 높을수록 고해상도의 이미지이기 때문에 사진을 확대해도 깨지지 않고, 대형인화에 유리하게 됩니다. 단, 개별 픽셀의 크기가 작아지기 때문에 어두운 환경에서 픽셀마다 빛을 수용할 수 있는 확률이 줄어들게 됩니다. 이런 상황에서 센서가 인식하는 이미지는 어두워 보이게 될 것이며, 신호를 증폭시킬 경우에는 노이즈가 커지게됩니다. 이러한 문제를 해결하기 위해 등장한 개념이 픽셀비닝(Pixel Bining)입니다.

픽셀비닝 픽셀 비닝은 고해상도 이미지 센서에서 인접한 동일 색상의 픽셀들을 하나의 슈퍼 픽셀로 그룹화하여 처리하는 기술로 처리속도와 야간상황의 노이즈 문제를 개선해주는 이점이 있습니다. 처리 방식은 복잡하기 때문에 해당 포스트에서 다루지 않으며, 이에 대한 자세한 내용은 아래 래퍼런스를 참고하시길 바랍니다.

옵션의 기본값은 PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION 이며, 선택할 수 있는 옵션은 아래와 같습니다.

PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION

높은 해상도(Higher Resolution)보다는 촬영 속도(Capture Rate)가 중요할 때 사용하는 옵션으로, 일반적인 해상도 옵션만 사용합니다.

PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE

촬영 속도(Capture Rate)보다는 높은 해상도(Higher Resolution)이 중요할 때 사용되는 옵션으로, 일반적인 해상도에 추가적으로 처리속도가 느린 고해상도 목록까지 수집합니다. 이때 추가된 고해상도 목록은 픽셀비닝을 풀어낸 해상도들을 의미합니다.


내용이 길어졌지만 궁극적으로 Preview의 Builder를 통해 ResolutionSelector을 설정(setResolutionSelector)할 수 있으며, ResolutionSelector를 구성할 때 아래와 같이 여러가지 옵션을 조합하여 구성할 수 있습니다.

val preview: Preview = Preview.Builder()
    .setResolutionSelector(
        ResolutionSelector.Builder()
            .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
            .setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)
            .build()
    )
    .build()

TargetRotation

이미지 데이터의 회전 정보를 설정합니다. 이후 포스트에서 다룰 PreviewView를 쓰면 대부분 자동으로 처리되지만, 회전 상태를 직접적으로 다룰 때 사용합니다. 회전 상태는 Android의 Surface 값 4가지 중 하나를 사용합니다.

  • Surface.ROTATION_0
  • Surface.ROTATION_90
  • Surface.ROTATION_180
  • Surface.ROTATION_270

현재 디스플레이의 회전값을 사용하여 targetRotation을 적용하는 방식이 안전하며 마찬가지로 값을 별도로 설정하지 않을 경우, 기본 디스플레이 회전 값을 기본으로 사용하게 됩니다.

setResolutionSelctor()와 마찬가지로 setTargrtRotation() 또한 Preview.Builder()를 통해 등록할 수있습니다.

val preview: Preview = Preview.Builder()
    .setTargetRotation(Surface.ROTATION_270)
    .build()

TargetFrameRate

Preview의 FrameRate를 조절할 수 있는 속성입니다. 단, 디바이스의 스펙과 다른 UseCase에서 설정된 FrameRate에 따라 최종적으로 결정되는 FrameRate가 달라질 수 있습니다. TargetFrameRate는 Range<Integer> 를 사용해 범위를 지정하는 방식으로 정의할 수 있습니다.

Preview.Builder()에서 해당 기능을 사용한다면, 아래와 같이 적용할 수 있습니다.

val preview: Preview = Preview.Builder()
    .setTargetFrameRate(Range(30, 60))
    .build()

FrameRate

FrameRate는 디스플레이 장치나 카메라가 1초 동안 보여주는 이미지(프레임)의 개수를 의미합니다. 프레임 레이트의 단위는 FPS이며, 30fps의 경우 1초에 30장의 이미지가 지나감을 의미합니다. 숫자가 높을 수록 움직임이 부드럽고 자연스럽습니다.

추가적으로 카메라에서의 FrameRate는 한 장을 찍는데 걸리는 최대 시간(셔터 속도)으로 치환할 수 있습니다. 즉, 60fps는 한장을 찍는데, 최대 1/60초 밖에 사용할 수 없음을 의미합니다. 만약 사진을 찍기위한 셔터를 1/60 밖에 열지 못한다면 빛을 받는 시간이 줄어들게 되며 어두운 곳에서의 영상이 급격하게 어두워질 수 있습니다.

FrameRate 위키

DynamicRange

Preview UseCase는 해상도 외에도 색상의 깊이와 밝기 범위를 결정하는 Dynamic Range를 설정할 수 있습니다.

DynamicRange

DynamicRange는 화면의 가장 밝은 부분과 가장 어두운 부분 사이의 범위를 얼마나 세밀하게 표현할 수 있는지를 의미합니다. 일반적으로 SDR은 8-bit 색 깊이를 사용하며, HDR(High Dynamic Range) 콘텐츠는 더 넓은 밝기 범위를 표현하기 위해 PQ/HLG 같은 HDR 인코딩과 함께 더 높은 10-bit 색 깊이를 사용합니다

DynamicRange 위키

DynamicRange 타입은 DynamicRangeEncodingBitDepth 두 가지 요소로 구성되며, 각각 mEncodingmBitDepth 필드로 대응됩니다. DynamicRange는 생성자를 활용하여 생성이 가능할 뿐 아니라 사전 정의된 상수를 사용할 수 있습니다.

public DynamicRange(  
        @DynamicRangeEncoding int encoding,  
        @BitDepth int bitDepth) {  
    mEncoding = encoding;  
    mBitDepth = bitDepth;  
}

DynamicRange의 2가지 요소인 DynamicRangeEncodingBitDepth 모두 사용할 수 있는 상수 값들을 정의하고 있습니다.

DynamicRangeEncoding의 경우 아래와 같은 값들로 구성되어있습니다.

  • ENCODING_UNSPECIFIED
  • ENCODING_SDR
  • ENCODING_HDR_UNSPECIFIED
  • ENCODING_HLG
  • ENCODING_HDR10
  • ENCODING_HDR10_PLUS
  • ENCODING_DOLBY_VISION

BitDepth의 경우 아래와 같은 값들로 구성되어있습니다.

  • BIT_DEPTH_UNSPECIFIED = 0;
  • BIT_DEPTH_8_BIT = 8;
  • BIT_DEPTH_10_BIT = 10;

그리고 이들을 조합하여 구성된 DynamicRange 의 상수값들이 정의되어 있습니다.

public static final @NonNull DynamicRange UNSPECIFIED = new DynamicRange(ENCODING_UNSPECIFIED,  
        BIT_DEPTH_UNSPECIFIED);  
  
public static final @NonNull DynamicRange SDR = new DynamicRange(ENCODING_SDR, BIT_DEPTH_8_BIT);  
  
public static final @NonNull DynamicRange HDR_UNSPECIFIED_10_BIT =  
        new DynamicRange(ENCODING_HDR_UNSPECIFIED, BIT_DEPTH_10_BIT);  
  
 
public static final @NonNull DynamicRange HLG_10_BIT =  
        new DynamicRange(ENCODING_HLG, BIT_DEPTH_10_BIT);  
  

public static final @NonNull DynamicRange HDR10_10_BIT = new DynamicRange(ENCODING_HDR10,  
        BIT_DEPTH_10_BIT);  
  
 
public static final @NonNull DynamicRange HDR10_PLUS_10_BIT =  
        new DynamicRange(ENCODING_HDR10_PLUS, BIT_DEPTH_10_BIT);  
  

public static final @NonNull DynamicRange DOLBY_VISION_10_BIT =  
        new DynamicRange(ENCODING_DOLBY_VISION, BIT_DEPTH_10_BIT);  
  

public static final @NonNull DynamicRange DOLBY_VISION_8_BIT =  
        new DynamicRange(ENCODING_DOLBY_VISION, BIT_DEPTH_8_BIT);

위에 목록에서 보았듯이, DynamicRange에는 UNSPECIFIED 라는 특수한 상수값이 존재합니다. 해당 상수는 다른 UseCase에 적용되어 있는 DynamicRange를 바탕으로 요구사항에 맞는 최적의 DynamicRange를 결정하여 충돌을 방지합니다. DynamicRange.UNSPECIFIED Preview DynamicRange의 기본 값이며, 특수한 요구사항이 존재하지 않을 경우 해당 값을 변경하지 않고 유지하는 것을 권장합니다.

DynamicRange를 Preview.Builder()에서 적용한다면 아래와 같은 방식으로 사용될 수 있습니다.

val preview: Preview = Preview.Builder()
    .setDynamicRange(DynamicRange.HLG_10_BIT)
    .build()

TargetName

TargetName은 디버깅 로깅에서 UseCase의 이름을 부여하기 위해 사용되는 설정으로 여러 개의 Preview를 동시에 사용할 때 로그를 식별하기 위헤 사용됩니다.

별도로 설정하지 않을 경우 클래스의 표준 이름과 랜덤 UUID로 조합된 이름으로 부여됩니다.

Preview.Builder()에서 setTargetName()을 사용하여 지정할 수 있습니다.

val preview: Preview = Preview.Builder()
    .setTargetName("HelloPreview")
    .build()

setMirrorMode

Preview의 거울 모드를 설정합니다.

MirrorMode를 적용하기 위해서는 정의된 상수 값 중 하나를 선택하여 사용해야합니다. 사용할 수 있는 값은 아래와 같습니다.

MIRROR_MODE_OFF

미러효과를 제거합니다.

MirrorMode.MIRROR_MODE_ON

미러효과를 적용합니다.

MirrorMode.MIRROR_MODE_ON_FRONT_ONLY

미러 모드의 기본 값으로 미러 효과는 카메라의 렌즈 방향이 CameraSelector.LES_FACING_FRONT일 때만 적용되도록 설정합니다.

Preview.Builder()에서 setMirrorMode()에 거울모드 관련 상수 값을 지정하여 옵션을 구성할 수있습니다.

val preview: Preview = Preview.Builder()
    .setMirrorMode(MIRROR_MODE_ON)
    .build()

글의 호흡이 길어져서 포스트를 마무리 짓고자 합니다. 이번 포스트에서는 UseCase중 하나인 Preview에 대한 소개와 그에 대한 구성전략을 알아보았습니다.

Preview의 기능들 중 낯선 부분이 많았겠지만, 이러한 부분들은 다른 UseCase들의 개념들을 익히고 CameraX에 조합하다보면 무슨의미인지 점차 이해하게 될 것 입니다.

서론에서 간단하게 소개하였 듯이 Preview는 surfaceProvider 다른 여러가지 모듈들과 결합하여 여러 요구사항에 대응할 수 있는 모듈입니다. 이후 포스트에서는 surfaceProvider를 활용하여 카메라 뿐만 아니라 여러가지 모듈에 결합하는 사례들을 다뤄볼 것이며, 한편으로 CameraX의 다른 UseCase들을 순차적으로 소개하고 최종적으로 이들을 결합하는 방향으로도 글을 이어나갈 것입니다.

이후에 Preview UseCase에 대한 궁금증이 생긴다면 다시 이곳으로 돌아와 글을 한번 더 읽어보는 것을 추천드리며, 글을 마칩니다.