Wireframe

이미지를 익명화하는 프로그램 – Vision + CoreImage

Vision을 이용한 얼굴 인식의 보다 상세한 예제로 자동 익명화 프로그램을 작성해보았다. 이 프로그램은 터미널에서 이미지 파일의 이름을 제시하면 해당 이미지에서 사람의 얼굴을 찾아, 해당 영역을 모자이크 처리한 PNG 파일을 생성한다.

이 프로그램은 크게 세 개의 함수로 구성된다.

  1. detectFaces(in: completionHandler:) – 주어진 이미지(CGImage)에서 얼굴을 찾아낸다. 얼굴을 찾으면 해당 영역을 모자이크 처리한 이미지를 만들고, 처리된 이미지를 콜백 클로저에 전달해서 사용하도록 한다.
  2. createPixellatedImage(from:CGImage rects:[CGRect]) – 이미지와 정규화된 CGRect 들을 받아서 해당 영역을 모자이크 처리한 이미지를 합성하고, 다시 CGImage 형식으로 만들어 리턴한다.
  3. main() – 명령줄 인자로 전달된 정보를 통해 이미지 파일을 열고 1, 2의 함수를 사용해서 얼굴만 모자이크 처리한 이미지를 만들어 PNG 파일로 저장한다.

얼굴을 찾기

이미지 내에서 얼굴을 찾기 위해 Vision 프레임워크를 사용한다. 이 방법에 대해서는 이미 한 번 다룬바가 있는데, 간단히 절치를 정리하면 다음과 같다.

  1. 이미지에 대해 얼굴의 특성을 찾아내는 요청 객체를 만든다. 요청 객체를 생성할 때에는, 찾은 결과를 처리할 수 있는 콜백 클로저를 작성해준다.
  2. 요청의 완료 핸들러는 요청 객체 자신과 에러를 인자로 받는다. 처리된 요청에는 .results 속성이 있는데, 이 속성의 세부타입은 요청의 종류에 따라 다르다. 얼굴 인식의 경우 VNFaceObservation 인스턴스들이 각각의 얼굴에 대한 정보를 나타낸다.
  3. 생성한 요청 객체를 처리할 핸들러 객체를 생성한다. 핸들러 객체는 이미지를 기준으로 생성된다.
  4. 최종적으로 핸들러의 .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])
}

  

이미지 익명화하기

이미지의 특정 영역들을 모자이크 처리하는 함수를 구현할 차례이다. 특정한 부분이 모자이크 처리된 이미지는 전체가 모자이크 처리된 이미지와 원본 이미지를 합성하여 생성한다. 따라서 다음과 같은 과정을 거친다.

  1. 이미지 원본을 A라 하자.
  2. A에 대해서 전체를 모자이크 처리한 B를 만든다. 모자이크 처리에는 CIPixellate 필터를 사용한다.
  3. 비어있는 이미지를 하나 만들고, 모자이크 처리할 영역을 흰색등의 값으로 채워서 새로운 이미지를 만든다. 비어 있다는 것은 색을 칠하지 않은 영역의 픽셀들은 알파채널 값이 0이라는 뜻이다. 이 이미지를 C라 한다.
  4. 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:[:])
                }
            }
        }
}
Exit mobile version