Galaxy 수채화 현상 완전 정복! Raw Image 를 활용한 Android Camera Module 개발 [3/3]
앞선 글에서 Raw Image를 DNG Creator 를 활용함으로 최종적으로 JPEG 파일까지 만드는 과정에 대해 이야기를 나눴다.
추가적으로 우리 키트 배경지에는 ArUco 마커가 그려져 있다. AI 개발자와의 논의 끝에 도입한 내용이였으며, 회사에서도 배경지에 추가하기로 하였다. 굳이 사진 촬영을 하는 가운데 ArUco 마커를 도입한 이유와 과정에 대해 설명해보려고 한다.
// 파일명: ArucoDetector.kt
package com.example.mycameraapp.vision
import org.opencv.aruco.Aruco
import org.opencv.aruco.Dictionary
import org.opencv.core.Mat
/**
* 이미지(Mat)에서 ArUco 마커들을 감지하고, 그 결과를 반환하는 클래스
*/
class ArucoDetector {
// 1. 어떤 종류의 마커를 찾을지 '사전(Dictionary)'을 정의합니다.
// DICT_6X6_250은 6x6 그리드 패턴을 가진 250개의 고유 마커 세트를 의미합니다.
// 이 사전은 한 번만 생성하여 재사용하는 것이 효율적입니다.
private val dictionary: Dictionary = Aruco.getPredefinedDictionary(Aruco.DICT_6X6_250)
/**
* 입력받은 이미지에서 ArUco 마커들을 감지합니다.
*
* @param image Mat 형식의 입력 이미지. 흑백(grayscale) 이미지일 때 더 좋은 성능을 보입니다.
* @return 감지된 마커들의 정보가 담긴 데이터 클래스를 반환합니다.
*/
fun detect(image: Mat): DetectionResult {
// 2. 마커의 코너 좌표와 ID를 담을 비어있는 Mat 객체를 생성합니다.
// OpenCV 함수들은 결과를 이 객체들에 직접 채워 넣습니다.
val corners = mutableListOf<Mat>()
val ids = Mat()
// 3. 핵심: 마커 탐지 함수를 호출합니다.
// 이 함수가 내부적으로 모든 복잡한 컴퓨터 비전 연산을 수행합니다.
Aruco.detectMarkers(
image, // 분석할 원본 이미지
dictionary, // 찾을 마커의 종류가 담긴 사전
corners, // 찾은 마커들의 코너 좌표가 여기에 저장됨 (출력 파라미터)
ids // 찾은 마커들의 고유 ID가 여기에 저장됨 (출력 파라미터)
)
// 4. 감지 결과를 사용하기 쉬운 데이터 클래스로 포장하여 반환합니다.
return DetectionResult(corners, ids)
}
/**
* 마커 감지 결과를 담는 데이터 클래스
* @param corners 각 마커의 4개 꼭짓점 좌표. List<Mat> 형태이며, 각 Mat은 4x1 크기.
* @param ids 각 마커의 고유 ID. Mat 형태이며, int 타입으로 값을 읽을 수 있음.
*/
data class DetectionResult(
val corners: List<Mat>,
val ids: Mat
)
}실제로 ArUco 마커를 detect 하는 코드를 작성하기 위해서는 OpenCV SDK 를 설치하고, JNI(Java Native Interface) 를 이용하고 C++ 코드를 작성하여 ArUco 마커 detect 결과값을 받아와야 한다.
다행히 소프트뱅크에서 OpenCV SDK 를 한번 더 편리하게 감싸서 Kotlin 에서도 호출이 가능하게끔 하도록 만든 라이브러리가 있다. pepper-aruco 라이브러리이다. 이 라이브러리를 사용하면 결과도 다루기 쉽게 'Marker' 객체로 반환 받을 수 있다. 나아가, marker.id, marker.corners 등으로 바로 접근도 가능하다.
ArUco 마커를 사용함으로서 얻게 되는 이득은 다음과 같았다.
- 절대적인 '거리' 기준 확립
- Android 개발자로서의 가장 큰 고충은 기기마다의 파편화가 크다는 것이다. 스마트폰마다 최소 초점 거리도 다르고, 거리가 달라지면 이미지의 크기와 해상도가 바뀌면서 분석 결과의 일관성이 떨어진다.
- 예를 들어 "피사체와 3cm ± 0.5cm 거리에서만 촬영을 허용한다." 오아 같은 엄격한 규칙을 적용할 수 있다. 이는 모든 촬영이 항상 동일한 거리에서 이루어지도록 보장한다.
- 완벽한 '수평과 각도' 강제
- 카메라가 키트를 정면에서 바라보고 있는지(수직인지) 알 수 없는 문제가 있다. 사용자가 스마트폰을 수평으로 든 채, 키트를 비스듬히 내려다보며 찍을 수도 있다.
- ArUco 마커는 완벽한 정사각형이다. 만약 카메라가 키트를 비스듬하게 보면, 이미지 속의 마커는 사다리꼴이나 마름모꼴처럼 찌그러져 보이게 된다. 사용자가 스트립을 정면에서 수직으로 바라볼 때까지 촤영이 진행되지 않도록 강제할 수 있다.
ArUco Marker 는 통제된 환경을 만들어줌으로써, 일관성을 갖게 해줬다.
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-feature
android:name="com.softbank.hardware.pepper"
tools:node="remove" />한가지 중요한 정보 중 하나는 softbank 의 라이브러리를 사용하면 위와 같이 tools:node="remove" 를 반드시 적용시켜줘야 한다. 이렇게 하지 않으면 Google Play Store 에 앱이 aab 파일로 업로드가 되지 않는다. 로봇 관련 이슈가 나오면서 말이다.
회고
Android Camera Module 을 최적화 하기 위한 발버둥은 안개속을 걷는 느낌이였다. 'RAW Image 를 활용하는거 별거 아니네' 라고 생각할 수도 있다. 하지만 처음 RAW Image 를 활용해보자고 생각하는 것이 마냥 쉬운 일은 아닌 것 같다. 과정 가운데 기기별 테스트는 물론이고 비지니스팀과 Bio 개발자들에게 구두로 합의점을 찾아내는 과정도 쉬운일이 아니다. 기기별 테스트와 서칭(Diggin 같은 서칭) 의 과정이 더 중요했다고 봐도 될 것 같다.
현재 Cursor, Claude code 를 활용하면 왠만한 코드는 다 짜준다. 하지만 앞서 말한 이런 가이드는 내려주기 어려울 것이다. (실제로 찾아주지 못했다.) 기기별로 어떻게 나오는지도 AI Agent 들에겐 정보가 거의 없다.
한편으론 2개월의 이 과정을 겪으며 뿌듯했다. Presentation Layer 만 만들어내는 듯한 느낌이 들어서 무미건조한 느낌이였는데 단비와도 같았다. (기억 왜곡인가?)
Camera2 API 는 여간 복잡한게 아니다. 해당 API 에 대한 숙지가 우선인 것 같다. Google Sample 이 정말 보석이다. 다만 현재 Jetpack Compose 로 UI 를 만든 부분은 없다. Jetpack Compose 와 Camera2 API 를 엮어서 개발해보는 것도 꽤 재밌을 것 같다.