킹의 개발일지

CameraX에 MLKit 얹기 (1) 본문

Android

CameraX에 MLKit 얹기 (1)

k1ng 2023. 2. 7. 02:50

휴대폰 카메라로 사람의 동작을 인식한다면 얼마나 멋있을까...! 휴대폰을 앞에 두고 팔로 O나 X를 만들 때, 디바이스가 그 동작을 인식하고 특정 로직을 실행시 킬 수 있다면 그만한 재미는 없을 것 같다!  마치 아이언맨이 된것같은 기분이 들지 않을까 싶다. ㅎㅎ

 

이번 글에서는 안드로이드의 Jetpack의 CamearX 라이브러리에다가 MLKit에서 제공하고 있는, 실시간으로 사람의 동작을 인식하는 PoseDetection을 적용시켜 보려고 한다!

 

https://developers.google.com/ml-kit/vision/pose-detection

 

자세 인식  |  ML Kit  |  Google Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 자세 인식 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 API는 베타 버전으로 제공됩

developers.google.com

 

MLKit에서 제공하는 Pose detection에 대한 자세한 정보는 위 사이트에서 찾아 볼 수 있다.

 

또한 CameraX 라이브러리에 대한 설명은 아래 사이트에서 찾아 볼 수 있다.

https://developer.android.com/training/camerax?hl=ko 

 

CameraX 개요  |  Android 개발자  |  Android Developers

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. CameraX 개요   Android Jetpack의 구성요소 CameraX는 더 쉬운 카메라 앱 개발을 위해 빌드된 Jetpack 라이브러리입니

developer.android.com


PoseDetection

아래 짤을 보면 팔굽혀 펴기, 스쿼트 동작을 실행할 때마다 동작을 인식하고 숫자가 하나씩 올라가고 있다. 이는 MLKit의 PoseDetection API가 제공하는 대표적인 사용 케이스다. 이것뿐만 아니라 팔로 O 모양을 만들거나 X 모양을 만들 때마다 특정 로직을 실행할 수 도 있다.

출처:https://developers.google.com/ml-kit/vision/pose-detection/classifying-poses

우선 PoseDetection에서 제공하는 33개의 PoseLandmark 를 보자. 왼쪽 어깨부터 시작해서 오른쪽 입 까지 있다. 만약 사람이 image에 잡히면 pose detection API는 아래 랜드마크들을 반환한다.

// Get all PoseLandmarks. If no person was detected, the list will be empty
val allPoseLandmarks = pose.getAllPoseLandmarks()

// Or get specific PoseLandmarks individually. These will all be null if no person
// was detected
val leftShoulder = pose.getPoseLandmark(PoseLandmark.LEFT_SHOULDER)
val rightShoulder = pose.getPoseLandmark(PoseLandmark.RIGHT_SHOULDER)
val leftElbow = pose.getPoseLandmark(PoseLandmark.LEFT_ELBOW)
val rightElbow = pose.getPoseLandmark(PoseLandmark.RIGHT_ELBOW)
val leftWrist = pose.getPoseLandmark(PoseLandmark.LEFT_WRIST)
val rightWrist = pose.getPoseLandmark(PoseLandmark.RIGHT_WRIST)
val leftHip = pose.getPoseLandmark(PoseLandmark.LEFT_HIP)
val rightHip = pose.getPoseLandmark(PoseLandmark.RIGHT_HIP)
val leftKnee = pose.getPoseLandmark(PoseLandmark.LEFT_KNEE)
val rightKnee = pose.getPoseLandmark(PoseLandmark.RIGHT_KNEE)
val leftAnkle = pose.getPoseLandmark(PoseLandmark.LEFT_ANKLE)
val rightAnkle = pose.getPoseLandmark(PoseLandmark.RIGHT_ANKLE)
val leftPinky = pose.getPoseLandmark(PoseLandmark.LEFT_PINKY)
val rightPinky = pose.getPoseLandmark(PoseLandmark.RIGHT_PINKY)
val leftIndex = pose.getPoseLandmark(PoseLandmark.LEFT_INDEX)
val rightIndex = pose.getPoseLandmark(PoseLandmark.RIGHT_INDEX)
val leftThumb = pose.getPoseLandmark(PoseLandmark.LEFT_THUMB)
val rightThumb = pose.getPoseLandmark(PoseLandmark.RIGHT_THUMB)
val leftHeel = pose.getPoseLandmark(PoseLandmark.LEFT_HEEL)
val rightHeel = pose.getPoseLandmark(PoseLandmark.RIGHT_HEEL)
val leftFootIndex = pose.getPoseLandmark(PoseLandmark.LEFT_FOOT_INDEX)
val rightFootIndex = pose.getPoseLandmark(PoseLandmark.RIGHT_FOOT_INDEX)
val nose = pose.getPoseLandmark(PoseLandmark.NOSE)
val leftEyeInner = pose.getPoseLandmark(PoseLandmark.LEFT_EYE_INNER)
val leftEye = pose.getPoseLandmark(PoseLandmark.LEFT_EYE)
val leftEyeOuter = pose.getPoseLandmark(PoseLandmark.LEFT_EYE_OUTER)
val rightEyeInner = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE_INNER)
val rightEye = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE)
val rightEyeOuter = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE_OUTER)
val leftEar = pose.getPoseLandmark(PoseLandmark.LEFT_EAR)
val rightEar = pose.getPoseLandmark(PoseLandmark.RIGHT_EAR)
val leftMouth = pose.getPoseLandmark(PoseLandmark.LEFT_MOUTH)
val rightMouth = pose.getPoseLandmark(PoseLandmark.RIGHT_MOUTH)

이 랜드마크들을 가지고, 랜드마크들이 연결되어 있는 각도를 계산하여 특정 동작을 인식하는 것이다.

 

CamearX는 ImageAnalysis.Analyzer 인터페이스를 구현 함으로써 '이미지 분석 기능' 을 할 수 있도록하는 기능을 제공한다. 아래 링크를 보면 CamearX에 MLKit API를 입힐 수 있는 방법을 제공하고 있다.

 

https://developer.android.com/training/camerax/mlkitanalyzer

 

ML Kit 분석 도구  |  Android 개발자  |  Android Developers

ML Kit 분석 도구 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Google의 ML Kit는 얼굴 인식, 바코드 스캔, 이미지 라벨 지정 등을 위한 기기 내 머신러닝 Vision A

developer.android.com


 

imageAnalyzer

우선 위에서 언급한 ImageAnalysis.Analyzer 인터페이스를 우리가 원하는 기능을 수행하도록 구현해보자.

private class PoseAnalyzer(
        private val poseDetector: PoseDetector, private val onPoseDetected: (pose: Pose) -> Unit
    ) : ImageAnalysis.Analyzer {

        override fun analyze(imageProxy: ImageProxy) {
            val mediaImage = imageProxy.image ?: return
            val inputImage =
                InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

            poseDetector.process(inputImage)
                .addOnSuccessListener { pose ->
                    onPoseDetected(pose)
                }
                .addOnFailureListener { e ->
                    //handel error
                }
                .addOnCompleteListener {
                    imageProxy.close()
                    mediaImage.close()
                }
        }
    }

ImageAnalysis.Analyzer 인터페이스는 내부적으로 analyze 메서드를 구현해야한다.

 

이미지에서 포즈를 감지하려면 Bitmap, media.Image, ByteBuffer, 바이트 배열 또는 디바이스의 파일에서 InputImage 객체를 만들어야한다. 그리고 이를 PoseDetector에게 전달해야한다.

 

media.Image로 만들어진 InputImage를 poseDetector.process(inputImage)의 파라미터로 보내 분석을 시작한다. 만약 사람을 감지하는데 성공한다면, onPoseDetected 메서드에 감지한 pose 객체를 넘기고 호출한다. onPoseDetected에 대해서는 뒤에서 다시 설명하도록 하겠다.

 

그리고 모든 과정이 끝난다면 onCompleteListener에서 이미지들을 close한다.


이렇게 구현한 Analyzer를 카메라에 세팅을 해줘야 정상적으로 동작 할 수 있다. 먼저 카메라 기능을 수행하는 startCamera() 메서드를 살펴보자.

private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val options = PoseDetectorOptions.Builder()
                    .setDetectorMode(PoseDetectorOptions.STREAM_MODE)
                    .build()

            poseDetector = PoseDetection.getClient(options)

            // Preview
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
                }

            val imageAnalyzer = ImageAnalysis.Builder()
                .build()
                .also {
                    it.setAnalyzer(
                        cameraExecutor, PoseAnalyzer(poseDetector, onPoseDetected)
                    )
                }
            // Select back camera as a default
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageAnalyzer)

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }

위의 startCamera 메서드는 안드로이드 디벨로퍼에서 제공하는 codelab에서, 튜토리얼로 제공되는 ImageAnalysis use case에서 poseDetection 기능을 수행하도록 ImageAnalysis 인스턴스만 바꿔주었다.

 

우선 포즈를 분석하기 위해서는 PoseDetector가 필요하다. PoseDetector는 원하는 option 값을 주어서 인스턴스를 만들어 낼 수 있는데, 옵션에는 Detection mode를 정할 수 있는 기능이 있다.

 

기본적으로 PoseDetector는 STREAM_MODE를 사용하는데, 이는 비디오나 실시간 영상분석을 하는데 사용되고 두번 째 모드인 SINGLE_IMAGE_MODE는 정적 파일 분석에 사용된다. 우리는 STREAM_MODE를 사용할 것이다 이후 options를 가지고 PoseDetector 인스턴스를 만들어 낸다.

 

이렇게 만들어낸 imageAnalyzer를 통해서 cameraProvider에 바인딩시켜 카메라가 pose detection을 수행하도록 할 수 있다.


onPoseDetected 

위에서 카메라가 이미지를 감지하는데 성공했을 때 호출된다고 했던 onPoseDetected 메서드를 살펴보자.

private val onPoseDetected: (pose: Pose) -> Unit = { pose ->
        val isOSign = poseMatcher.match(pose, targetPoseOSign)
        val isXSign = poseMatcher.match(pose, targetPoseXSign)
        when {
            isOSign -> {
                Log.d(TAG, "o sign!!")
            }
            isXSign -> {
                Log.d(TAG, "x sign!!")

            }
        }
    }

 

해당 메서드에서는 poseMatcher의 match 메서드는 넘어온 pose객체와 targetPose를 비교해 두 포즈가 같다면, when 분기점에서 O 싸인 인지, X싸인 인지에 따라서 로그를 다르게 찍을 것이다.

 

poseMatcher.match에 대해서는 설명이 길어질 것 같아 다음 글에서 마무리 하도록 하겠다!