코어 이미지 시작하기

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

핵심 클래스들

컨텍스트 : CIContext

코어그래픽에서 컨텍스트는 실제 이미지 비트맵 데이터 영역을 캔버스로 하고, 여기에 그림을 그리는 여러가지 선과 색등에 관련된 옵션을 가지고 있는 객체였다. 이는 곧 프로그래밍 API를 통해서 제어하는 가상의 그림판 앱이나 마찬가지인 셈이다. 비슷하게 코어 이미지의 컨텍스트는 이미지에 필터를 적용하는 무대가 되며, 가상의 포토샵류 프로그램을 API를 통해서 제어한다고 생각하면 된다.

필터 : CIFilter

CIFilter는 포토샵의 필터와 같이 이미지를 처리해서 변형하는 장치를 표현하는 클래스이다. 코어 이미지에서는 엄청나게 많은 필터 효과를 지원하는데, 이들 각각의 필터가 각각 CIFilter의 서브 클래스로 존재하는 것이 아니라, CIFilter 하나로 사용하게 되고, 대신에 문자열로 필터 이름을 지정하여 사용할 효과를 선택하게 된다.

또한 필터마다 사용하는 파라미터가 다르기 때문에 (극단적으로 어떤 필터는 입력 이미지를 2개 이상 받기도 하고, 어떤 필터는 입력 이미지가 필요 없는 것도 있다.) 필터에 전달되는 모든 파라미터는 키-값쌍을 통해서 설정한다.

필터는 주로 CIImage 객체를 입력 값으로 받고, 다시 CIImage 객체를 출력한다. (원본은 immutable하다.) 이 출력은 outputImage 라는 프로퍼티로 접근이 가능하다. outputimage는 필터가 적용된 결과 이미지가 아니라, 필터가 어떤 식으로 적용될지에 대한 “레시피”가 쓰여진 상태이다. 실제로 하나의 효과를 만들기 위해서는 여러 가지의 필터를 순차적으로 적용하여 최종 효과를 만들게 되는데, 이 때 중간과정의 이미지를 실제로 렌더링하는 것은 메모리나 성능에 있어 그다지 도움이 되지 않을 것이기 때문이다.

최종 렌더링을 통해 결과 이미지를 얻어내는 통작은 CIImage에 기록된 “레시피”에 따라 컨텍스트를 통해서 이루어진다.

이미지 : CIImage

코어 이미지 프레임워크에서 CIFilter, CIContext 등과 같이 맞물려 동작하는 이미지 클래스. 하지만 CIImage는 (참조하는 이미지 데이터가 있을 수 있지만) 그 자체로 이미지가 아니다. 대신에 앞서 설명한 바와 같이 이미지를 만드는 레시피가 기록된 객체라고 보면 된다. 필터로부터 생성되는 outputImage는 필터의 입력 이미지에 어떤 효과를 적용할 것인지에 대한 정보가 기록된 상태이고, 최종 결과물은 컨텍스트를 통해서 렌더링 되어 생성된다. CIImage가 다룰 수 있는 비트맵은 CGImage부터,  CGContext의 비트맵데이터, NSBitmapImageRep, (iOS 한정으로) UIImage 등이며, URL로 부터 이미지 파일을 읽어서 생성할 수도 있고, Core Video 이미지 버퍼를 사용할 수도 있다.

CIImagecgImage(CGImage?타입) 프로퍼티가 있어서 CGImage로 변환할 수 있다. 이 때 이 값은 실제로 컨텍스트에 의해서 렌더링된 결과를 가지고 있는 상태일 때만 유효하다.

예제

다음은 특정한 URL상의 이미지 파일에 대해 세피아 톤 필터를 적용하는 예이다.

import Cocoa
import CoreImage


let fileURL = ...
let image = CIImage(contentsOf: fileURL)!
let ctx = CIContext()


let filter = CIFilter(name: "CISepiaTone")! // #1

// #2
filter.setValue(image, forKey: kCIInputImageKey)
filter.setValue(0.8, forKey: kCIInputIntensityKey)

let result = filter.outputImage // #3

if let result = result {
  // #4
  let cgImage: CGImage? = ctx.createCGImage(result, from: result.extent)
  // do something with resultImage
}
  1. 세피아 톤을 적용하기 위한 필터를 생성한다. 각각의 필터 이름은 문자열로 기입하며, 아직 상수로 정의된 바는 없다. 사용가능한 필터의 목록은 코어 이미지 필터 레퍼런스를 참고한다.
  2. 필터의 적용에 필요한 입력 이미지와 입력 값들을 키-값 기반으로 적용해준다.
  3. 이 때의 결과물은 “최종 이미지를 렌더링 하기 위한 레시피”임에 주의하자. 즉 아직까지 최종 렌더링은 이루어지지 않았다.
  4. 레시피를 렌더링하여 실제 비트맵을 생성하는 것은 컨텍스트의 몫이다.  createCGImage(_:from:)가 호출되는 시점에 만들어질 것이다.

UI와 함께 쓰기

코어 이미지가 제공하는 주된 API들에서 입출력되는 이미지는 늘 CIImage 타입을 사용한다. 하지만 UIKit이나 AppKit에서 사용되는 대표적인 이미지 표현 클래스는 UIImage, CGImage, NSImage 들이다. 이 들과 CIImage 간의 관계에 대해서 살펴보자.

iOS – UIImage

iOS의 UIImage는 이미지 비트맵 데이터를 가지고 있는 형식이며, 따라서 CGImageCIImage와는 간단하게 상호 변환이 가능하다.

// Swift 4.0

let image = UIImange(named: "myimage.png")
// CGImage 얻기
let myCGImage = image.cgImage!
// CGImage로부터 CIImage를 만들 수 있다. 
let someCIImage = CIImage(cgImage: myCGImage)

// CIImage는 UIImage로부터 바로 변환된다. 
let myCIImage = CIImage(image: image)!
// 반대로 UIImage 역시 CIImage로부터 바로 얻을 수 있다. 
let displayedImage = UIImage(ciImage: myCIImage)

따라서 필터링의 결과물인 CIImage를 화면에 디스플레이하기 위해서는 init(ciImage:)를 사용하여 CIImageUIImage로 변환하여 사용할 수 있다.

macOS – NSImage

AppKit의 NSImage는 데이터 상으로는 비트맵 이미지를 나타내는 클래스가 아니다. 결국 네이밍의 문제이긴한데, 어떤 콘텐츠는 “어디로 출력되냐”에 따라서 사실 다른 데이터로 이루어져야 맞는데, (저해상도 사진 파일을 프린터로 출력하면 깨져서 나오는 것 처럼) NSImage는 특정한 출력의 맥락에서 최선의 결과물을 자동으로 골라주기 위한 장치이다. 따라서 NSImage는 그 스스로는 비트맵 데이터를 관리하지 않으며, 그 내부에 여러 개의 표현형 데이터를 가지고 있는 배열 비슷한 클래스이다.

따라서 macOS 앱을 만들 때 코어 이미지를 쓴다면 이미지의 입출력은 모두 CIImage 기반으로 하되,  NSImageView를 사용해서 이미지를 디스플레이해야 하는 시점에 NSImage를 생성하는 전략을 선택하는 것이 바람직하겠다.

CIImage로부터 NSImage를 생성하기 위해서는 NSImageRep의 서브 클래스인 CIImageRep 클래스를 사용한다. 공교롭게도 이 클래스는 코어이미지가 아니라 AppKit에 정의되어 있기 때문에 코어 이미지 레퍼런스만 찾아서는 알 수가 없었다.

// CIImage를 NSImageViwe에 사용하기 

// 먼저 CIImage -> CIImageRep을 생성
let resultImage = someCIFilter.outputImage!
let rep = CIImageRep(ciImage: resultImage)

// 빈 NSImage를 생성하고, representation을 추가한다.
let displayedImage = NSImage()
displayedImage.addRepresentataion(rep)
self.imageView.image = displayedImage

CIImage를 만들기

사실 CIImage는 비트맵데이터, CGImage, NSBitmapImageRep 뿐만 아니라 코어 비디오 프레임(CVImageBuffer)이나 이미지 파일로부터도 생성이 가능하다. 또한 CIImage를 다양한 파일 포맷의 비트맵 데이터로 변환할 수도 있는데, 이는 CIContext 객체의 기능을 통해서 구현이 가능하다.

즉 AppKit에서는 NSImage를 사용하지 않고도 얼마든지 파일로부터 CIImage를 읽어들여서 조작하고 저장하는 작업을 수행할 수 있다. 다음은 파일의 경로 문자열로부터 이미지 파일을 읽어서 필터를 적용하고, 다시 이를 저장하는 과정을 구현한 것이다.

// 경로 준비
let filePath = "~/images/sample01.png"
let fileURL = URL(fileURLWithPath: (filePath as NSString).expandingTildeInPath)!

let saveURL = ... 

// 컨텍스트
let context = CIContext() // #1

// 필터를 적용한 이미지
if let image = CIImage(contentsOf: fileURL) {
  let myFilter = CIFilter(name: "CISepiaTone", withInputParameters:
    [kCIInputImageKey: image,
     kCIInputIntensityKey: 0.8])
  let result = myFilter.outputImage!

  // 결과 이미지를 디스크에 기록한다.
  do {
    try writePNGRepresentation(of: result, to: saveURL, format: kCIFormatARGB8,
                               colorSpace: CGColorSpaceCreateDeviceRGB(),
                               options:[:])
  catch {
    fatalError("Fail to write PNG file")
  }
}

그외 알아둘 것

CIFilter는 mutable한 객체이기 때문에 여러 스레드에서 동시에 참조하는 것은 안전하지 않다. 같은 기능을 하는 필터를 여러 스레드에서 사용한다면 각각의 스레드마다 필터를 생성하도록 하자. 대신에 필터를 통해서 거쳐 나오는 CIImage는 변경불가능한 객체이기 때문에 여러 스레드 사이에서 동일 인스턴스를 참조하는 것에 별다른 문제가 생기지 않는다.

CIContext 역시 불변 객체이기에 스레드 안전하다. 또, CIContext는 생성, 유지하는데 비용이 크게 들어가는 객체이기 때문에 가급적 하나만 생성하여 사용할 것이 권장된다.

정리

  • CIContext는 원본 이미지와 필터 레시피를 가지고 효과를 적용한 결과물을 렌더링해내는 포토샵 같은 존재이다.
  • CIFilter는 개별 필터에 대한 세세한 설정 정보를 기록한다.
  • CIImage는 원본 이미지로부터 생성되어, 필터들을 거치며 각각의 필터에 대한 레시피 기록이 부가되는 이미지이다.
  • 포토샵에서 필터를 적용하듯, CIImage 자체에 필터를 적용한 결과물을 얻기 위해서는 필터의 출력 이미지를 다시 컨텍스트를 통해 렌더링해야 한다.

참고자료