Vision을 이용한 얼굴 인식의 보다 상세한 예제로 자동 익명화 프로그램을 작성해보았다. 이 프로그램은 터미널에서 이미지 파일의 이름을 제시하면 해당 이미지에서 사람의 얼굴을 찾아, 해당 영역을 모자이크 처리한 PNG 파일을 생성한다.
이 프로그램은 크게 세 개의 함수로 구성된다.
detectFaces(in: completionHandler:)
– 주어진 이미지(CIImage
)에서 얼굴을 찾아낸다.createAnonymizedImage(from: boundingBoxes)
– 주어진 이미지와 사각형영역을 사용해서 해당 영역이 모자이크처리된 이미지를 합성한다.main()
– 명령줄 인자로 전달된 정보를 통해 이미지 파일을 열고 1, 2의 함수를 사용해서 얼굴만 모자이크 처리한 이미지를 만들어 PNG 파일로 저장한다.
얼굴을 찾기
이미지 내에서 얼굴을 찾기 위해 Vision 프레임워크를 사용한다. 이 방법에 대해서는 이미 한 번 다룬바가 있다. Vision이 얼굴을 찾아내면 찾아낸 결과인 VNFaceObservation
에는 boundingBox
속성이 있는데, 이를 통해 이미지 내의 얼굴이 위치한 영역들을 얻어낼 수 있고, 이 값을 createAnonymizedImage(from:boundingBoxes:)
에 넘겨서 모자이크 처리된 이미지를 얻을 것이다. 이렇게 만들어진 익명화처리된 이미지(CIImage
)를 completionHandler
에 전달해주면 된다.
func detectFaces(in source: CIImage, completionHandler:((CIImage) -> Void)?)
{
let request = VNDetectFaceLandmarksRequest{ req, error in
// 에러 확인
guard error == nil else {
NSLog("Error: \(error!.localizedDescription)")
return
}
// 결과 추출
let results = req.results as? [VNFaceObservation] else {
NSLog("Fail: Not supported result type.")
return
}
// 중간 보고
print("> found \(results.count) faces in the image")
print("> anonymizing...")
// 결과 이미지 생성
let boundingBoxes = results.map{ $0.boundingBox }
let resultImage = createAnonymizedMiage(from: source,
boundingBoxes:boundingBoxes)
completionHandler?(resultImage)
}
let handler = VNImageRequestHandler(ciImage: source, options: [:])
try? handler.perform([request])
}
이미지 익명화하기
이미지를 익명화하는 함수가 받는 boundingBoxes
파라미터는 0~1 사이의 위치와 크기로 노멀라이즈된 영역이다. 이를 실제 이미지 크기에 맞게 확대하는 작업이 필요한데, 이를 위해서 Vision에서 제공하는 함수인 VNImageRectForNormalizedRect(_:_:_:)
를 사용한다. 첫번째 인자는 노멀라이징된 CGRect
값이고 뒤의 두 인자는 이미지의 폭과 높이 값으로 각각 Int
타입이다.
이미지를 익명화하는 순서는 다음과 같다.
- 먼저 이미지 전체를 모자이크 처리한 사본을 하나 만든다.
- 다음으로 각
boundingBox
에 해당하는 영역을 흰색으로 칠한 투명 이미지 하나를 만든다. 이 이미지는 마스크로 쓰일 것이다. - 다시 원래의 소스 위에 1에서 만든 이미지에 2의 이미지를 마스킹하여 합성한다. 그리하면 얼굴의 영역만 모자이크된 이미지가 남아서 원본 위에 덧그려진다.
- 3의 결과를 리턴한다.
func createAnonymizedImage(from source: CIImage,
boundingBoxes: [CGRect]) -> CIImage
{
let (imageWidth, imageHeight) : (Int, Int) = {
let size = source.extent.size
return (Int(size.width), Int(size.height))
}()
// 마스크로 쓰일 비트맵이미지를 그리자.
let maskImage: CIImage = {
let ctx = CGContenxt(data: nil,
width: imageWidth,
height: imageHeight,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceGray(),
bitmapInfo: CGImageAlphaInfo.none.rawValue)!
ctx.setFillColor(NSColor.white.cgColor)
// fill(_ rects:) 를 이용해서 여러 사각형을 한 번에 그릴 수 있다.
ctx.fill(boundingBoxes.map{VNImageRectForNormalizedRect($0, imageWidth, imageHeight)})
return CIImage(cgImage: ctx.makeImage()!)
}()
// 모자이크 처리된 이미지
let inputImage = source.applyingFilter("CIPixllate",
parameters: [kCIInputScaleKey: 18.0])
// 합성한 이미지
let resultImage = inputImage.applyingFilter("CIBlendWithMask",
parameters:
[kCIInputMaskImageKey: maskImage,
kCIInputBackgroundImageKey: source])
return resultImage
}
조립하기
이상의 내용을 조립하는 메인 함수를 작성해보자. 메인 함수에서는 프로그램 실행 시 인자로 넘겨진 파일을 열어서 익명화처리를 수행하고 그 결과를 다시 PNG 파일로 저장할 것이다.
생성된 최종 결과물은 CIImage
인데, 이를 파일로 만들기 위해서는 CIContext
를 사용한다. CIContext
는 writePNGRepresentation(of:to:format:colorSpace:options:)
라는 이미지 파일 기록 메소드를 제공하고 있다.
이 메소드에서는 format
이라는 파라미터를 요구하는데, 이는 각 픽셀의 데이터가 기록될 포맷이다. 우리는 여기서 RGB 및 Alpha 채널을 모두 사용하고, 여기에 각 픽셀은 8비트씩 사용하므로 .ARGB8
(XCode 9까지는 kCIFormatARGB8
을 쓴다.) 값을 쓸 것이다.
다음은 프로그램의 전체 코드이다. 이미지의 얼굴이 너무 작은 경우나 얼굴이 옆으로 누워있는 경우에는 인식이 잘 안되는 것 같다.