Vision을 이용한 얼굴 인식의 보다 상세한 예제로 자동 익명화 프로그램을 작성해보았다. 이 프로그램은 터미널에서 이미지 파일의 이름을 제시하면 해당 이미지에서 사람의 얼굴을 찾아, 해당 영역을 모자이크 처리한 PNG 파일을 생성한다.
이 프로그램은 크게 세 개의 함수로 구성된다.
detectFaces(in: completionHandler:)
– 주어진 이미지(CGImage
)에서 얼굴을 찾아낸다. 얼굴을 찾으면 해당 영역을 모자이크 처리한 이미지를 만들고, 처리된 이미지를 콜백 클로저에 전달해서 사용하도록 한다.createPixellatedImage(from:CGImage rects:[CGRect])
– 이미지와 정규화된 CGRect 들을 받아서 해당 영역을 모자이크 처리한 이미지를 합성하고, 다시 CGImage 형식으로 만들어 리턴한다.main()
– 명령줄 인자로 전달된 정보를 통해 이미지 파일을 열고 1, 2의 함수를 사용해서 얼굴만 모자이크 처리한 이미지를 만들어 PNG 파일로 저장한다.
얼굴을 찾기
이미지 내에서 얼굴을 찾기 위해 Vision 프레임워크를 사용한다. 이 방법에 대해서는 이미 한 번 다룬바가 있는데, 간단히 절치를 정리하면 다음과 같다.
- 이미지에 대해 얼굴의 특성을 찾아내는 요청 객체를 만든다. 요청 객체를 생성할 때에는, 찾은 결과를 처리할 수 있는 콜백 클로저를 작성해준다.
- 요청의 완료 핸들러는 요청 객체 자신과 에러를 인자로 받는다. 처리된 요청에는
.results
속성이 있는데, 이 속성의 세부타입은 요청의 종류에 따라 다르다. 얼굴 인식의 경우VNFaceObservation
인스턴스들이 각각의 얼굴에 대한 정보를 나타낸다. - 생성한 요청 객체를 처리할 핸들러 객체를 생성한다. 핸들러 객체는 이미지를 기준으로 생성된다.
- 최종적으로 핸들러의
.perform()
메소드를 호출하면서 요청 객체를 전달해 준다. 한 번에 요청에 여러 종류의 디텍션을 처리할 수 있기에, 전달하는 값은 요청 객체의 배열 형식을 전달해야 한다.
각각의 감지 결과는 boundingBox
프로퍼티를 가지고 있는데, 이는 해당 feature가 이미지에서 차지하는 영역을 나타내는 CGRect 값이다. 이 값은 이미지의 가로, 세로를 각각 1로 정의했을 때를 기준으로 정규화되어 있다. 따라서 이를 이미지 합성에서 사용하기 위해서는 이미지 좌표에 맞게 환산해야 한다. 어렵지 않은 계산이지만, VNImageRectForNormalizedRect()
함수를 사용하면 이미지의 가로/세로 크기로 늘려진 사각형 값을 얻을 수 있다.
먼저 이미지를 감지하는 기능을 구현한 코드를 살펴보자.
import Vision
import CoreImage
import CoreGraphics
import Foundation
func detectFaces(in source: CIImage, completionHandler:((CIImage) -> Void)?)
{
let request = VNDetectFaceLandmarksRequest{ req, err in
guard err == nil else {
NSLog("Error: \(err!.localizedDescription)")
return
}
guard let results = req.results as? [VNFaceObservation] else {
NSLog("Fail: not supported result type.")
return
}
let rects = results.map{ $0.boundingBox }
let resultImage = createPixellatedImage(from: source, rects: rects)
if let resultImage = resultImage {
completionHandler?(resultImage)
}
}
let handler = VNImageRequestHandler(ciImage: source, options: [:])
try? handler.perform([request])
}
이미지 익명화하기
이미지의 특정 영역들을 모자이크 처리하는 함수를 구현할 차례이다. 특정한 부분이 모자이크 처리된 이미지는 전체가 모자이크 처리된 이미지와 원본 이미지를 합성하여 생성한다. 따라서 다음과 같은 과정을 거친다.
- 이미지 원본을 A라 하자.
- A에 대해서 전체를 모자이크 처리한 B를 만든다. 모자이크 처리에는
CIPixellate
필터를 사용한다. - 비어있는 이미지를 하나 만들고, 모자이크 처리할 영역을 흰색등의 값으로 채워서 새로운 이미지를 만든다. 비어 있다는 것은 색을 칠하지 않은 영역의 픽셀들은 알파채널 값이 0이라는 뜻이다. 이 이미지를 C라 한다.
- B를 A에 합성할 때, C를 알파채널 마스크로 사용하여 합성한다. 그렇게하면 C에서 사각형이 칠해진 영역만 B가 표시되고 나머지 영역은 A가 그려진다.
이 과정을 코드로 나타내면 다음과 같다.
func createPixellatedImage(from source: CIImage, rects: [CGRect]) -> CIImage?
{
let width = Int(source.extent.size.width)
let height = Int(source.extent.size.height)
let space = CGColorSpaceCreateDeviceRGB()
let ctx = CGContext(
data:nil,
width:width,
height:height,
bitsPerComponent:8,
bytesPerRow:0,
space:space,
bitmapInfo:CGImageAlphaInfo.premultipliedLast.rawValue)
ctx?.setFillcolor(CGColor(red:1.0, green:1.0, blue:1.0, alpha:1.0)
ctx?.fill(rects:rects.map{VNImageRectForNormalizedRect($0, width, height)})
guard case let mask_ = ctx?.makeImage(), case let mask = CIImage(cgImage:mask_)
else { return nil }
let result = source.applyingFilter("CIPixellate", parameters:[
kCIInputScaleKey: 50.0])
.applyingFilter("CIBlendWithAlphaMask", parameters:[
kCIInputMaskImageKey: mask,
kCIInputBackgroundImageKey: source])
let cix = CIContext()
return cix.createCGImage(result, from: result.extent.size)
}
조립하기
이상의 내용을 조립하는 메인 함수를 작성해보자. 메인함수에서는 파일 경로를 인자로 전달받아, 해당 이미지 파일을 처리하고 변환된 파일을 저장하면 된다. 생성된 최종 결과물은 CIImage
객체인데, 이 클래스는 실제로 필터가 적용된 결과가 아니라 원본 이미지와 적용해야할 필터의 목록을 가지고 있는 형태이다. 이로부터 실제 결과 이미지를 생성하기 위해서는 CIContext
를 사용한다. 코어이미지 컨텍스트는 실제 이미지를 렌더링하여 CGImage
를 만들거나 혹은 원하는 파일 포맷으로 기록할 수 있는 기능을 제공한다. 이미지를 PNG 파일로 저장하기 위해서는 writePNGRepresentation(of:to:format:colorSpace:options:)
를 사용하여 파일을 저장하면 된다. 여기서 사용되는 이미지 포맷은 kCIFormatARBG8
혹은 .ARGB8
을 주면 된다.
func main() {
guard case let arguments = CommandLine.arguments, arguments.count > 1 else {
print("Usage: pixellate <filepath>")
return
}
let filepath = (arguments[1] as NSString).expandingTildeInPath
if case let fileURL = URL(fileURLWithPath:filepath),
let image = CIImage(contentsOf:fileURL) {
let savingURL: URL = {
let filename = fileURL.lastPathComponent
let path = fileURL.deletingLastPathComponent()
return path.appendingPathComponent("result-\(filename).png")
}()
detectFaces(in: image){ resultImage in
defter {
DispatchQueue.main.async {
CFRunLoopStop(CFRunLoopGetMain())
}
}
do {
let context = CIContext()
try context.writePNGPresentation(
of:res,
to:savingURL,
format:kCIFormatARGB8,
colorspace:CGColorSpaceCreateDeviceRGB(),
options:[:])
}
}
}
}