cameraX로 미리보기를 구현하다 보면, previewView의 형태를 정의해야한다. 그 중 FILL_CENTER는 무엇을 의미할까?
정답은 매우 간단하다. 전체 화면으로 보는 것이다. 그럼 preview에서 받아온 기존의 비율은 어떻게 되는 것일까?
이 의문이 필요한 이유는 YOLO의 객체 검출된 결과에 대해 바운딩 박스를 그려야 하기 때문이다.
//카메라 제공 객체
val processCameraProvider = ProcessCameraProvider.getInstance(this).get()
//전체 화면
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
// 전면 카메라
val cameraSelector =
CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
// 16:9 화면으로 받아옴
val preview = Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_16_9).build()
// preview 에서 받아와서 previewView 에 보여준다.
preview.setSurfaceProvider(previewView.surfaceProvider)
//분석 중이면 그 다음 화면이 대기중인 것이 아니라 계속 받아오는 화면으로 새로고침 함. 분석이 끝나면 그 최신 사진을 다시 분석
val analysis = ImageAnalysis.Builder().setTargetAspectRatio(AspectRatio.RATIO_16_9)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build()
analysis.setAnalyzer(Executors.newSingleThreadExecutor()) {
imageProcess(it)
it.close()
}
// 카메라의 수명 주기를 메인 액티비티에 귀속
processCameraProvider.bindToLifecycle(this, cameraSelector, preview, analysis)
위 코드에서 preview, 즉 카메라에서 받아온 이미지를 16:9 의 이미지로 받아온다는 것을 확인할 수 있다. 그리고 imageAnalysis (분석할 이미지)의 비율도 16:9 임을 확인 할 수 있다.
YOLO의 학습데이터의 [가로 세로] 가 [640 640] 이라고 가정한다면, 안드로이드에서 추론할 때에도 [640 640]의 이미지로 변환해야 한다.

ImageAnalysis에서 사진을 받아오면 1280:720의 사진을 가져온다.
그럼 그 화면을 다시 640:640으로 압축하고, 추론을 하게 되면, 바운딩 박스의 좌표들은 640:640의 좌표값에 대한 바운딩 박스 좌표값을 가지게 된다. 우리는 이를 화면에 보여주는 실제 크기는 3100:1440의 비율에 맞게 변환 시켜주면 된다. 그러면 바운딩 박스의 좌표값을 3100/640 만큼 x 좌표값을 곱해주고, 1440/640 만큼 y좌표를 곱해주면 우리가 생각하는 바운딩 박스가 생길까? 정답은 "아니요" 이다.
아래 사진을 보자

예시로 든 모델은 yolov8n로 만든 화재 감지 모델이며, 사진은 구글에서 화재를 검색하고, 이미지 부분을 본 화면이다. 바운딩 박스를 보면 박스가 가운데에 모여있다는 것을 확인할 수 있다. 화면에서 위에 부분의 바운딩 박스는 아래에 내려와 있고, 아래 부분의 바운딩 박스는 위로 올라가 있다.
그 이유는 비율에서 생기는 사진의 변화 때문이다.
출력 화면인 3100:1440의 비율은 16:9일까? 계산해보면 비율은 약 2.15:1 이다.
그렇다면 16:9의 화면으로 받아왔는데 2.15:1로 압축시킨 것이라 생각할 수 있다. 만약, 그렇다면 위에서 생각했던 "그러면 바운딩 박스의 좌표값을 3100/640 만큼 x 좌표값을 곱해주고, 1440/640 만큼 y좌표를 곱해주면 우리가 생각하는 바운딩 박스가 생길까?" 가 성립해야 한다.
위에 썻던 코드 중 PreviewView는 아래로 정의했다.
//전체 화면
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
그리고 안드로이드 cameraX의 공식 사이트에는 아래와 같은 사진이 있다.
https://developer.android.com/training/camerax/preview?hl=ko

우리가 코드로 짰던, FILL_CENTER는 이미지(16:9 비율)를 실제 화면에 보여줄 때, 비율에 맞게 압축시키는 것이 아니라, 이미지를 잘라서 (2.15:1)로 만들고 화면에 보여주는 것이다.
즉, 우리는 바운딩 박스의 좌표값에 대해 잘리는 화면을 고려해서 코드를 짜야한다는 것을 의미한다.
위 화면을 보면 알겠지만, FILL_CENTER는 긴 축을 기준으로 화면을 자른다. 나무 사진의 경우 가로가 1920으로 더 긴 모습이다. 따라서 가로를 다 보여주는 만큼 세로 부분을 잘라서 화면에 보여준다.
val scaleX = width / DataProcess.INPUT_SIZE.toFloat()
val scaleY = height/ DataProcess.INPUT_SIZE.toFloat()
results.forEach {
it.rectF.left *= scaleX
it.rectF.right *= scaleX
it.rectF.top *= scaleY
it.rectF.bottom *= scaleY
}
이런 식으로 실제 화면의 비율에 그냥 변환을 하는 것은 FILL_CENTER가 화면을 자르지 않고, 압축하는 경우 성립하는 식이다.
val scaleX = width / DataProcess.INPUT_SIZE.toFloat()
val scaleY = scaleX * 9f / 16f
val realY = width * 9f / 16f
val diffY = realY - height
따라서 우리는 변하지 않는 가로 비율은 원래대로 구하고, 이론상 잘리지 않았다는 가정하의 16:9 화면의 높이 비율인 scaleY, 실제 높이 값인 realY를 구한다. 이후에 잘린 크기는 height 와의 차이 값인 diffY를 정의한다.
for (Result result : resultArrayList) {
result.getRectF().left *= scaleX;
result.getRectF().right *= scaleX;
result.getRectF().top = result.getRectF().top * scaleY - (diffY / 2f);
result.getRectF().bottom = result.getRectF().bottom * scaleY - (diffY / 2f);
}
위의 코드 처럼 너비값은 변하지 않으므로 scaleX를 그대로 곱해준다.
높이는 잘린 부분을 고려하기 위해서, 좌표 값에 scaleY를 곱해주고, 잘린 화면 만큼의 차이를 위해 좌표 이동을 시켜준다. 화면이 잘리는 부분은 가운데를 기준으로 위, 아래가 잘리니까 (diff/2f) 로 잘린 화면의 반 만큼 이동을 시켜주면 된다.

위 사진은 동일한 코드에서 좌표값 변환을 수정한 사진이다.
예상한 것과 같이 바운딩 박스의 오차가 사라진 것을 확인할 수 있다.
좋은 글 다시 한번 감사드립니다.!