[Android] 안드로이드 ML Kit를 사용한 얼굴 인식 예제

문승연·2024년 1월 25일
0

1. Android ML Kit

최근 구글에서 On-Device용 AI 칩을 발표했다. 내가 AI 개발자는 아니지만 호기심이 생겨서 알아보다가 안드로이드에서 사용할 수 있는 ML Kit 라는 이름의 공식 AI 라이브러리가 있는 걸 발견했다. 오늘은 해당 라이브러리를 이용해 가볍게 사진에서 사람의 얼굴을 인식하는 예제를 구현해볼 예정이다.

1. ML Kit 라이브러리 import하기

ML Kit 라이브러리를 import 하는 방법은 2가지가 있다.
1. 번들로 필요한 모델을 빌드시간에 정적으로 연결하는 방법
2. Google Play 서비스를 통해 모델을 동적으로 다운로드하는 방법이다.

1번의 방법으로 하면 앱 용량이 더 커지지만 (약 6.9MB 정도) 대신 모델이 빌드 과정에서 모두 연결되기 때문에 빠르게 모델을 사용할 수 있다. 반면 2번 방법은 용량은 작아지지만 모델을 동적으로 다운받기 때문에 처음 모델을 이용할 때 다운받는 시간이 필요하다.

둘 중에 개발하고자 하는 앱에 더 맞는 방법을 택하면 될 것 같다.

모델을 앱과 번들로 묶는 방법

dependencies {
  // ...
  // Use this dependency to bundle the model with your app
  implementation 'com.google.mlkit:face-detection:16.1.5'
}

Google Play 서비스에서 모델을 사용하는 경우

dependencies {
  // ...
  // Use this dependency to use the dynamically downloaded model in Google Play Services
  implementation 'com.google.android.gms:play-services-mlkit-face-detection:17.1.0'
}
<application ...>
      ...
      <meta-data
          android:name="com.google.mlkit.vision.DEPENDENCIES"
          android:value="face" >
      <!-- To use multiple models: android:value="face,model2,model3" -->
</application>

2. MainActivity UI 구성

버튼을 누르면 먼저 권한 체크를 한 뒤, 권한 확인 후 이미지를 가져오고 해당 이미지를 분석해 얼굴로 인식된 곳에 초록색 박스를 표시하는 동작을 구현할 것이다.

먼저 UI부터 구성한다.

class MainActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContent {
			MainScreen()
		}
	}
}

@Composable
private fun MainScreen() {
	val context = LocalContext.current

	var imageBitmap by remember { mutableStateOf(ImageBitmap(32, 32)) }

	Column(
		modifier = Modifier.fillMaxSize()
	) {
		Button(
			onClick = {
				// TODO 
			}
		) {
			Text(text = "갤러리에서 불러오기")
		}

		Canvas(
			modifier = Modifier.fillMaxSize()
		) {
			drawImage(
				image = imageBitmap
			)
            // TODO (얼굴에 사각형 그리기)
		}
	}
}

이제 버튼을 눌렀을 때 로직을 구성한다. onClick() 로직은 다음과 같다.
1. 이미지 접근 권한 확인 (checkPermission)
2. 권한이 있을 경우 갤러리에서 이미지 불러오기
3. 권한이 없을 경우 권한 동의하기

먼저 갤러리 이미지 접근 권한을 확인하는 로직을 구성하자.

3. PermissionChecker 기능 추가

PermissionChecker.kt 파일을 따로 생성해 함수를 추가해준다. 예제에서는 이미지 접근 권한만 확인하면 되지만 어떤 권한 요청이 필요하더라고 확인할 수 있게 구현했다.

fun checkSinglePermission(
	context: Context,
	permission: String
): Boolean {
	if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
		Log.d("test5", "권한이 이미 존재합니다.")
		return true
	}
	return false
}

fun checkMultiplePermission(
	context: Context,
	permissions: List<String>
): Boolean {
	permissions.forEach {
		if (ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED)
			return false
	}
	return true
}

그 다음 MainActivity에 적용한다.

Button(
	onClick = {
    	if (checkSinglePermission(context, Manifest.permission.READ_MEDIA_IMAGES)) {
        	// TODO (이미지 가져오기)
		} else {
			// TODO (권한 요청하기)
		}
	}
) {
	Text(text = "갤러리에서 불러오기")
}

4. 권한 요청하기

권한이 없을 때 사용자에게 권한을 요청하는 기능을 추가한다.

...
	val requestPermissionLauncher = rememberLauncherForActivityResult(
		contract = ActivityResultContracts.RequestPermission()
	) { isGranted ->
		if (isGranted) {
			Log.d("test5", "권한이 동의되었습니다.")
		} else {
			Log.d("test5", "권한이 거부되었습니다.")
		}
	}
    
    ...
    
    Button(
			onClick = {
				if (checkSinglePermission(context, Manifest.permission.READ_MEDIA_IMAGES)) {
					// TODO (이미지 가져오기)
				} else {
					requestPermissionLauncher.launch(Manifest.permission.READ_MEDIA_IMAGES)
				}
			}
		) {
			Text(text = "갤러리에서 불러오기")
		}

Compose에서 기존의 startActivityForResult를 사용하려면 rememberLauncherForActivityResult를 통해 Launcher 객체를 가져와야한다.

그런데 이 rememberLauncherForActivityResultComposable 함수라서 외부로 빼기가 쉽지 않다. UI 레이어와 비즈니스 로직을 분리하고 싶은데 유독 이 rememberLauncherForActivityResult를 사용할 때는 잘 안된다. 나중에 이 부분도 한번 찾아봐야겠다.

아무튼 이제 갤러리에서 이미지를 불러오는 과정만 진행하면 된다.

5. 갤러리에서 이미지 불러오기

...

val pickGalleryLauncher = rememberLauncherForActivityResult(
		contract = ActivityResultContracts.PickVisualMedia()
	) {
		if (it == null)
			throw NullPointerException()
		val source = ImageDecoder.createSource(context.contentResolver, it)
		imageBitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
			decoder.setTargetSize(1080, 1080)
		}.asImageBitmap()
	}
    
    ...
    
    Button(
			onClick = {
				if (checkSinglePermission(context, Manifest.permission.READ_MEDIA_IMAGES)) {
					pickGalleryLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
				} else {
					requestPermissionLauncher.launch(Manifest.permission.READ_MEDIA_IMAGES)
				}
			}
		) {
			Text(text = "갤러리에서 불러오기")
		}

6. FaceDetector로 얼굴 인식하기

먼저 이미지를 처리하고 얼굴을 인식해 줄 ImageProcessor 를 구현한다.

object ImageProcessor {

	private val highAccuracyOpts = FaceDetectorOptions.Builder()
		.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
		.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
		.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
		.build()

	private val detector = FaceDetection.getClient(highAccuracyOpts)

	suspend fun processInputImage(image: InputImage): List<Face> {
		return suspendCoroutine { continuation ->
			detector.process(image).addOnSuccessListener {  faces ->
				continuation.resume(faces)
			}
			.addOnFailureListener {
				throw it
			}
		}
	}
}

FaceDetection.getClient() 메소드를 통해 FaceDetector 객체를 가져올 수 있는데, 이 때 FaceDetectorOptions 객체를 이용해 설정을 지정할 수 있다.

위 예제에서는 PERFORMANCE_MODE_ACCURATE, LANMARK_MODE_ALL, CLASSIFICATION_MODE_ALL 을 사용했는데 각각 정확도 위주, 얼굴의 표지점 표시(눈, 코, 입 등), 카테고리 분류(웃음, 눈 뜸 등)을 뜻한다.

나는 얼굴 인식 기능만 사용할 거라 위 설정이 굳이 필요하지는 않았지만 한번 넣어봤다. 설정에 관한 자세한 정보는 공식 문서에서 확인해보자.

이제 ImageProcessor로 이미지를 분석할 수 있다. 나머지는 갤러리에서 불러온 이미지를 ImageProcessor에 넣고 결과값으로 나타난 얼굴 영역을 이미지 위에 그리는 것 뿐이다.

...

	val faces = remember { mutableStateListOf<Face>() }

	LaunchedEffect(imageBitmap) {
		faces.clear()

		val image = InputImage.fromBitmap(imageBitmap.asAndroidBitmap(), 0)
		faces.addAll(ImageProcessor.processInputImage(image))
	}
   
	...
    
    	Canvas(
			modifier = Modifier.fillMaxSize()
		) {
			drawImage(
				image = imageBitmap
			)
			faces.forEach { face ->
				val rect = face.boundingBox
				drawRect(
					color = Color.Green,
					style = Stroke(
						width = 2.dp.toPx()
					),
					topLeft = Offset(rect.left.toFloat(), rect.top.toFloat()),
					size = Size(rect.width().toFloat(), rect.height().toFloat()),
				)
			}
		}

이미지를 불러와서 imageBitmap 값이 변하면 LaunchedEffect 블럭 내 코드가 동작한다.

ImageProcessor.processInputImage()로 불러온 이미지를 분석하여 나온 얼굴값 List<Face>faces에 모두 넣어준다.

그리고 Canvas에서 각 얼굴마다 boundingBox 객체를 가져와 사각형으로 해당 얼굴 위치를 그려준다.

7. 결과

얼굴 인식이 정상적으로 작동되는 것을 확인할 수 있다. 실제로 사용해보면 꽤 속도도 빠르고 정확도도 높아서 배포된 공식 라이브러리만으로도 괜찮은 앱을 만들 수 있을 거 같다는 생각이 들었다.

Github 링크
참고문서: 안드로이드 공식 문서

profile
"비몽(Bemong)"이라는 앱을 개발 및 운영 중인 안드로이드 개발자입니다.

0개의 댓글