Android 실시간 얼굴 인식 예제

https://github.com/cornpip/android_face_detection_example
GitHub - cornpip/android_face_detection_example
Contribute to cornpip/android_face_detection_example development by creating an account on GitHub.
github.com
- cameraX - 디바이스 카메라를 컨트롤하여 이미지를 취득하고 처리한다.
- ML Kit - input image에서 얼굴을 인식하고 bounding box를 반환한다.
- preview 화면에 bounding box를 그린다.
CameraX
Camera camera = cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
imageAnalysis
);
CameraX는 목적에 따라 UseCase를 구성하고 바인딩하여 사용한다. (UseCase 간의 순서는 보장되지 않는다.)
preview와 imageAnalysis는 독립적인 UseCase로 각각 구성된다.
private void switchCamera() {
// 현재 카메라가 후면이면 전면으로, 전면이면 후면으로 전환
if (cameraSelector.equals(CameraSelector.DEFAULT_BACK_CAMERA)) {
cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA;
} else {
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
}
// 카메라 재시작
startCamera();
}
CameraSelector는 전면을 사용할지 후면을 사용할지 결정한다.
Preview preview = new Preview.Builder()
.build();
preview.setSurfaceProvider(binding.previewView.getSurfaceProvider());
preview는 xml파일의 PreviewView Layout과 연결된다.
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
cameraExecutor = Executors.newSingleThreadExecutor();
imageAnalysis.setAnalyzer(cameraExecutor, imageProxy -> {
Image mediaImage = imageProxy.getImage();
if (mediaImage != null) {
....
} else {
imageProxy.close();
}
});
ImageAnalysis.Builder의 setBackpressureStrategy()는 프레임 수신 시 처리 전략을 설정하는 메서드다.
KEEP_ONLY_LATEST : 항상 최신 프레임 하나만 유지하고 나머지는 버린다. (일부 프레임 누락)
BLOCK_PRODUCER : 분석이 끝날 때까지 새 프레임 수신을 막는다. (모든 프레임 처리)
실시간 처리는 KEEP_ONLY_LATEST가 적합하다.
ImageAnalysis나 ImageCapture는 명시적으로 Executor를 지정하여 별도의 쓰레드에서 실행한다.
ML Kit
// FaceDetector 초기화
FaceDetectorOptions options = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST) // 빠른 성능을 위해 설정
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE) // 랜드마크 검출 안함
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE) // 얼굴 감지만
.build();
faceDetector = FaceDetection.getClient(options);
ML Kit은 다양한 머신러닝 기능을 제공하는데, 이 중 Face Detection API를 사용하면 얼굴 인식 기능을 구현할 수 있다.
bounding box뿐 아니라 눈,귀,코,뺨 같은 얼굴의 랜드마크도 검출할 수 있고,
'웃고 있음', '눈을 뜸'과 같은 카테고리로 분류한 결과를 얻을 수도 있다.
Bounding Box Draw
imageAnalysis.setAnalyzer(cameraExecutor, imageProxy -> {
Image mediaImage = imageProxy.getImage();
if (mediaImage != null) {
InputImage image = InputImage.fromMediaImage(
mediaImage, imageProxy.getImageInfo().getRotationDegrees());
faceDetector.process(image)
.addOnSuccessListener(faces -> {
List<RectF> faceRects = new ArrayList<>();
for (Face face : faces) {
Rect boundingBox = face.getBoundingBox();
// 좌표 변환
RectF transformedRect = mapCoordinate(
boundingBox,
new Size(mediaImage.getWidth(), mediaImage.getHeight()), // 원본 이미지 크기
new Size(binding.previewView.getWidth(), binding.previewView.getHeight()) // 프리뷰 화면 크기
);
Log.d(TAG, String.format("%s => %s", boundingBox.toString(), transformedRect.toString()));
faceRects.add(transformedRect);
}
runOnUiThread(() -> {
binding.faceOverlay.setFaceBoundingBoxes(faceRects);
});
})
.addOnFailureListener(Throwable::printStackTrace)
.addOnCompleteListener(task -> imageProxy.close());
} else {
imageProxy.close();
}
});
faceDetector로부터 얼굴 인식 결과를 받으면, 각 얼굴에 대한 bounding box가 Rect 객체로 반환된다.
이 Rect는 카메라 원본 이미지 해상도 기준의 좌표를 나타낸다.
예를 들어,
카메라 센서에서 얻은 원본 해상도는 640x480 이고,
촬영 중인 이미지를 보여주던 preview 해상도는 1440x1440 이라면,
faceDetector가 반환하는 Rect는 640×480 기준 좌표이다.
ML Kit의 faceDetector의 Rect는 640x480 에 대한 위치이다.
따라서, preview에 bounding box를 표시하려면 해상도 비율 및 오프셋을 계산하여, 좌표를 변환해 주는 과정이 필요하다.
public RectF mapCoordinate(Rect imageRect, Size imageSize, Size previewSize) {
...
}
다음 함수는 원본 이미지에 대한 bounding box를 PreviewLayout에 맞게 조정한다.
mapCoordinate(좌표 변환)
CameraX는 다음과 같은 ScaleType을 제공한다.
FIT_CENTER/START/END
FILL_CENTER/START/END


ScaleType의 동작 방식은 공식 문서에 있는 이미지 예시를 보면 한눈에 이해할 수 있다.
기본값은 FILL_CENTER이며, 예제에서는 XML 속성에 명시적으로 설정해 사용했다.
예제에서는 아래 두 가지 설정에 대해 살펴보자.
- FILL_START
- FILL_CENTER
FILL 계열의 ScaleType은 이미지를 View의 크기에 맞게 확대 또는 축소하여 채우되, 넘치는 부분은 잘라낸다.
FILL_START: 이미지의 시작점을 기준으로 맞추며, 일반적으로 아래쪽 또는 오른쪽이 잘린다.
FILL_CENTER: 이미지의 중앙을 기준으로 맞추며, 상하 또는 좌우 양쪽이 잘릴 수 있다.
float viewAspectRatio = (float) previewSize.getWidth() / previewSize.getHeight();
float imageAspectRatio = (float) imageSize.getWidth() / imageSize.getHeight();
float scaleFactor;
float postScaleWidthOffset = 0;
float postScaleHeightOffset = 0;
if (viewAspectRatio > imageAspectRatio) {
scaleFactor = (float) previewSize.getWidth() / imageSize.getWidth();
postScaleHeightOffset = ((float) previewSize.getWidth() / imageAspectRatio - previewSize.getHeight()) / 2;
} else {
scaleFactor = (float) previewSize.getHeight() / imageSize.getHeight();
postScaleWidthOffset = ((float) previewSize.getHeight() * imageAspectRatio - previewSize.getWidth()) / 2;
}
viewAspectRatio가 더 크다면, Preview의 가로 비율이 원본 이미지의 가로 비율보다 크다는 의미이다.
반대로, imageAspectRatio가 더 크다면, 원본 이미지의 가로 비율이 Preview의 가로 비율보다 크다는 뜻이다.
예를 들어, 다음과 같은 설정을 고려해 보자.
프리뷰: 1440x1440
이미지: 640x480
viewAspectRatio: 1
imageAspectRatio: 1.33333
이 경우, scaleFactor는 1440/480 = 3으로 계산된다.
즉, 이미지의 가로 비율이 프리뷰보다 크기 때문에, 이미지의 세로(짧은 쪽)를 프리뷰의 세로에 맞춰 확대하게 된다.
만약 큰 쪽을 맞추면 FIT 방식이 되어 빈 공간이 생기고, 짧은 쪽을 맞추면 FILL 방식이 되어 이미지가 잘린다.
FILL_START
Matrix matrix = new Matrix();
matrix.setScale(scaleFactor, scaleFactor);
if (cameraSelector.equals(CameraSelector.DEFAULT_FRONT_CAMERA)) {
matrix.postScale(-1, 1, previewSize.getWidth() / 2f, previewSize.getHeight() / 2f);
}
오프셋 없이 scaleFactor를 곱한 후, 전면 카메라라면 좌우 반전을 적용한다.
FILL_CENTER (논리적으로 생각한 계산)

자르기 전에는 (10, ~)에 위치하던 박스가
자른 후에는 (5, ~)에 위치한 것처럼 되었다.
FILL_CENTER에서는 넘친 크기의 절반만큼 offset이 적용된 것과 동일하다.
offset을 구하는 수식 유도는 다음과 같다.
// viewAspectRatio <= imageAspectRatio
scaleFactor = viewHeight / imageHeight
scaledImageWidth = imageWidth * scaleFactor
= imageWidth * (viewHeight / imageHeight)
= viewHeight * (imageWidth / imageHeight)
= viewHeight * imageAspectRatio
cropWidth = scaledImageWidth - viewWidth
= (viewHeight * imageAspectRatio) - viewWidth
postScaleWidthOffset = cropWidth / 2
= ((viewHeight * imageAspectRatio) - viewWidth) / 2
프리뷰: 1440x1440
이미지: 640x480
viewAspectRatio: 1
imageAspectRatio: 1.33333
scaleFactor = 1440/480 = 3
postScaleWidthOffset = (1440 * 1.333 - 1440) / 2 = 240
Matrix matrix = new Matrix();
matrix.setScale(scaleFactor, scaleFactor);
matrix.postTranslate(-postScaleWidthOffset, -postScaleHeightOffset); // off-set
if (cameraSelector.equals(CameraSelector.DEFAULT_FRONT_CAMERA)) {
matrix.postScale(-1, 1, previewSize.getWidth() / 2f, previewSize.getHeight() / 2f);
}
FILL_CENTER 에서는 오프셋을 적용하기 전에도 프리뷰와 이미지의 중앙이 일치하지만, 다른 ScaleType을 사용하면 일치하지 않을 수 있다.
그런 경우에는 오프셋 적용과 좌우 반전의 순서가 결과에 영향을 미친다.
FILL_CENTER (박스가 잘 그려지는 계산)
위 챕터의 계산이 틀린 것 같지 않지만, 얼굴 위치와 맞지 않게 박스를 그린다.
Matrix matrix = new Matrix();
matrix.setScale(scaleFactor, scaleFactor);
matrix.postTranslate(-postScaleHeightOffset, -postScaleWidthOffset);
if (cameraSelector.equals(CameraSelector.DEFAULT_FRONT_CAMERA)) {
matrix.postScale(-1, 1, previewSize.getWidth() / 2f, previewSize.getHeight() / 2f);
}
이것저것 시도해 보니, x축에 heightOffset, y축에 widthOffset 값을 주면 정확하게 그려진다.
이유는 모르겠다.
그리고 모든 예제(FILL_START/CENTER)는 프리뷰 사이즈가 1:1 비율일 때만 올바른 위치에 박스를 그린다.
이 부분도, 왜 1:1 비율에서만 위치가 맞는지 모르겠다.
참고 링크
https://developer.android.com/media/camera/camerax/preview?hl=ko#scale-type
미리보기 구현 | Android media | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 미리보기 구현 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱에 미리보기를 추가하려면 PreviewView
developer.android.com
https://developers.google.com/ml-kit?hl=ko
ML Kit | Google for Developers
모바일 개발자를 위한 Google의 기기별 머신러닝 키트입니다.
developers.google.com
https://developers.google.com/ml-kit/vision/face-detection/android?hl=ko
Android에서 ML Kit를 사용하여 얼굴 인식 | Google for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. Android에서 ML Kit를 사용하여 얼굴 인식 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. ML Kit를 사용하
developers.google.com
https://github.com/googlesamples/mlkit/tree/master/android/vision-quickstart
mlkit/android/vision-quickstart at master · googlesamples/mlkit
A collection of sample apps to demonstrate how to use Google's ML Kit APIs on Android and iOS - googlesamples/mlkit
github.com
'android' 카테고리의 다른 글
| Android Network Binding: Switching Wi-Fi and Cellular (0) | 2025.07.13 |
|---|---|
| 안드로이드 스튜디오 Java 버전 변경(Gradle, JDK, AGP) (0) | 2025.03.25 |