iOS

Vision Framework로 얼굴인식하기

소재훈 2023. 5. 5. 01:13

Notion 블로그에 작성되어 있던 포스트 입니다. Notion의 접속 속도가 느려 medium 블로그에 다시 정리하게 되었습니다. 기존의 노션링크로도 읽어보실 수 있습니다.

되새김 프로젝트 중 사용자가 다이어리에 등록한 이미지로 얼굴인식을 하고 모자이크 처리하는 부분을 담당하게 되었습니다.

얼굴을 인식하기 위해서 애플에서 기존에 제공하는 기능이 있습니다. 그중에서 이번에는 Vision Framework를 사용하게 되었습니다. Vision Framework 에서는 얼굴인식 뿐만이 아니라 사각형, 바코드, 텍스트 등등 여러가지 기능을 기본적으로 제공하고 있었습니다.

개인적으로 경험해본 이후로의 생각은 프레임워크를 이용해서 인식시키는 작업은 그다지 어렵지 않았지만

결과로 나온 데이터를 사용자에게 어떻게 표현하고, 응용하는 부분이 고민되었습니다.

얼굴인식 Request 작성하기

얼굴인식을 위해서는 먼저 Request를 작성해야 합니다. 얼굴인식 부분에서는 VNDetectFaceRectangleRequest가 이를 담당하고 있습니다.

이후에 요청들을 만들고, 배열에 담아주고 요청을 perform하게 되면

request.results로 결과를 받아볼 수 있는 구조입니다.

VNDetectFaceRectangleRequest는 얼굴을 인식해서 얼굴 부분을 사각형의 bound 결과로 보여줍니다.

만약 이목구비까지 감지하고싶다면 VNDetectFaceLandmarksRequest를 정의해야합니다.

private lazy var faceDetectionRequest = VNDetectFaceRectanglesRequest(completionHandler: handleDetectedFaces)

컴플리션 핸들러도 등록되어 있는 것을 볼 수 있는데, 얼굴인식작업이 끝나고 나서 수행할 작업을 completionHandler로 넣어주면됩니다. 저같은 경우에는 사용자에게 얼굴인식된 부분을 노란색 사각형으로 표시해 주고 싶어서 Core Animation을 이용해서 Layer를 그려주는 작업을 컴플리션 핸들러에 등록해주었습니다.

만약 M1 애플실리콘 환경에서 실기기로 사용하지 않고 시뮬레이터로 작업하고 있다면 다음 코드를 삽입해주어야합니다. 그렇지 않다면 언제나 인식작업이 실패하게 됩니다.

#if targetEnvironment(simulator)
        faceDetectionRequest.usesCPUOnly = true
#endif

얼굴인식에 필요한 데이터 준비하기.

얼굴인식 작업을 하기 위해서 필요한 데이터로는 무엇이 있을까요?

  1. 얼굴인식을 수행할 이미지.
  2. 이미지의 CGImagePropertyOrientation (Orientation 정보)

두 가지가 필요합니다. 이미지의 orientation이 왜필요하지? 라고 생각하실 수 있는데 공식문서에는 다음과같이 적혀있습니다.

우리가 이미지를 기반으로 요청을 처리할 때 VNImageRequestHandler라는 것을 사용하는데 이것이 이미지가 수직으로 똑바로 세워져 있다는 것을 가정하고 데이터를 처리하기 때문에 방향에 대한 정보도 함께 전달해주어야 한다고 합니다. 문서를 더 읽어보면 이미지 데이터 형식별로 어떤 형식의 데이터를 전달해 주어야하는지 추가적으로 적혀있습니다.

CGImage는 CGImagePropertyOrientation을 제공해야 한다.

CIImage는 문서에 나온 생성자로 데이터를 전달해라..등등 정보가 나와있는것을 볼 수 있습니다.

저는 여기서 저희가 이미지를 주로 다룰때 UIImage를 사용할 수 있고, UIImage는 cgImage라는 프로퍼티로

간단하게 CGImage로 바꿀 수 있으므로 CGImagePropertyOrientation을 사용해서 orientation정보를 제공해보겠습니다. 순수 CGImagePropertyOrientation의 생성자로는 현재 UIImage에서 orientation을 뽑아낼 수 없어서

간단하게 익스텐션을 선언해 주었습니다. UIImage의 orientation이나 거기에서 뽑아넨 CGImage의 orientation이나 결국에는 동일하기 때문입니다.

extension CGImagePropertyOrientation {
    init(_ uiImageOrientation: UIImage.Orientation) {
        switch uiImageOrientation {
        case .up: self = .up
        case .down: self = .down
        case .left: self = .left
        case .right: self = .right
        case .upMirrored: self = .upMirrored
        case .downMirrored: self = .downMirrored
        case .leftMirrored: self = .leftMirrored
        case .rightMirrored: self = .rightMirrored
        default: self = .up
        }
    }
}

공식문서의 예제에서도 위와같은 방법을 사용해서 orientation을 구하는 것을 볼 수 있었습니다.

실제로 사용할 때는 다음과 같이 사용합니다. image라는 저희가 다룰 이미지, UIImage 인스턴스입니다.

let cgOrientation = CGImagePropertyOrientation(image.imageOrientation)

UIImage의 imageOrientation을 통해서 UIImage.Orientation타입의 열거형을 얻어오고, 이를 저희가 정의한 익스텐션을 통해서 CGImagePropertyOrientation열거형으로 변환하는 것입니다.

Vision Request 만들기

그러면 이제 진짜 저희가 요청할 Request를 만들어보기로 하겠습니다.

사실 Request는 첫번째 단계에서 이미 진행했습니다. faceDetectionRequest프로퍼티를 만들어주었던 것을 기억하시죠?

이것을 [VNRequest]의 배열에 넣어주면 됩니다.

얼굴이 위치한 곳을 직사각형으로 표현해달라는 요청뿐이지만 여러가지 요청이 있으면 요청을 작성한 후에 배열로 한꺼번에 넣어주면 됩니다.

VNImageRequestHandler 만들기

이제 저희가 만든 이미지 요청을 다루어줄 핸들러를 정의해야합니다. VNImageRequestHandler의 여러가지 생성자가 존재하지만, 어떤 이미지에 대해 처리하고 그 이미지의 orientation은 어떤지에 대해 파라미터로 넘겨주면 됩니다.

let imageRequestHandler = VNImageRequestHandler(
    cgImage: image,
    orientation: orientation,
    options: [:]
)

Request Handler 실행하기

Request Handler를 실행하는 방법은 아주 간단합니다.

저희가 만든 리퀘스트 배열을 perform메서드의 파라미터로 넘겨주면 요청 실행이 완료됩니다. 주의해야할 것은 문서에서도 main 스레드가 아닌 다른 스레드에서 작업을 수행할 것을 권장하고 있다는 점입니다.

얼굴을 인식하는데에는 시간이 걸릴 수 있기 때문에 작업시간이 오래걸리면main스레드에서 작업하는 경우 사용자의 UI경험이 저하될 수 있기 때문입니다.

DispatchQueue.global(qos: .userInitiated).async {
    do {
        try imageRequestHandler.perform(reqeusts)
    } catch {
        print(error.localizedDescription)
        // TODO: - 에러처리. 알림 등
        return
    }
}

결과 처리하기

결과가 처리되면 저희가 컴플리션 핸들러로 등록했던 메서드, 클로저로 결과가 전달됩니다. 아까 Request를 작성할 때 completionHandler의 타입은 VNReqeust?, Error? 두 가지 타입을 파라미터로 가져야하는 조건이 있고, 이중에서 VNRequest?에 결과가 전달됩니다.

fileprivate func handleDetectedFaces(request: VNRequest?, error: Error?) {
    ...
}

결과는 VNRequest의 results 프로퍼티에서 받아볼 수 있습니다.

단 결과를 받아올 때도 받고싶은 결과에 따라서 타입 캐스팅을 시켜주어야합니다.저는 받아온 얼굴에 대한 정보를 보고싶었기 때문에 [VNFaceObservation] 타입으로 캐스팅해주었습니다.

guard let drawLayer = self.pathLayer,
      let results = request?.results as? [VNFaceObservation] else { return }

여기까지가 얼굴이 인식된 부분의 결과를 받아오는 과정입니다. 사실 얼굴인식 자체는 Vision Framework라는 이미 제공되는 기능을 사용하는 것이기 때문에 그다지 어렵지 않았습니다.

진짜로 고민이 되었던 부분은 제공되는 정보를 어떻게 정제해서 반영시킬지에 대한 것이였습니다. 그냥 받아온 데이터 그대로 사용하면 되는 것 아니야? 라고 생각하실 수도 있지만 고민하게 된 이유에는 두 가지가 있습니다.

  • 화면에서의 좌표는 좌측 상단을 기준으로 제공되지만, Vision Framework의 작업 결과의 좌표는 좌측 하단을 기준으로 제공된다.
  • 얼굴이 인식된 좌표는 이미지 사이즈에 따른 절대적인 크기가 아니라 0.0 ~ 1.0 사이의 상대적인 크기로 제공된다.

이 두가지를 고려해서 사용자에게 보여주는 데이터를 따로 정제해야하는 부분입니다. 다음에는 어떻게 이러한 데이터를 정제하였는지 그 부분에 대해서도 다루어보도록 하겠습니다.