본문 바로가기

OnePIC (Android App)/사진 편집 기능

[얼굴 변경 :Face Blending] 얼굴 잘라서 사진 위에 합성

반응형

 

동시에 찍힌(연사된) n장의 사진이 주어질 때, 동일 얼굴을 찾아서 바꾸는 기능을 구현하고자 한다.

해당 기능을 본 프로젝트에서는 Face Blending이라고 칭한다.

 

본 포스팅에서는 두 사진에서 원하는 얼굴을 잘라서 합성하는 방법에 대해 작성하려고 한다.

 

두 사진에서 원하는 얼굴을 찾는 방법은 아래 포스팅에서 확인할 수 있다.

 

[얼굴 변경 :Face Blending] 두 사진에서 동일 얼굴 찾기

동시에 찍힌(연사된) n장의 사진이 주어질 때, 동일 얼굴을 찾아서 바꾸는 기능을 구현하고자 한다. 해당 기능을 본 프로젝트에서는 Face Blending이라고 칭한다. 본 포스팅에서는 두 사진에서 동일

kyumq.tistory.com

 


 

얼굴 위치 값(Rect)에 따라 크롭 및 합성

MLKit FaceDetection을 통해 얻어지는 얼굴 위치 값(Rect)에 따라 이미지를 자르고 합성한다.

 

1. 이미지 자르기

 

val result = Bitmap.createBitmap(
    bitmap,
    left,                // X 시작위치
    top,                 // Y 시작위치
    right-left,          // 넓이
    bottom-top           // 높이
)

 

 

2. 이미지 합치기

 

앞서 구한 original(얼굴을 바꾸고 싶은 사진) 속 해당 얼굴 위치(Rect)에 change(바꾸고자하는 사진)에서 자른 이미지를 합성한다.

 

MLKit로 구한 해당 얼굴 위치(Rect)에 위에 자른 이미지 합성한다. (캔퍼스에 이미지를 올리고 사진을 합성하는 방법으로 합성)

 

    /**
     * overlayBitmap(original: Bitmap, add: Bitmap, optimizationX:Int, optimizationY:Int): Bitmap
     *      - original 이미지에 add 이미지를 (x, y) 좌표에 추가,
     *        만들어진 bitmap을 반환
     */
    fun overlayBitmap(original: Bitmap, add: Bitmap, x:Int, y:Int): Bitmap {

        var startX = x
        var startY = y

        if (startX < 0) {
            startX = 0
        }
        if (startY < 0) {
            startY = 0
        }

        //결과값 저장을 위한 Bitmap
        val resultOverlayBmp = Bitmap.createBitmap(
            original.width, original.height, original.config
        )

        //캔버스를 통해 비트맵을 겹치기한다.
        val canvas = Canvas(resultOverlayBmp)
        canvas.drawBitmap(original, Matrix(), null)
        canvas.drawBitmap(add, startX.toFloat(), startY.toFloat(), null)

        return resultOverlayBmp
    }

 

 


 

이렇게 크롭 후 합성할 경우, 합성한 것이 너무 티가 나는 단점이 존재한다.

위와 같이 얼굴만 자르고 싶은데 주변 환경이나 배경이 같이 잘려져 부자연스럽게 합성된다.

 

이는 얼굴이 크기 잘리는 것이 문제가 되는 것이다.

그럼 어떻게 얼굴만 자를 수 있을지를 알아내야한다.

 


얼굴 Landmark 위치 이용 

 

MLKit의 FaceDetection 전에 Face Detector 구성할 때, setLandmarkModeLANDMARK_MODE_ALL로 설정한다.

눈, 귀, 코, 뺨, 입과 같은 얼굴의 '표지점'을 식별할 것인지 여부입니다.

 

 

얼굴 랜드마크는 List<Point>로 다음 위치들의 정보를 알 수 있다.

 

0: 입 아래  |  1: 왼쪽 볼     |  2: 왼쪽 볼(광대)      |  3: 왼쪽 눈 중앙    |  4: 왼쪽 입꼬리
5: 코 중앙  |  6: 오른쪽 볼  |  7: 오른쪽 볼(광대)  |  8: 오른쪽 눈 중앙 |  9: 오른쪽 입꼬리

 

 

1. 너비 조정

 

( 2: 왼쪽 볼, 7: 오른쪽 볼 )를 이용해 최대한 얼굴만 자를 수 있도록 너비 조정

 

2. 높이 조정

 

눈 중앙하고 입 아래으로만 높이를 조정하기에는 눈썹과 턱 등 포함되지 않는 얼굴이 생긴다.

그래서 눈 중앙 위로 조금 더, 입 아래 밑으로 조금 더 공간을 두기 위해 다음 두 여유 공간을 추가한다.

 

눈 중앙 위로는 다음 그림처럼 눈부터 코 중앙까지의 높이를 추가한다.

입 아래 밑으로는 다음 그림처럼 코 중앙부터 입 아래까지의 높이를 추가한다.

 

정리하자면,

top = 눈 중앙 - (코 중앙 - 눈 중앙)

bottom = 입 아래 + (입 아래 - 코 중앙)

 

높이 = top부터 bottom 까지

 


최종 얼굴 자르기

 

최종적으로 빨간색 선 혹은 점이 넓이와 높이를 알 수 있는 끝 값이다.

그리고 최종적으로 잘라지는 얼굴 부분은 보라색 네모이다.

 

 


 

아직도 조금 부자연스러울 때가 존재한다.

 

이를 해결하기 위해 얼굴을 동그랗게 잘라서 합성시키기로 했다.

부자연스러움이 더욱 해결되었다.

 


 

3. 동그랗게 자르기

 

    /**
     * circleCropBitmap(bitmap: Bitmap): Bitmap
     *      - bitmap을 둥글게 잘라 반환
     */
    fun circleCropBitmap(bitmap: Bitmap): Bitmap {
        val output = Bitmap.createBitmap(
            bitmap.width,
            bitmap.height, Bitmap.Config.ARGB_8888
        )
        val canvas = Canvas(output)
        val color = -0xbdbdbe
        val paint = Paint()
        val rect = Rect(0, 0, bitmap.width, bitmap.height)
        paint.isAntiAlias = true
        canvas.drawARGB(0, 0, 0, 0)
        paint.color = color
        // canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
        canvas.drawCircle(
            (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat(),
            (bitmap.width / 2).toFloat(), paint
        )
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
        canvas.drawBitmap(bitmap, rect, rect, paint)

        return output
    }

 

반응형