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 속성에 명시적으로 설정해 사용했다.

 

예제에서는 아래 두 가지 설정에 대해 살펴보자.

  1. FILL_START
  2. 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