[Swift] 코어 이미지
Beginning Core Image in Swift
http://www.raywenderlich.com/76285/beginning-core-image-swift
코어 이미지는 이미지에 손쉽게 필터처리를 할 수 있는 강력한 프레임워크이다. 이는 어지간한 기본적인 사진 효과를 만들어낼 수 있는데, CPU 혹은 GPU 기반으로 프로세싱하고 있어서 매우 빠르다. 그냥 빠른 게 아니라 매우 빠르기 때문에 리얼타임으로 비디오 프레임에 적용될 수도 있다.
코어 이미지 필터는 여러 필터가 연쇄적으로 하나의 사진이나 비디오 프레임에 한꺼번에 적용될 수 있다. 이 때 여러 필터는 하나의 필터로 결합되어 이미지에 적용된다. 이런 방식은 한 번에 하나씩 이미지에 적용되는 것에 비해 훨씬 효율적이다.
이 글에서는 코어이미지 맛배기를 할 것이다. 몇 가지 다른 필터들을 적용해보면서 실시간으로 쿨한 이미지를 만들어보자.
시작하기
시작하기 이전에 코어 이미지 프레임 워크에서 핵심이 되는 클래스 세 가지를 짚고 넘어가겠다.
- CIContext : 모든 코어이미지 프로세싱은 CIContext 내에서 이루어진다. (일종의 상태 스택이라 보면 된다.)
- CIImage : 이 클래스는 이미지 데이터를 받는다. UIImage, 이미지파일, 이미지데이터 등으로부터 생성가능하다.
- CIFilter : 이 클래스는 필터의 종류와 각 속성을 담고 있는 일종의 사전객체이다.
프로젝트 만들기
CoreImageFun
이라는 프로젝트를 신규로 생성한다. 메인 스토리보드에서는 메인 뷰에 이미지 뷰를 하나 추가해준다. 크기 조절 모드는 Aspect Fit
를 설정하여 이미지가 찌그러지지 않도록 하자. 그리고 이 이미지 뷰를 루트 뷰 컨트롤러의 imageView
아울렛으로 설정해주자.
기본 이미지 필터링
기본적으로 간단히 이미지에대해 CIFilter를 적용하고 이를 스크린에 표시할 것이다. 이미지에 필터를 한 번 적용할 때마다 다음 네 가지 과정을 거치게 된다.
- CIImage 객체를 생성한다.
- CIContext 객체를 생성한다.
- CIFilter 객체를 생성한다.
- 필터 결과를 받는다.
자, 다음 코드는 뷰 컨트롤러의 viewDidLoad()
내에 들어갈 코드 조각이다.
// from MainViewController.swift
// in -viewDidLoad()
let fileURL = NSBundle.mainBundle().URLForResource("image", withExtension: "png")
// 1
let beginImage = CIImage(contentsOfURL: fileURL)
// 2
let filter = CIFilter(name: "CISepiaTone")
// 3
filter.setValue(beginImage, forKey: kCIInputImageKey)
filter.setValue(0.5, forKey:kCIInputIntensityKey)
// 4
let newImage = UIImage(CIImage: filter.outputImage)
self.imageView.image = newImage
1) CIImage를 만드는 가장 간단한 방법은 이미자 파일의 경로를 사용하는 것이다.
2) CIFilter는 미리 정의된 필터 이름을 사용하여 생성한다.
3) 필터에 필요한 값들을 세팅하고
4) outputImage
를 호출하면 프로세싱이 완료된 CIImage 객체가 반환되는데, 이를 UIImage
객체로 변환하면 뷰에 그릴 수 있다.
컨텍스트로
컨텍스트를 사용하는 단계로 진행하기 이전에 몇 가지 최적화와 관련된 팁. 앞서 과정에서 설명한 것 중, CIContext
를 만드는 과정이 위 예제에서는 생략되었다. 사실 이 과정은 UIImage(CIImage: )
에서 대신 처리한다. (해당 작업들은 가능한한 lazy하게 동작한다) 그래서 이 프레임워크가 매우 쓰기 쉽다는 것이다. 하지만 CIContext는 재사용이 가능해야 하고, 그를 통해서 성능을 향상 시켜야 한다.
예를 들어 슬라이드를 통해서 세피아의 강도를 조정해야 한다면, 위 코드에서는 약간의 움직임만 일어나도 새로운 CIContext가 만들어질 텐데, 손가락이 움직이면 계속해서 CIContext가 만들어지고 이는 앱을 매우 느리게 만들 것이다.
그래서 위 예제를 CIContext를 명시적으로 생성하는 방법을 통해 최적화해보자.
let fileURL = NSBundle.mainBundle().URLForResource("image", withExtension: "png")
let beginImage = CIImage(contentsOfURL: fileURL)
let filter = CIFilter(name: "CISepiaTone")
filter.setValue(beginImage, forKey: kCIInputImageKey)
filter.setValue(0.5, forKey:kCIInputIntensityKey)
/*******************************************************/
// 컨텍스트를 명시적으로 추가
let context = CIContext(options:nil)
let cgImg = context.createCGImage(filter.outputImage,
fromRect:filter.outputImage.extent())
/*******************************************************/
// let newImage = UIImage(CIImage: filter.outputImage)
let newImage = UIImage(CGImage: cgImg)
self.imageView.image = newImage
이제 CIContext 객체를 만들고, 이를 사용해서 CGImage를 그리게 하였다. CIContext의 컨스트럭터 메소드는 컬러포맷, CPU/GPU 사용 등의 정보를 담는 옵션 정보(사전)를 인자로 받지만 여기서는 디폴트 값으로도 충분하다.
컨텍스트 객체에 대해서 createCGImage(outputImage:fromRect:)
를 호출하면 이는 새로운 CIImage 인스턴스를 리턴한다. 그리고 newImage를 UIImage(CGImage:)
를 사용하여 CGImage로부터 이미지 객체를 만들도록 수정했다. 여기서 주목할 것은 CGImage 객체에 대해서 Objective-C와 달리 Swift는 별도의 릴리즈 과정이 필요없다는 점이다.
필터값 변경하기
이제 컨텍스트를 통해서 필터를 적용하는 방법을 숙지했으니 실시간으로 필터의 값을 변경해보자. 컨텍스트, 필터, 원본이미지를 viewDidLoad가 아닌 함수에서도 액세스할 수 있도록 이를 프로퍼티로 만들자. 이 프로퍼티의 초기화는 viewDidLoad에서 해도 되지만, 초기화 메소드에서 하도록 한다.
스토리보드에 저장된 모든 객체는 UIApplicationMain이 호출되면서 파싱되고 unarchiving되면서 객체 그래프가 복원된다. 이 때 init(coder:)
가 호출되기 때문에 이 부분을 오버라이드하면 된다.
class RootViewController : UIViewController {
@IBOutlet weak var imageView : UIImageView!
@IBOutlet weak var slider : UISlider!
//
var beginImage: CIImage!
var context: CIContext!
var filter: CIFilter!
var intensity: Float = 0.5
//
required init(coder aDecoder:NSCoder) {
super.init(coder:aDecoder)
context = CIContext(options: nil)
filter = CIFilter(name: "CISepiaTone")
}
//
override func viewDidLoad() {
super.viewDidLoad()
let fileURL = NSBundle.mainBundle().URLForResource("lucy", withExtension: "jpg")
beginImage = CIImage(contentsOfURL: fileURL)
self.updateImageView()
}
//
func updateImageView() {
filter.setValue(beginImage, forKey:kCIInputImageKey)
filter.setValue(intensity, forKey:kCIInputIntensityKey)
let output = context.createCGImage(filter.outputImage,
fromRect: filter.outputImage.extent())
let newImage = UIImage(CGImage: output)
self.imageView.image = newImage
}
//
@IBAction amountOfSliderDidChange(sender: AnyObject) {
intensity = sender.value
self.updateImageView()
}
}
카메라롤에서 이미지 불러오기
카메라롤에서 이미지를 불러올 때는 UIImagePickerController
를 사용한다. 이 클래스는 자체적으로 카메라롤로부터 이미지를 가져오거나 즉석에서 사진을 찍을 수 있는 뷰 컨트롤러를 포함하고 있다.
사진 선택 이후의 동작을 위해서 델리게이트가 필요하며, 추가적으로 Swift에서는 presentViewController(_:, animated:, completion:)
을 사용하여 뷰 컨트롤러 위에 뷰 컨트롤러를 올리기 위해서 UINavgationControllerDelegate프로토콜을 따라야 한다.
class RootViewController : UIViewController, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
이렇게 클래스 선언부를 수정하고, 스토리 보드에서 버튼을 하나 적당한 위치에 추가하고 IBAction을 연결한다.
@IBAction loadPhoto(sender: AnyObject) {
// Get Photo 버튼을 탭하면 이미지 피커뷰를 올린다.
let imagePickerViewController = UIImagePickerController()
imagePickerViewController.delegate = self
self.presentViewController(imagePickerViewController,
animated: true,
completion: nil)
}
이 액션은 이미지 피커 컨트롤러의 인스턴스를 생성하고, 해당 뷰 컨트롤러를 presentViewController(_:animated:completion:)
메소드를 통해서 표시하게 된다. 특이한 점은 이미지 피커 컨트롤러의 델리게이트는 imagePickerControllerDelegate
프로토콜과 UINavigationControllerDelegate
를 함께 따라야 한다. 후자의 경우 전체 메소드가 선택적으로 구현하면 되는거라 그냥 지정만 필요하다.
이미지 피커 컨트롤러의 뷰에서 사진을 선택하면 델리게이트의 imagePickerController(picker:, didFinishPickingMediaWithInfo:)
가 호출된다. 여기서 info 파라미터는 선택된 미디어에 대한 정보를 담고 있는데, 그 내용을 출력해보면
UIImagePickerControllerMediaType = "public.image";
UIImagePickerControllerOriginalImage = " size {1165, 770} orientation 0 scale 1.000000";
UIImagePickerControllerReferenceURL = "assets-library://asset/asset.PNG?id=DCFE1435-2C01-4820-9182-40A69B48EA67&ext=PNG";
미디어 타입이나, 이미지, 참조url 같은 것을 확인할 수 있다. 이중에서 UIImagePickerControllerOriginalImage
는 UIImage 타입으로 실제 이미지 객체 인스턴스이다. 따라서,
func imagePickerViewController(picker: UIImagePickerController!,
didFinishPickingMediaWithInfo info: NSDictionary!) {
self.dismissViewControllerAnimated(true, completion: nil)
beginImage = CIImage(image: info[UIImagePickerControllerOriginalImage] as UIImage)
self.updateImageView()
}
열었던 뷰 컨트롤러를 닫고, 넘어온 이미지로 CIImage를 새로 만들어서 이미지 뷰를 업데이트한다. 이 때 info의 타입은 [NSString!: AnyObject]
이기 때문에 as UIImage
로 다운캐스팅할 필요가 있다.
카메라롤에 저장하기
카메라롤에 저장하기 위해서는 AssetsLibrary
프레임워크가 필요하다. (음? 이거 그냥 되는 거 아니었나?) 암튼 그래서 프로젝트에 이 프레임워크를 추가하고, 파일 상단에 다음 문구를 추가해준다.
import AssertsLibrary
그리고 카메라롤에 저장하기 위한 버튼을 스토리보드에 만들고 다음 IBAction과 연결한다.
@IBAction func savePhoto() {
let imageToSave = filter.outputImage
let softwareContenxt = CIContext(options:[kCIContextUseSoftwareRenderer: true])
let renderedImage = softwareContenxt.createCGImage(imageToSave, fromRect: imageToSave.extent())
let library = ALAssetsLibrary()
library.writeImageToSavedPhotosAlbum(renderedImage,
metadata: renderedImage.properties(),
completionBlock:nil)
}
이 예에서는 소프트웨어 렌더러를 사용하여 파일을 렌더링하도록 했다. (그래서 iOS시뮬레이터에서는 작동하지 않는다.)
메타 정보들
이미지의 메타정보에는 사진의 위치, 찌힌 시각이나 노출, 초점 거리, 회전 여부등이 기록되어 있다. 위 예제에서는 이 정보들을 그대로 저장해준다. 만약 사진이 돌아간 경우라면 이를 회전 시켜서 화면에 표시해보도록 하자.
func imagePickerViewController(picker: UIImagePickerController!,
didFinishPickingMediaWithInfo info: NSDictionary!) {
self.dismissViewControllerAnimated(true, completion: nil)
pickedImage = info[UIImagePickerControllerOriginalImage] as UIImage
// orientation check
var orientation = pickedImage.imageOrientation
beginImage = UIImage(CGImage: pickedImage, scale: 1.0, orientation:orientation)
self.updateImageView()
}
더 많은 필터들
기본필터들은 kCICategoryBuiltIn
카테고리에 정의되어 있고 이는 +filterNamesInCategory:
메소드로 알아낼 수 있다.
다음 함수는 기본 필터들의 정보를 출력하는 유틸리티 함수이다.
func logAllFilters() {
let properties = CIFilter.filterNamesInCategory(kCICategoryBuiltIn)
println(properties)
for filterName in properties as String{
let fltr = CIFilter(name: filterName as String)
println(fltr.attributes())
}
}
필터 체이닝
필터의 outputImage의 타입은 CIImage이고, 대부분 필터의 inputImageKey의 타입 역시 CIImage 타입이므로 다음과 같은 식으로 필터를 연쇄적으로 적용하면 된다.
func chainFilters(#img: CIImage, withAmount intensity:Float) -> CIImage {
// 1
let sepia = CIFilter(name: "CISepiaTone")
sepia.setValue(img, forKey: kCIInputImageKey)
sepia.setValue(intensity, forKey: kCIInputImageKey)
// 2
let random = CIFilter(name: "CIRandomGenerator")
// 3
let lighten = CIFilter(name: "CIColorControls")
lighten.setValue(random.outputImage, forKey:kCIInputImageKey)
lighten.setValue(1 - intensity, forKey:"inputBrightness")
lighten.setValue(0, forKey: "inputSaturation")
// 4
let croppedImage = lighten.outputImage.imageByCroppingToRect(beginImage.extent())
// 5
let composite = CIFilter(name: "CIHardLightBlendMode")
composite.setValue(sepia.outputImage, forKey:kCIInputImageKey)
composite.setValue(croppedImage, forKey: kCIInputBackgroundImageKey)
// 6
let vignette = CIFilter(name: "CIVgnette")
vignette.setValue(composite.outputImage, forKey: kCIInputImageKey)
vignette.setValue(intensity * 2, forKey: "inputIntensity")
vignette.setValue(intensity * 30, forKey: "inputRadius")
//
return vignette.outputImage
}
여기서 random 필터는 랜덤 노이즈를 생성해 내는데, 결과 이미지는 동일 패턴이 끊임없이 타일링 되고 있으므로 이를 적절한 사이즈로 잘라내어야 한다. 이는 CIImage에서 imageByCroppingToRect()
메소드를 통해 지원한다.