Galaxy 수채화 현상 완전 정복! Raw Image 를 활용한 Android Camera Module 개발 [1/3]

Galaxy 수채화 현상 완전 정복! Raw Image 를 활용한 Android Camera Module 개발 [1/3]

💡 TL;DR
Galaxy S22/S23 수채화 현상으로 헬스케어 앱의 키트 판독 정확도가 15%까지 떨어졌다. CameraX에서 Camera2 API로 전환하고 Raw Image 처리 기반 커스텀 Camera Module을 개발해 판독 정확도를 95%까지 향상시켰다. 이 시리즈에서는 문제 분석부터 해결 과정, 성능 최적화까지 전 과정을 다룬다.

Galaxy S22 이후부터 수채화 현상이 나타나는 경우가 많이 있다. 정확히 말하면 글자가 뭉개지는 느낌, 사물의 윤곽이 흐릿한 느낌 등을 이야기하는 것이다. 과도한 후처리(Post-processing) 알고리즘을 사용하고 있을 것이다.(아래 링크 참조) 아메리카노 한잔 앞에 두고 찍는 셀카는 잡티가 뭉개져야만 더 아름답게 나올수도 있을 것이다. 안타깝게도 지금 회사 앱에서는 디테일이 생명이었다.

갤럭시 S22 카메라 촬영물 뭉개짐(텍스쳐 필터 느낌)
안녕하세요 갤럭시 시리즈로 사진 활동을 좀 깊게 하는 사람입니다. S7을 이용하다가 작년에 카메라 성능 개선을 기대하며 S22울트라로 기기 변경을 했습니다. 디지털 하이엔드 카메라는 기동성이 떨어져서 요즘은 스마트폰 카메라 성능이 좋아져서 폰카메라를 선호합니다. 주로 필름 카메라를 메인으로 쓰고 스냅 용도로 폰카메라를 쓰고 있습니다. 예전에 S7로 찍은 사진을 최대 1미터 이상(긴폭) 까지 뽑아도 큰 문제가 없어 사진 전시도 하고 했었습니다. S7에서 기변 후 현재는 S22U를 많이 사용하는데 이번에 전시회를 목적으로 그동안 S…
갤럭시s22 카메라 G8보다 못하네요 ㄷㄷ : 클리앙
갤럭시s22로 바꾸고 어째 사진을 찍는데 글자가 다 뭉개지는 겁니다. 분명히 4.5천만 화소정도면 노이즈는 더할지라도 디테일이라도 살아있어야 하는데요, 그래서 이전에 G8이랑 비교를 해봤습니다.위가 G8, 아래가 S22 둘 다 갤럭시에서 최대크기로 확대한거고 아래가 갤럭시라 4.5천만화소라 더 크게 보이긴 하죠. 그런데... 일단 디테일 더 뭉개지는 것도 그렇지만 색상이 침범합니다. 디테일 향상 모드켜면 될까요? 글자를 합성해서 엉뚱한 문자로 바꿉니다. ㅡㅡㅋ 결론은 4.5천만화소는 사기같습니다.(800만 뻥튀기인 듯) 게다가 색상까지 섞어버리고 색도 빠집니다. ㅡㅡㅋ 끔찍합니다. 3년전 G8보다 못한 폰이라니... 심지어 게임도 더 느려요... 해상도도 더 낮고 좋은건 120Hz 디스플레이뿐... 다시는 갤럭시 사기 싫어집니다. 차라리 소니 살래요.


임신을 준비하는 여성들을 위해 작은 키트를 카메라로 판독하는 Andorid Camera Module 을 개발 중이었다. 개발 도중, 이마에 진땀 홍수가 났다. 이유는, Galaxy S22 이후의 모델에서 약양성 키트를 대부분 음성으로 판독하는 것이다. 당시(2024년 8월) 아이폰 같은 경우는 거의 100% 확률로 약양성을 캐치해 냈는데, 유독 Galaxy S22 이후 시리즈 에서만 평균 15% 만 약양성으로 판독하는 것이다.

정해진 시간 내에 무조건 해결해야 했다. 개발 능력도 중요했지만, 현재 회사의 바이오 전문가 분들과의 소통, 해결하기 위해 깊이있는 Searching 능력(Diggin?)이 필요했다. 눈 앞에 안개는 자욱했다.

결국엔 해결했다. 95% 까지 정확도를 높였으며, 앱은 정해진 시기에 론칭을 했다. 개발 라이프 가운데 생각보다 뿌듯한 이 고군분투 과정을 나누면 좋을 것 같았다. 안개 자욱한 이 문제에 자그마한 표지판을 두면 사고는 덜 할 것이다. 글의 흐름은 다음과 같다.

1번: Raw Image (DNG) 활용이 '수채화 현상' 의 해답

2번: Camera2 API 활용, SurfaceView Customize 하기, DNG 활용

3번: DNGCreator 활용, Galaxy A 시리즈의 한계.

4번: ArucoMarker 적용 및 회고

Galaxy 수채화 현상 완전 정복! Raw Image 를 활용한 Android Camera Module 개발[1/3] 에서는 Raw Image 와 Camera2 API 활용 방법(1번, 2번) 에 이야기 하고, 3번은[2/3] 에서, 4번은 [3/3] 에서 이야기하겠다.

Raw Image 를 가져오는 것이, 내가 찾은 해결책이다. Raw Image 를 가져오기 위해서는 Android 최신 Camera Library 인 CameraX 를 사용하지 않고, Camera2 API를 사용해야 한다. 최신 라이브러리에서 해결될 거 같은 데 왜 예전 Library 를 사용하느냐고 묻는 사람이 있을 것이다. 아니다. 자동차에 비유하면 CameraX 는 자동변속기, CameraX 는 수동 변속기다. Raw 데이터 접근, 각 프레임의 노출 시간 수동 조절, 렌즈 포커스 거리 직접 제어같은 경우는 Camera2 에서 할 수 있다. CameraX는 Camera2 '기반' 으로 만들어졌다.

// 카메라의 정보를 담기 위한 간단한 데이터 클래스
data class CameraCapability(
    val cameraId: String,
    val orientation: String,
    val supportsRaw: Boolean
)

/**
 * 기기에서 사용 가능한 모든 카메라를 순회하며
 * RAW 지원 여부를 포함한 주요 정보를 반환합니다.
 *
 * @param cameraManager 시스템의 CameraManager 인스턴스
 * @return 각 카메라의 기능 정보가 담긴 List
 */
fun enumerateCameras(cameraManager: CameraManager): List<CameraCapability> {
    val cameraList = mutableListOf<CameraCapability>()

    // 1. Camera2 API와 호환되는 카메라 ID 목록만 필터링합니다.
    //    이는 오래된 기기와의 호환성을 확보하는 좋은 습관입니다.
    val compatibleCameraIds = cameraManager.cameraIdList.filter { cameraId ->
        val characteristics = cameraManager.getCameraCharacteristics(cameraId)
        val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
        capabilities?.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) ?: false
    }

    // 2. 호환 카메라 목록을 순회하며 각 카메라의 특성을 확인합니다.
    compatibleCameraIds.forEach { cameraId ->
        val characteristics = cameraManager.getCameraCharacteristics(cameraId)
        val orientation = getLensOrientationString(
            characteristics.get(CameraCharacteristics.LENS_FACING)
        )

        // 3. 핵심: RAW 지원 여부를 '이중'으로 확인합니다.
        val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!
        val outputFormats = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.outputFormats!!

        // 조건 1: 카메라의 기능 목록에 'RAW 기능'이 포함되어 있는가?
        val hasRawCapability = capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)
        // 조건 2: 카메라의 출력 포맷에 'RAW_SENSOR 포맷'이 포함되어 있는가?
        val canOutputRaw = outputFormats.contains(ImageFormat.RAW_SENSOR)

        val supportsRaw = hasRawCapability && canOutputRaw
        
        if (supportsRaw) {
            Log.d("CameraCheck", "카메라($cameraId)는 RAW를 지원합니다.")
        }

        cameraList.add(
            CameraCapability(
                cameraId = cameraId,
                orientation = orientation,
                supportsRaw = supportsRaw
            )
        )
    }
    return cameraList
}

/**
 * 카메라 렌즈 방향(facing) 값을 문자열로 변환하는 헬퍼 함수
 */
private fun getLensOrientationString(facing: Int?): String = when (facing) {
    CameraCharacteristics.LENS_FACING_BACK -> "Back"
    CameraCharacteristics.LENS_FACING_FRONT -> "Front"
    CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
    else -> "Unknown"
}

위 코드는 Raw 지원 여부를 확인하는 코드다. 가장 중요한 것 중에 하나는 이중으로 확인 로직을 개발해야 한다. 1번은 기능 플래그 확인이다. REQUEST_AVAILABLE_CAPABILITIES 목록 안에 REQUEST_AVAILABLE_CAPABILITIES_RAW 플래그가 있는 지 확인한다. 이것은 제조사가 "우리는 카메라 RAW 기능을 지원합니다" 라고 공식적으로 이야기 하는 것이다. 2번은 출력 포맷 확인이다. SCALER_STREAM_CONFIGURATION_MAP 에서 실제로 이 카메라가 출력할 수 있는 이미지 포맷 목록(outputFormats) 을 가져온다. 그리고 이 목록에 ImageFormat.RAW_SENSOR 가 있는지 확인한다. 참고로 orientation 을 확인 하는 이유는 뒷면 카메라만 사용하기 때문에 해당 내용도 넣어뒀다. 위 코드를 바탕으로 Raw Image 가능하면, Camera2Fragment 에서 Camera Module 을 실행했고, Raw Image 가 불가능하면, CameraXFragment 에서 실행했다. (추후에 Galaxy A 관련한 내용을 이야기하면서 구체적으로 나누겠다.)

Camera2 API 는 SurfaceView 를 사용해야 한다. SurfaceView 는 날 것 그대로의 캔버스라고 생각하면 된다. "날 것 그대로" 이기 때문에, 개발자가 직접 생명 주기를 관리하고, 카메라 데이터 스트림을 수동으로 연결해줘야 한다.

/**
 * 지정된 화면 비율에 맞게 크기가 자동으로 조절되는 SurfaceView.
 * 카메라 프리뷰의 왜곡 현상을 방지하기 위해 사용됩니다.
 */
class AutoFitSurfaceView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : SurfaceView(context, attrs, defStyle) {

    private var aspectRatio = 0f

    /**
     * 이 뷰가 유지해야 할 화면 비율을 설정합니다.
     * onMeasure()에서 이 비율을 기반으로 뷰의 크기를 다시 계산합니다.
     *
     * @param width  카메라 프리뷰의 원본 너비
     * @param height 카메라 프리뷰의 원본 높이
     */
    fun setAspectRatio(width: Int, height: Int) {
        require(width > 0 && height > 0) { "Size cannot be negative." }
        aspectRatio = width.toFloat() / height.toFloat()
        // 레이아웃을 다시 계산하도록 시스템에 요청합니다.
        requestLayout()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val viewWidth = MeasureSpec.getSize(widthMeasureSpec)
        val viewHeight = MeasureSpec.getSize(heightMeasureSpec)
        if (aspectRatio == 0f) {
            // 화면 비율이 설정되지 않았다면, 측정된 크기를 그대로 사용합니다.
            setMeasuredDimension(viewWidth, viewHeight)
        } else {
            // 화면 비율이 설정되었다면, '센터 크롭' 방식으로 크기를 재계산합니다.
            val newWidth: Int
            val newHeight: Int
            
            // 현재 뷰의 방향에 따라 실제 비율을 조정합니다.
            val actualRatio = if (viewWidth > viewHeight) aspectRatio else 1f / aspectRatio

            if (viewWidth < viewHeight * actualRatio) {
                newWidth = (viewHeight * actualRatio).roundToInt()
                newHeight = viewHeight
            } else {
                newWidth = viewWidth
                newHeight = (viewWidth / actualRatio).roundToInt()
            }
            
            // 최종적으로 계산된 크기를 뷰의 크기로 설정합니다.
            setMeasuredDimension(newWidth, newHeight)
        }
    }
}

SurfaceView 를 Customize 해서 AutoFitSurfaceView 클래스를 만들었다. 아니, 사실 가져왔다. 구글의 Camera 관련 Sample (Git) 에서 대부분 가져왔다. Android 의 새로운 기능을 (Jetpack Compose, Jetpack Navigation 등) 을 배워보려면 우선은 Google Sample Git Repository 를 확인하는 습관을 들여야 한다. 각설하고, 화면 사용자에게 보여지는 화면의 찌그러짐 현상을 해결하기 위해 AutoFitSurfaceView 를 사용하는 것이다.

구체적으로 만약에 Camera2 API 의 getOutputSize 메소드를 통해 목록을 받아와서 1920 x 1080 이 선택되었다고 가정해보자. AutoFitSurfaceView 는 전달 받은 비율을 수동적으로 따르면서 화면이 이상하게 보이지 않게(찌그러지지 않게) 처리해준다.

Raw 촬영의 화룡점정은 DNG(DngCreator) 이다. DNG(Digital Negative) 는 어도비(Adobe) 사가 만든 Raw 이미지 파일 형식의 표준 규격이다. 이름 그대로 '디지털 필름 원판' 이라고 생각하면 쉽다. 기존의 JPEG 가 완성된 요리라고 하면, RAW 는 신선한 밀키트라고 할 수 있다. DNG 는 이러한 신선한 밀키트를 담은 포장 규격같은 것이다.

카메라 제조사마다 자신들만의 '밀키트 포장 방식' (SONY 는 .ARW, 캐논은 .CR3) 이 제각각이라는 점이다. 이 때문에 A사(임의의 회사) 카메라 Raw 파일이 열리지 않는 문제가 있었다고 한다. 이 때 Adobe 가 나서서 "앞으로 우리 모두 이 포장 규격으로 통일하겠다" 라고 제안한 게 DNG 라고 한다.

DNG 의 역할
1. 높은 호환성: DNG는 공개된 표준이므로, 포토샵, 라이트룸은 물론 안드로이드, iOS 시스템 자체에서도 DNG 파일을 안정적으로 읽고 해석가능.
2. 모든 정보 포함: 단순히 픽셀 데이터만 담는 것이 아니라, 어떤 카메라로, 어떤 렌즈와 어떤 설정값으로 찍었는지에 대한 '레시피 노트(메타데이터)' 까지 파일 안에 함께 저장.

Read more

Galaxy 수채화 현상 완전 정복! Raw Image 를 활용한 Android Camera Module 개발 [3/3]

Galaxy 수채화 현상 완전 정복! Raw Image 를 활용한 Android Camera Module 개발 [3/3]

앞선 글에서 Raw Image를 DNG Creator 를 활용함으로 최종적으로 JPEG 파일까지 만드는 과정에 대해 이야기를 나눴다. 추가적으로 우리 키트 배경지에는 ArUco 마커가 그려져 있다. AI 개발자와의 논의 끝에 도입한 내용이였으며, 회사에서도 배경지에 추가하기로 하였다. 굳이 사진 촬영을 하는 가운데 ArUco 마커를 도입한 이유와 과정에 대해 설명해보려고 한다. // 파일명: ArucoDetector.kt

By Jeongsu Choi
from https://r1.community.samsung.com/갤럭시-s/s22-울트라-카메라-수채화-현상-개선-요청드립니다-타-기기와-비교-결과-심층분석-꼭-답변-부탁드려요

Galaxy 수채화 현상 완전 정복! Raw Image 를 활용한 Android Camera Module 개발 [2/3]

앞의 글에서 Camera2 API 를 사용하는 이유, RAW 가능 여부 확인, SurfaceView 커스터마이징, DNG 란 무엇인가, 에 대해 이야기 나누었다. 이번에는 DNG Creator 사용하는 것과 Galaxy A 시리즈의 한계에 대해서 나누어 보고자 한다. Camera2 API 에서 이미지를 캡처하고, 그 결과물을 DngCreator 로 처리하는 실제 코드를 흐름 순으로 확인해보자. import android.

By Jeongsu Choi
그래도 걸어야 한다 (영화 다가오는 것들)

그래도 걸어야 한다 (영화 다가오는 것들)

3개월에 한번은 프랑스 영화를 본다. 서른살이 넘으면서 나에게 했던 작은 다짐이다. 잘 한 것 같다. 영화, 다가오는 것들(L'Avenir)을 최근들어 다시 봤다. 이 영화는 반드시 글로 나름대로 남겨야 한다는 생각이 들었다. 줄거리를 어느정도는 이야기해야겠지. 철학 교수인 나탈리는 한꺼번에 말도 안되는 일을 경험한다. 남편은 바람이 나서 따로 살겠다고

By Jeongsu Choi