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

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

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

  1. detectFaces(in: completionHandler:) – 주어진 이미지(CIImage)에서 얼굴을 찾아낸다.
  2. createAnonymizedImage(from: boundingBoxes) – 주어진 이미지와 사각형영역을 사용해서 해당 영역이 모자이크처리된 이미지를 합성한다.
  3. 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 타입이다.

이미지를 익명화하는 순서는 다음과 같다.

  1. 먼저 이미지 전체를 모자이크 처리한 사본을 하나 만든다.
  2. 다음으로 각 boundingBox에 해당하는 영역을 흰색으로 칠한 투명 이미지 하나를 만든다. 이 이미지는 마스크로 쓰일 것이다.
  3. 다시 원래의 소스 위에 1에서 만든 이미지에 2의 이미지를 마스킹하여 합성한다. 그리하면 얼굴의 영역만 모자이크된 이미지가 남아서 원본 위에 덧그려진다.
  4. 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를 사용한다. CIContextwritePNGRepresentation(of:to:format:colorSpace:options:)라는 이미지 파일 기록 메소드를 제공하고 있다.

이 메소드에서는 format 이라는 파라미터를 요구하는데, 이는 각 픽셀의 데이터가 기록될 포맷이다. 우리는 여기서 RGB 및 Alpha 채널을 모두 사용하고, 여기에 각 픽셀은 8비트씩 사용하므로 .ARGB8 (XCode 9까지는 kCIFormatARGB8을 쓴다.) 값을 쓸 것이다.

다음은 프로그램의 전체 코드이다. 이미지의 얼굴이 너무 작은 경우나 얼굴이 옆으로 누워있는 경우에는 인식이 잘 안되는 것 같다.