코어이미지를 사용한 QRCode 생성기

지난 시간에 코어 이미지를 사용해서 QRCode 인식기를 만드는 법에 대해서 간략히 설명하였는데, 그렇다면 반대로 문자열을 인코딩하여 QR코드를 만드는 것은 어떻게 할 수 있을까?

QR코드 생성은 코어 이미지를 통해서 할 수 있다. 코어이미지는 이미지 내의 바코드와 QR코드 탐지 API를 제공하는데, 반대로 해당 코드를 이미지로 생성하는 기능도 제공한다. QRCode 생성은 CIQRCodeGenerator 라는 이름의 CIFilter를 통해서 수행할 수 있다. 해당 필터의 파라미터는 다음과 같다.

  1. inputMessage : QR코드에 인코딩될 문자열 데이터이다. 해당 문자열은 UTF8로 인코딩된 바이트스트림으로 전달한다.
  2. inputCorrectionLevel : QR코드의 보정값 수준이다. 보통 “L” 을 쓰면 된다.

실제로 이미지를 만드는 방법은 다음과 같다.

let context = CIContext()
let message = "https://soooprmx.com"
if let data = message.data(using: .utf8),
   let filter = CIFilter(name: "CIQRCodeGenerator",
                         inputParameters:[
                            "inputMessage": data,
                            "inputCorrectionLevel": "L"])
{
  let qrCode = filter.outputImage()!
  let output = UIImage(ciImage: qrCode)
  ...
}

다만, 문제는 이렇게 생성된 이미지가 매우 크기가 작다는 문제이다. (정말 쥐똥만함….) 이 이미지를 보다 큰 이미지뷰에 넣으면 확대되면서 픽셀 보간1이 일어나서 이미지가 흐려지게 된다. (물론 적당히 크기가 크다면 이런 흐린 QR이미지도 왠만한 앱에 의해서는 다 인식된다.) 하지만 QR이미지를 파일로 저장하려고 하거나 하는 경우에는 흐릿한 이미지도, 너무 작은 이미지도 쓸 수가 없으므로 다음과 같이 확대하자. 코어 그래픽을 사용해서 픽셀 보간 없이 큰 영역에 해당 이미지를 그려주면 된다.

...
let qrCode = filter.outputImage()!
let cgImg = context.createCGImage(qrCode, from: cqCode.extent)!

// 비트맵 컨텍스트를 생성한다.
let v_size: Int = 400
guard let ctx = CGContext(data:nil, width: v_size, height: v_size,
                    bitsPerComponent:8, bytesPerRow:0,
                    space: CGColorSpaceCreateDeviceRGB(),
                    bitmapInfo: CGImageAlphaInfo.none.rawValue)
else {
   return
}

let outputFrame = CGRect(x:0, y:0, width: CGFloat(v_size), height: CGFloat(v_size))

// 보간 옵션을 제외한 후, QR코드를 확대하여 그린다.
ctx.interpolationQuality = .none
ctx.draw(cgImg, in: outputFrame)

// 최종적으로 확대된 결과물
let resultImage = ctx.makeImage()!

let qrCodeImage = UIImage(cgImage: resultImage)

  1. 흔히 ‘안티앨리어싱’이라 부르는 그것. 

코어 이미지 시작하기

코어이미지를 사용한 이미지 프로세싱 방법에 대해 알아보려고 한다. 가장 먼저 코어 이미지를 사용한 이미지 프로세싱에서 가장 핵심적인 세 가지 클래스에 대해서 살펴보자. 이들 클래스는 각각 CIContext, CIImage, CIFilter 이다.

코어 이미지 시작하기 더보기

코어 이미지를 통한 이미지 분석 예제

코어 이미지(Core Image)는 흔히 알려진 바와 같이 이미지에 대한 고성능 필터 효과 처리를 지원하는 프레임워크이면서 이미지에서 사람의 얼굴이나 QR코드, 텍스트를 탐지해내는 탐지 기능도 제공하고 있다. 코어 이미지가 제공하는 이미지 분석 기술을 제공하면 이러한 탐지를 빠르게 수행할 수 있을 뿐 아니라, 코어 이미지의 여러 필터 기능을 활용해서 찾아낸 부분을 하이라이트 처리하는 등의 기능을 손쉽게 구현할 수 있다. 이 글에서는 코어 이미지의 디텍터 클래스인 CIDetector를 사용하여 이미지에서 특정한 형상을 찾는 방법에 대해서 알아보고자 한다.

이미지에서 특정한 형상을 찾기

코어이미지가 제공하는 이미지 분석 기능을 사용하면 이미지 내에서 특정한 형상(feature)을 찾을 수 있다. 이 작업에서는 크게 두 가지 클래스를 다루게 된다.

  1. CIDetector : 이미지 분석처리를 담당한다.
  2. CIFeature : 분석된 결과에 대한 정보를 담는다.

CIDetector 클래스는 매우 간단한 API를 가지며, 다음의 절차를 거쳐 사용할 수 있다.

  1. init?(ofType: context: options:)를 통해서 새로운 디텍터를 생성한다.
  2. 생성된 디텍터에게 이미지를 전달하여 목표로 하는 형상을 탐지한다. 이 때 사용하는 메소드는 feature(in: options:) 이다.
  3. 탐지된 결과는 [CIFeature] 타입의 값으로 리턴된다.

CIFeature – 탐지된 결과에 대한 정보

CIFeature는 이미지 분석 결과에서 탐지된 매 항목에 대한 정보를 담고 있는데, 기본적으로 bounds 속성으로 이미지 내에서 해당 형상이 차지하는 부분을 알아 낼 수 있다. CIFeature는 여러 타입의 분석 결과에 대한 추상 클래스 타입이며, 어떤 것을 탐지하려고 했는가에 따라서 구체적인 서브 클래스를 사용하게 된다.

  1. CIRectangleFeature : 이미지 내에서 사각형을 탐지한 결과이다. 사각형은 딱 떨어지는 직사각형이 아니기 때문에 bottomLeft, bottomRight, topLeft, topRight 의 4개의 모서리 점 위치에 대한 정보를 추가적으로 가지고 있다.
  2. CITextFeature :  이미지에서 글자를 찾았을 때, 사용된다.
  3. CIQRCodeFeature : 이미지 내에서 QR코드를 탐지한 결과이다. QR코드를 디코딩한 문자열을 messageString 이라는 프로퍼티로 액세스할 수 있다.
  4. CIFaceFeature : 이미지 내에서 사람의 얼굴을 탐지한 결과이다.

이미지에서 사람 얼굴을 찾기

CIDetector를 사용하면 사람의 이미지 내에서 사람의 얼굴을 포착할 수 있다. 사람의 얼굴을 찾기 위해서는 CIDetector의 타입을 CIDetectorTypeFace로 주어 인스턴스를 생성한다. 이 때 컨텍스트는 기본적으로 nil을 전달할 수 있는데, 만약 소스로 주어지는 이미지와 연관된 CIContext 객체가 있다면 이를 넘겨줄 수 있다. (그렇게 하여 성능을 향상시킬 수 있다.) 주어진 이미지가 있을 때, 사람의 얼굴이 표현된 영역을 찾는 것은 다음과 같은 코드를 통해서 구현할 수 있다.

let image: CIImage = .... // #1
// #2
let detector = CIDetector(ofType: CIDetectorTypeFace,
                          context: nil,
                          options:
            [CIDetectorAccuracy: CIDetectorAccuracyHigh])!
// #3
if let features = detector.features(in: image) as? [CIFaceFeature] {
  let rects = features.map{ $0.bounds } // #4
  for rect in rects {
    // ... do something with face rect
  }
}
  1. 이미지는 이미 주어졌다고 가정한다.
  2. 디텍터를 생성한다. 디텍터 생성시 컨텍스트는 nil을 보낼 수 있고, 옵션은 정확도 수준을 지정할 수 있다.
  3. 이미지 내에 탐지되는 결과는 1개 이상일 수 있다. [CIFaceFeature]로 캐스팅한다.
  4. 각각의 CIFeature에 대해서 bounds 속성을 이용해서 이미지 내에 각 얼굴이 들어있는 영역을 지정할 수 있다.

얼굴이 들어간 부분을 하이라이트하기

얼굴을 찾아서 얼굴이 들어간 부분을 하이라이트하려면 어떻게 해야할까? CIImage 타입의 원본으로부터 이를 분석하여 얼굴의 영역들을 얻은 다음, 원본위에 하이라이트 영역을 칠한 결과물을 만드는 함수가 있다면 여기에 원본과 영역의 배열을 넘겨주어 최종 결과 이미지를 얻을 수 있다.

입력과 출력이 이렇게 결정되면 이 함수의 타입이 (CIImage, [CGRect]) -> CIImage라는 것을 알 수 있고, 이러한 함수를 구현하면 되는 것이다. 이를 구현하는 방법은 크게 코어 이미지를 이용해서 원본을 그린 후, 그 위에 하이라이트 영역을 그리는 방법이 있을 수 있고 또 하이라이트 영역에 대한 이미지를 만든 후에 이를 블렌드모드 필터를 이용해서 합성하는 방법이 있다. 여기서는 후자의 방법을 사용해보도록 하겠다.

/// 이미지의 특정 영역을 하이라이트 하기
func highlight(in source: CIImage, rects: [CGRect]) -> CIImage {
  let imageSize = source.extent.size
  let mask: CGImage = {
    let ctx = CGContext(data: nil, width: Int(imageSize.width), height: Int(imageSize.height),
                        bitsPerComponent: 8, bytesPerRow: 0,
                        space: CGColorSpaceCreateDeviceRGB(),
                        bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
    ctx.setFillColor(NSColor.yellow.cgColor)
    ctx.fill(rects)
    return ctx.makeImage()!
  }()

  // mask를 InputImage로 하고 source를 BackgroundImage로 해서 하이라이트된 이미지를 생성
  let inputImage = CIImage(cgImage: mask)
  return inputImage.applyingFilter("CIOverlayBlendMode", parameters:
         [kCIInputBackgroundImageKey: source])
}

사실 이 함수에서 이미지를 합성하는 방법만 약간 바꾸면, 어떤 사진 내에서 사람 얼굴만 모자이크 처리하는 익명화 프로그램을 만드는 도구를 간단히 생성할 수 있을 것이다. (원본을 모자이크/블러처리한 이미지를 만들고, 영역 내에 사각형을 그린 이미지와 컴포지팅하여 다시 원본에 덧그리는 방식이다. 이는 재미있는 토픽으로 보이니 조만간 살펴보도록 하겠다.)

QRCode를 찾기

이미지 분석에서 유용한 기술 중 하나는 QRCode를 탐지하고, QRCode내에 인코딩된 메시지를 얻는 것이다. 이는 위의 예제에서 Face 대신 QRCode만 넣으면 된다고 할 정도로 간단한 작업이다. QR코드 탐지 결과인 CIQRCodeFeaturemessageString이라는 프로퍼티를 통해서 인코딩된 메시지에 액세스할 수 있다.

다음 함수는 주어진 CIImage에 대해서 QRCode를 찾고, 그 속에 인코딩된 메시지를 완료 핸들러로 전달하는 함수이다.

/// QRCode를 찾아서 해석하기
func detectQRCode(in image: CIImage, completionHandler: ((String) -> Void)?) 
{
  let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil,
                   options:[CIDetectorAccuracy: CIDetectorAccuracyHigh])!
  if let result = detector.features(in: image).first as? CIQRCodeFeature,
     let message = result.messageString
  {
    completionHandler?(message)
  }
}

정리

코어 이미지가 제공하는 이미지 분석 도구는 Vision에 비해서 정확도는 아주 약간 떨어질 수 있지만, 여전히 빠르고 쓸만하게 동작한다. 또 API의 디자인 역시 심플하고 쉽게 사용할 수 있기 때문에 이를 사용해서 여러가지 유용한 도구들을 만들 수 있다.

참고자료

[Swift] 코어 이미지

Beginning Core Image in Swift

http://www.raywenderlich.com/76285/beginning-core-image-swift

코어 이미지는 이미지에 손쉽게 필터처리를 할 수 있는 강력한 프레임워크이다. 이는 어지간한 기본적인 사진 효과를 만들어낼 수 있는데, CPU 혹은 GPU 기반으로 프로세싱하고 있어서 매우 빠르다. 그냥 빠른 게 아니라 매우 빠르기 때문에 리얼타임으로 비디오 프레임에 적용될 수도 있다.

코어 이미지 필터는 여러 필터가 연쇄적으로 하나의 사진이나 비디오 프레임에 한꺼번에 적용될 수 있다. 이 때 여러 필터는 하나의 필터로 결합되어 이미지에 적용된다. 이런 방식은 한 번에 하나씩 이미지에 적용되는 것에 비해 훨씬 효율적이다. [Swift] 코어 이미지 더보기

Chaining Multiple CIFilters

Chaining CIFilters

Swift의 문법적 특징은 Objective-C를 사용할 때보다 훨씬 더 간결하고 간단한 구조의 코드를 작성할 수 있게 해주는데, 이번에는 Swift의 문법을 활용하여 CIFilter를 연쇄적으로 적용하는 자료 구조를 만들어 보도록 하겠다.

기본적으로 하나의 이미지에 여러 개의 필터를 적용하는 경우에는 각 필터의 outputImage 속성을 다음 순서의 필터의 kCIInputImageKey로 전달하는 방식으로 처리할 수 있다.

이를 아예 연산자로 정의하는 것도 가능하다. 이미지 인스턴스는 필터라는 상태로 둘러싸여 그 내부에서 조작되는 엔티티로 볼 수 있기 때문에 필터 자체를 모나드로 볼 수 있다. Chaining Multiple CIFilters 더보기