Android JetPack Compose와 Google ML Kit을 이용한 얼굴 인식 이미지 센터링 뷰 개발하기

Hongsupport
PIXO
16 min readJun 9, 2023

--

안녕하세요 😃 픽소에서 Android 개발을 담당하고 있는 홍지원입니다.

개발을 하면서 다양한 문제들을 마주치게 되죠.

문제에 대한 솔루션 중 “와 정말 대단하다”라는 감탄사를 자아내었던 기술 중 하나를 소개하고, 문제 해결 과정을 블로그에 공유하고자 합니다.

Face Focusing

제가 담당하고 있는“베이비스토리” 제품은 사용자의 사진을 앱 내에서 다양한 형태로 보여주고 있습니다.

단순히 보여주는 것이 아닌 조금 더 예쁘게 보여주기 위해 많은 고민을 했고, 그 고민 중 하나는 “어떻게 하면 사진 속 얼굴이 화면 가장자리에 잘려서 보이지 않을까?” 였습니다.

그렇기 때문에 이미지에서 얼굴을 감지하고 중앙에 배치할 수 있는 솔루션이 필요했습니다.

Google ML Kit 적용

솔루션으로 선택한 기술은 Google ML Kit으로, 모바일 개발자에게 애플리케이션에 쉽게 통합할 수 있는 사전 학습된 머신 러닝 모델 호스트를 제공하는 Google이 배포한 API입니다.

ML Kit의 Face Detection API는 머신러닝 기반의 얼굴 감지를 제공하며, 단순히 얼굴의 위치 뿐만 아니라 얼굴의 세부적인 요소를 식별할 수 있는 랜드마크(예: 눈, 코, 입) 인식, 미소 감지, 심지어 사람의 눈을 감았는지 여부 등의 다양한 기능을 제공합니다. 이 기술을 이용해 매우 얼굴을 이미지의 중심에 맞추는 기능을 구현할 수 있었습니다.

얼굴 인식 이미지 센터링 뷰 구현

사용자가 선택한 이미지를 렌더링할 때 사람의 얼굴을 자동으로 가운데 정렬하기 위해 Google ML Kit를 애플리케이션에 통합했습니다.

요구 사항에 맞게 설계한 기능 프로세스는 아래와 같이 나뉩니다.

  1. 이미지 선택: 사용자는 갤러리에서 원하는 이미지를 선택하거나 장치의 카메라에서 직접 선택합니다.
  2. 얼굴 인식: 이미지가 선택되면 ML Kit의 얼굴 인식 API를 통해 처리됩니다. 이 AI 도구는 이미지를 스캔하고 얼굴을 식별하며 이미지 내에서 얼굴의 위치를 계산합니다.
  3. 얼굴 중심 맞추기: 다음 단계에서는 ML Kit에서 식별한 얼굴 좌표를 사용하여 이미지를 얼굴 중심에 맞춥니다. 얼굴이 중앙에 이상적으로 위치하도록 이미지가 재배치됩니다.
  4. 이미지 렌더링: 얼굴 중심에 완벽하게 맞춰진 최종 이미지가 렌더링되어 사용자에게 표시됩니다. 이 결과 이미지는 필요에 따라 애플리케이션 내에서 저장, 공유 또는 사용할 수 있습니다.

위 과정으로 이미지와 상호 작용할 때 사용자에게 보다 부드럽고 사용자 친화적인 경험을 제공하는 얼굴 인식 이미지 센터링 기능을 만들었습니다.

다음은 실제 작업한 코드입니다.

우선 빌드 구성 파일(Gradle)에 ML Kit Dependency를 추가합니다.

Build.gradle.kts

dependencies {
// android ML Kit - face.
implementation ("com.google.mlkit:face-detection:16.1.5")
}

얼굴 인식기에 대한 기본 설정을 FaceDetectorOption 객체를 사용하여 지정합니다.

FaceDetectorOptions에 대한 자세한 정보는 Google Document에 기재되어있습니다.

얼굴 인식 센터링을 구현하기 위한 요소로는 얼굴의 위치만 필요하므로 그외의 불필요한 요소인 랜드마크, 윤곽, 표정 등의 설정은 모두 비활성화 해줍니다.

AutoFaceFocusImage.kt

val options = FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
.setMinFaceSize(1f)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE)
.build()

FaceDetection 클라이언트를 생성하고 전용 Input Image를 생성해줍니다.

프로젝트 내에서는 삽입 요소로 android.graphics.Bitmap을 사용했습니다.

val detector = FaceDetection.getClient(options)
val inputImage = InputImage.fromBitmap(bitmap, 0)

이제 생성된 detector를 실행 시켜서 실제로 사용자가 선택한 이미지내에 사람의 얼굴이 인식이 되는지 사전에 디버깅을 해봅시다.

detector.process(inputImage)
.addOnSuccessListener{ faces ->
Log.d(TAG, "faces: $faces")
}.addOnFailureListener {
Log.d(TAG, "error: ${it.message}")
}

인식된 얼굴들이 존재한다면 모든 얼굴들의 위치사이 중심값을 getCenterBetweenCentersOfRects 메서드를 통해 구합니다.

detector.process(inputImage)
.addOnSuccessListener{ faces ->
Log.d(TAG, "faces: $faces")

if (faces.isNotEmpty()) {
val rects: List<Rect> = faces.map { it.boundingBox }
val centerOffset: PointF = getCenterBetweenCentersOfRects(rects = rects)
}
}.addOnFailureListener {
Log.d(TAG, "error: ${it.message}")
}

getCenterBetweenCentersOfRects는 인식된 얼굴 개수만큼 생성되는 사각형 목록(Rects)의 중심점 사이의 중심점을 찾도록 설계되었습니다. 다음과 같이 동작합니다.

설명 다이어그램
  1. 이 함수는 minX, minY, maxX, maxY의 네 가지 변수를 초기화하여 시작합니다. minXminYFloat.MAX_VALUE로 초기화되는 반면, maxXmaxYFloat.MIN_VALUE으로 초기화됩니다. 이 변수는 각각 사각형 중심의 가장 작은 X와 Y 좌표 및 가장 큰 X 및 Y 좌표를 저장합니다.
  2. 각 사각형에 대해 사각형 개체에서 exactCenterX()exactCenterY() 메서드를 호출하여 중심의 X 및 Y 좌표를 계산합니다.
  3. 그런 다음 이러한 X 및 Y 좌표를 현재 minX, minY, maxXmaxY와 비교합니다. 중심의 X 좌표가 minX보다 작으면 minX를 대체합니다. maxX보다 크면 maxX를 대체합니다. minYmaxY를 사용하여 Y 좌표에 대해 조건에 따라 처리합니다.
  4. 모든 직사각형이 처리되면 함수는 최소 및 최대 X 좌표((minX + maxX) / 2)와 최소 및 최대 Y 좌표((minY + maxY) / 2)의 평균값을 계산합니다.
  5. 이 평균 좌표는 PointF 로 반환됩니다. 이 점은 입력 사각형의 중심 사이의 중심점을 나타냅니다. 주어진 직사각형의 모든 중심을 포함할 수 있는 가장 작은 직사각형의 중심입니다.
private fun getCenterBetweenCentersOfRects(
rects: List<Rect>
): PointF {
var minX = Float.MAX_VALUE
var minY = Float.MAX_VALUE
var maxX = Float.MIN_VALUE
var maxY = Float.MIN_VALUE

for (rect in rects) {
val centerX = rect.exactCenterX()
val centerY = rect.exactCenterY()

minX = min(minX, centerX)
minY = min(minY, centerY)
maxX = max(maxX, centerX)
maxY = max(maxY, centerY)
}
return PointF((minX + maxX) / 2, (minY + maxY) / 2)
}

getCenterBetweenCentersOfRects 메서드에서 반환한 PointF의 값을 이용하여 원하는 사각형 모형으로 원본 이미지를 잘라보겠습니다.

  • 해당 포스팅에서는 정사각형으로 자르는 메서드를 구현 했습니다.
설명 다이어그램

정사각형의 크기 계산 및 시작점 설정

함수는 원본 이미지의 너비와 높이 중 작은 값을 선택하여 정사각형의 크기(size)를 결정합니다. 이를 통해 원본 이미지의 어느 한 쪽이 잘리는 것을 방지합니다. 이어서, 이 정사각형의 왼쪽 상단 점(left, top)을 계산합니다. 이 점은 중심점으로부터 size / 2만큼 떨어져 있어야 하지만, 이렇게 하면 이미지의 경계 밖으로 나갈 수도 있기 때문에, coerceIn (값이 범위 안에 있으면 해당 값을, 값이 범위 안에 없으면 경계값을 반환) 함수를 사용하여 이 점이 이미지 내부에 위치하도록 합니다.

Bitmap 생성

Bitmap.createBitmap 함수를 호출하여 새로운 정사각형 Bitmap을 생성합니다. 이 함수는 원본 비트맵, 시작점의 x 좌표(left), 시작점의 y 좌표(top), 그리고 너비와 높이를 인자로 받습니다.

/**
* 정사각형으로 Crop.
*/
fun cropBitmapToSquare(original: Bitmap, center: PointF): Bitmap {
val size = min(original.width.toFloat(), original.height.toFloat())
val left = (center.x - size / 2).coerceIn(0f, original.width - size).toInt()
val top = (center.y - size / 2).coerceIn(0f, original.height - size).toInt()

return Bitmap.createBitmap(original, left, top, size.roundToInt(), size.roundToInt())
}

전체 코드

얼굴 인식 이미지 센터링 뷰를 만들기 위한 모든 준비가 끝났습니다. 전체 코드를 살펴보겠습니다.

@Composable
fun AutoFaceFocusImage(
modifier: Modifier,
uri: Uri
) {
val context: Context = LocalContext.current

var imageOffset: Offset by remember {
mutableStateOf(Offset(0f, 0f))
}
var faceCenterPoint: PointF by remember {
mutableStateOf(PointF())
}

var processingImage: Bitmap? by remember {
mutableStateOf(null)
}

var isSuccessProcessed: Boolean by remember {
mutableStateOf(false)
}

LaunchedEffect(Unit) {
val bitmap: Bitmap = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri))
} else {
MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
}
} catch (e: Exception) {
null
} ?: return@LaunchedEffect

val options = FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
.setMinFaceSize(1f)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE)
.build()

val detector = FaceDetection.getClient(options)
val inputImage = InputImage.fromBitmap(bitmap, 0)

detector.process(inputImage)
.addOnSuccessListener { faces ->
Log.d(TAG, "faces: $faces")

if (faces.isNotEmpty()) {
val rects: List<Rect> = faces.map { it.boundingBox }
val centerOffset: PointF = getCenterBetweenCentersOfRects(rects = rects)

faceCenterPoint = centerOffset
Log.d(TAG, "faces. centerPoint: $faceCenterPoint")

processingImage = cropBitmapToSquare(
bitmap,
faceCenterPoint
)
isSuccessProcessed = true
}
}
.addOnFailureListener {
Log.d(TAG, "error: ${it.message}")
isSuccessProcessed = false
}
}

Image(
modifier = modifier.onGloballyPositioned {
imageOffset = it.positionInParent()
},
painter = rememberAsyncImagePainter(if (isSuccessProcessed) processingImage else uri),
contentDescription = null,
contentScale = if (isSuccessProcessed) ContentScale.Fit else ContentScale.Crop
)
}

/**
* Rects의 중심점을 반환.
*/
private fun getCenterBetweenCentersOfRects(
rects: List<Rect>
): PointF {
var minX = Float.MAX_VALUE
var minY = Float.MAX_VALUE
var maxX = Float.MIN_VALUE
var maxY = Float.MIN_VALUE

for (rect in rects) {
val centerX = rect.exactCenterX()
val centerY = rect.exactCenterY()

minX = min(minX, centerX)
minY = min(minY, centerY)
maxX = max(maxX, centerX)
maxY = max(maxY, centerY)
}
return PointF((minX + maxX) / 2, (minY + maxY) / 2)
}

/**
* 정사각형으로 Crop.
*/
private fun cropBitmapToSquare(original: Bitmap, center: PointF): Bitmap {
val size = min(original.width.toFloat(), original.height.toFloat())
val left = (center.x - size / 2).coerceIn(0f, original.width - size).toInt()
val top = (center.y - size / 2).coerceIn(0f, original.height - size).toInt()

return Bitmap.createBitmap(original, left, top, size.roundToInt(), size.roundToInt())
}

이 코드는 Android Compose를 사용하여 이미지의 얼굴을 자동으로 포커스하는 기능을 가지는 AutoFaceFocusImage Composable 함수를 정의합니다.

코드는 크게 두 부분으로 나눌 수 있습니다.

얼굴 인식과 이미지 처리

이 부분에서는 Android의 ML Kit를 사용하여 얼굴을 인식하고, 그 얼굴을 중심으로 이미지를 재조정합니다.

이미지 표시

이 부분에서는 얼굴이 중심에 오도록 조정된 이미지를 표시합니다.

자세히 설명하자면, 먼저 LaunchedEffect 또는 SideEffect 를 통해 이미지를 Bitmap으로 불러오고, 그 이미지를 처리하여 얼굴을 인식하고 중심점을 찾습니다. 또한, 이미지를 사각형 또는 직사각형으로 잘라내는 작업을 수행합니다. ML Kit의 FaceDetection를 사용하여 Bitmap 이미지 내에서 얼굴을 인식하며, 그 결과를 성공적으로 얻으면 얼굴들의 중심점을 계산하고, 그 중심점을 기준으로 이미지를 재조정합니다.

그 후 Image composable을 이용하여 이미지를 표시하게 됩니다. 이미지 처리가 성공적으로 이루어지면 처리된 이미지를 표시하고, 그렇지 않으면 원래 이미지를 그대로 표시합니다. 이미지는 contentScale 파라미터를 통해 어떻게 화면에 맞출지를 설정합니다.

처리된 이미지는 ContentScale.Fit을 통해 전체 이미지가 보이도록 화면에 맞춰집니다. 그렇지 않은 경우 ContentScale.Crop을 통해 이미지가 화면을 꽉 채우도록 설정됩니다.

결과물

테스트 이미지

테스트 용도로 사용한 이미지 입니다.

구현한 뷰(AutoFaceFocusImage)

얼굴 인식 전 / 후

마치며

픽소 개발 팀은 문제를 해결하고 사용자 경험을 향상시키는 기술의 사용을 적극적으로 수용하고 있습니다.

이번 얼굴 인식 이미지 센터링 뷰도 Google ML Kit 기술을 결합하여 사용자에게 더 나은 경험을 제공할 수 있었습니다.

픽소 팀 블로그의 흥미로운 업데이트를 계속 지켜봐주세요. 감사합니다.

--

--