Galaxy 수채화 현상 완전 정복! Raw Image 를 활용한 Android Camera Module 개발 [2/3]
앞의 글에서 Camera2 API 를 사용하는 이유, RAW 가능 여부 확인, SurfaceView 커스터마이징, DNG 란 무엇인가, 에 대해 이야기 나누었다. 이번에는 DNG Creator 사용하는 것과 Galaxy A 시리즈의 한계에 대해서 나누어 보고자 한다.
Camera2 API 에서 이미지를 캡처하고, 그 결과물을 DngCreator 로 처리하는 실제 코드를 흐름 순으로 확인해보자.
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.DngCreator
import android.hardware.camera2.TotalCaptureResult
import android.media.Image
import android.os.Environment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 캡쳐된 RAW 데이터를 DNG 파일 형식으로 저장합니다.
* 이 함수는 파일 I/O 작업을 포함하므로, 별도의 IO 스레드에서 호출하는 것이 좋습니다.
*
* @param image 캡쳐된 RAW 데이터가 담긴 Image 객체 (메인 재료)
* @param characteristics 카메라 하드웨어의 고유한 특성 정보 (카메라 신분증)
* @param captureResult 사진 촬영 시점의 모든 설정값이 담긴 메타데이터 (촬영 레시피)
* @param context 파일 저장을 위한 애플리케이션 Context
* @return 성공적으로 저장된 DNG 파일 객체. 실패 시 null을 반환합니다.
*/
suspend fun saveRawImageAsDng(
image: Image,
characteristics: CameraCharacteristics,
totalCaptureResult: TotalCaptureResult,
context: Context
): File? = withContext(Dispatchers.IO) { // 파일 I/O는 IO 스레드에서!
try {
// 1. DngCreator를 생성합니다. '카메라 신분증'과 '촬영 레시피'를 전달합니다.
val dngCreator = DngCreator(characteristics, totalCaptureResult)
// 2. 고유한 파일명을 생성합니다. 타임스탬프를 사용하는 것이 일반적입니다.
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val dngFile = File(
context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), // 앱 외부 저장소의 사진 폴더
"RAW_IMG_${timestamp}.dng"
)
// 3. FileOutputStream을 통해 '메인 재료(Image)'를 DNG 파일로 씁니다.
// use 블록을 사용하면 스트림이 자동으로 닫혀 안전합니다.
FileOutputStream(dngFile).use { outputStream ->
dngCreator.writeImage(outputStream, image)
}
// 4. 모든 과정이 성공하면, 완성된 파일 객체를 반환합니다.
return@withContext dngFile
} catch (e: IOException) {
// 파일 쓰기 중 오류가 발생한 경우
e.printStackTrace()
return@withContext null
} finally {
image.close()
}
}실제 카메라 앱에서는 ImageReader.OnImageAvailableListner 안에서 이 함수를 호출하게 된다. 리스너에서 Image 객체를 얻고, 미리 저장해 둔 TotalCaptureResult (자동 초점값, 자동 노출, 수동 초점 거리, 줌 비율을 담고 있는 객체) CameraCharacteristics 를 함께 이 함수에 전달하는 방식이다. DngCreator 가 완벽한 DNG 파일을 만들기 위해서는 3가지 핵심 재료가 필요하다.
- Image 객체: 카메라 센서가 포착한 순수한 RAW 픽셀 데이터입니다. (메인 재료)
- TotalCaptureResult 객체: 사진이 촬영된 순간의 모든 설정값(ISO, 노출, 화이트밸런스 등)이 담긴 메타데이터입니다. (촬영 레시피)
- CameraCharacteristics 객체: 사용된 카메라 하드웨어의 고유한 물리적 특성 정보입니다. (카메라의 신분증)
DngCreator 를 사용하여 RAW 데이터를 DNG 파일로 저장을 한다. DNG 파일을 BitmapFactory 객체를 통해 Bitmap 으로 디코딩 한다. Bitmap 을 JPEG 형식으로 압축하여 파일로 저장을 한다. DNG 파일은 대략 25MB 를 넘었다. 그래서 최종적으로 JPEG 를 만들기까지 시간이 5초 이상은 걸렸다. 그래도 정확도를 위해 이렇게 해야만 했다. (더 좋은 방법이 있으면 알려주면 반영하겠다.)
// DNG 파일을 Bitmap으로 변환
val bitmap: Bitmap = BitmapFactory.decodeFile(dngFile.absolutePath)
// Bitmap을 JPEG로 저장
val jpegFile = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "IMG.jpg")
FileOutputStream(jpegFile).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}심지어 촬영된 이미지는 3840 * 2160 (4k) 의 이미지였다. Business 팀에서 이 화질의 이미지를 AI 분석에 활용하기로 최종적으로 합의 하였다.
개발 과정 가운데 Galaxy A 시리즈 폰들은 3840 * 2160(4k) 이미지를 가져오지 못한다. 따라서 1920 * 1080 의 이미지로 대체하였다. 나아가 앞의 글에서 말했듯이 RAW Image 를 가져오지 못한다. (App 이 Crash 됨). 따라서 앞단에서 RAW Image 를 가져오지 못하고 getOutputSizes 에서 3840 * 2160(4k) 이 목록에 존재하지 않을 때는 CameraX API 를 사용해 프로세스를 진행하였다.
한가지 조심해야할 것은, 실제 제조사 카메라 앱에서 4K 출력을 지원할 수도 있다. 하지만 어떠한 이유인지는 몰라도 CameraX API 에서 4k 촬영이 안될 것이다. 제조사의 정책과 하드웨어 설계에 따라 달라질 수 있는 부분이니 잘 염두해 둬야 한다.