[swift2] Core Image Filter 예제

Swift 2015. 11. 10. 23:42

절차지향 또는 객체지향 프로그래밍에 익숙한 상태에서, 함수형 프로그래밍에 적응하기란 쉽지 않습니다. 다른 프로그래밍 방법론을 공부했을 때처럼 많은 함수형 프로그램 코드를 읽고 이해하고, 또 많은 예제를 실제로 작성해봐야만 숙달될 수 있습니다.

swift 함수형 프로그램의 예로 Core Image 필터 예제를 소개합니다. 이 예제는 objc.io 에서 출판된 Funtional Programming in Swift 에서 발췌하였습니다.

출처: Funtional Programming in Swift

  • 이 예제에서는 MaxOSX Core Image 프레임워크의 함수형 wrapper API 작성을 목표로 합니다.
  • 또한 고차원함수(high-order function)와 같은 함수형 프로그래밍의 실질적인 활용예를 보는 것입니다.


Core Image

  • Core Image는 강력한 이미지 프로세싱 프레임워크지만 사용하기가 불편합니다.
  • Core Image API는 키밸류 코딩을 사용해서 설정할 수 있는 느슨하게 타이핑된 API 입니다.
  • 파라미터 이름을 입력할 때 실수할 수 있는 여지가 많아 런타임 에러를 발생시킬 수 있습니다.
  • 이번에 개발할 API 는 안전하고, 모듈화되었으며, (오타로 인한)런타임 에러가 발생하지 않도록 타입을 장점을 활용합니다.


Filter Type

  • Core Image의 핵심클래스는 이미지 필터를 생성하는데 사용하는 CIFilter 클래스입니다.
  • CIFilter 객체 생성시 반드시 kCIInputImageKey 키에 해당하는 input image를 전달해야합니다.
  • kCIOutputImageKey 키를 사용해서 필터가 적용된 결과 이미지를 얻어올 수 있습니다.
  • 또한 이 결과 이미지는 다른 필터의 입력으로 사용할 수 있습니다.

CIImage 타입을 파라미터로 받아 CIImage 타입의 결과 이미지를 반환하는 함수타입 Filter를 정의합니다.

typealias Filter = CIImage -> CIImage


Blur 필터 함수

  • 이미지에 블러를 적용하는 blur 함수를 정의해보겠습니다.
  • 이 블러필터는 한개의 blur radius 파라미터를 받습니다.
func blur(radius:Double) -> Filter {
    return { image in
        let parameters = [
            kCIInputRadiusKey : radius,
            kCIInputImageKey  : image
        ]
        let filter = CIFilter(name: "CIGaussianBlur", withInputParameters: parameters)
        return filter!.outputImage!
    }
}
  • blur 함수는 CIImage 타입의 이미지를 인자로 받고, 결과 이미지(filter!.outputImage)를 리턴하는 Filter 함수를 반환합니다.


Color Overlay 필터 함수

  • 이미지 위에 특정 컬러 레이어로 덮는 오버레이 필터를 정의해보겠습니다.
  • CoreImage 에는 오버레이 필터가 없지만 이미 있는 필터들을 조합하여 만들것입니다.
  • 컬러를 생성하는 color generator 필터와 레이어를 얹는 source-over compositing 필터를 사용하겠습니다.


Color 레이어 생성필터

func colorGenerator(color:NSColor) -> Filter {
    return { image in
        let parameters = [kCIInputColorKey:CIColor(color: color)!]
        let filter = CIFilter(name: "CIConstantColorGenerator", withInputParameters: parameters)
        return filter!.outputImage!.imageByCroppingToRect(image.extent)
    }
}   
  • blur 필터와 유사하지만 input 이미지가 필요하지 않다는 차이가 있습니다.
  • 생성된 컬러 레이어 이미지를 특정크기(인자로 받은 이미지의 크기)로 잘라주어야 합니다.
  • 다음으로 두 이미지를 복합하는 필터를 정의합니다.


레이어 오버필터

func compositeSourceOver(overlay:CIImage) -> Filter {
    return { image in
        let parameters = [
            kCIInputBackgroundImageKey: image,
            kCIInputImageKey: overlay
        ]
        let filter = CIFilter(name: "CISourceOverCompositing", withInputParameters: parameters)
        return filter!.outputImage!.imageByCroppingToRect(image.extent)
    }
}
  • 마지막에 input 이미지의 크기만큼 결과 이미지를 자릅니다.
  • 두 필터를 결합하여 color overlay 필터를 생성합니다.


Color Overlay 필터

func colorOverlay(color:NSColor) -> Filter {
    return { image in
        let overlay = colorGenerator(color)(image)
        return compositeSourceOver(overlay)(image)
    }
}
  • colorOverlay 함수는 컬러를 인자로 받아 필터함수를 반환합니다.
  • 이 필터함수에 이미지를 전달하면, 앞서 전달한 컬러로 오버레이된 이미지를 반환합니다.


Filter 복합하기

  • blur 필터와 color overlay 필터를 복합해서 사용해보겠습니다.
  • 원본 이미지 파일을 읽습니다.
let path = NSBundle.mainBundle().pathForResource("16.jpg", ofType: nil)!
let image = CIImage(contentsOfURL: NSURL(fileURLWithPath: path))!



  • blur 필터를 적용합니다.
let blurRadius = 5.0
let blurredImage = blur(blurRadius)(image)



  • 빨간색 레이어를 오버레이합니다.
let overlayColor = NSColor.redColor().colorWithAlphaComponent(0.2)
let overlaidImage =  colorOverlay(overlayColor)(blurredImage)



Function Composition

  • 위 예제처럼 두 필터함수를 한문장의 표현식으로 표현할 수 있습니다.
let result = colorOverlay(overlayColor)(blur(blurRadius)(image))
  • 하지만 여러 괄호들로 인해 조합된 필터의 수가 많아질 수록 가독성이 나타집니다.
  • 더 좋은 방법은 필터 조합을 위한 커스텀 연산자를 정의하는 것입니다.
  • 먼저 필터를 조합하는 함수를 정의해보겠습니다.
func composeFilters(filter1 filter1:Filter, filter2:Filter) -> Filter {
    return { image in filter2(filter1(image)) }
}
  • 위 함수는 두개의 필터를 인자로 받아 조합하여 새로운 필터를 정의합니다.
  • 반환된 필터 함수는 CIImage 타입의 image 인자를 filter1과 filter2에 순차적으로 전달합니다.
  • 아래와 같이 사용할 수 있습니다.
let myFilter1 = composeFilters(filter1: blur(blurRadius), filter2: colorOverlay(overlayColor))
let result1 = myFilter1(image)
  • 이제 필터 조합을 위한 연산자를 정의하여 좀더 가독성 있게 만들어보겠습니다.
  • 모든 곳에 자체 연산자를 정의할 필요는 없지만, 필터 조합은 이미지 처리 라이브러리에서 반복해서 발생하는 작업이기 때문에 그 용도가 적합합니다.
infix operator >>> { associativity left }

func >>> (filter1:Filter, filter2:Filter) -> Filter {
    return { img in filter2(filter1(img)) }
}
  • 이제 >>> 연산자를 이전에 정의했던 composeFilters 함수처럼 사용할 수 있습니다.
let myFilter2 = blur(blurRadius) >>> colorOverlay(overlayColor)
let result2 = myFilter2(image)
  • >>> 연산자를 left-associativity 로 정의했기 때문에, 유닉스 파이프처럼 왼쪽에서 오른쪽으로 필터가 적용됩니다.
  • 우리가 정의한 필터 조합은 Function Composition 의 한 예입니다.


전체소스

import Cocoa
import XCPlayground


// define Filter Type
typealias Filter = CIImage -> CIImage

// blur function
func blur(radius:Double) -> Filter {
    return { image in
        let parameters = [
            kCIInputRadiusKey : radius,
            kCIInputImageKey  : image
        ]
        let filter = CIFilter(name: "CIGaussianBlur", withInputParameters: parameters)
        return filter!.outputImage!
    }
}


// color layer function
func colorGenerator(color:NSColor) -> Filter {
    return { image in
        let parameters = [kCIInputColorKey:CIColor(color: color)!]
        let filter = CIFilter(name: "CIConstantColorGenerator", withInputParameters: parameters)
        return filter!.outputImage!.imageByCroppingToRect(image.extent)
    }
}

// overlay function
func compositeSourceOver(overlay:CIImage) -> Filter {
    return { image in
        let parameters = [
            kCIInputBackgroundImageKey: image,
            kCIInputImageKey: overlay
        ]
        let filter = CIFilter(name: "CISourceOverCompositing", withInputParameters: parameters)
        return filter!.outputImage!.imageByCroppingToRect(image.extent)
    }
}

// color overlay function
func colorOverlay(color:NSColor) -> Filter {
    return { image in
        let overlay = colorGenerator(color)(image)
        return compositeSourceOver(overlay)(image)
    }
}

// 원본 이미지 읽기
let path = NSBundle.mainBundle().pathForResource("16.jpg", ofType: nil)!
let image = CIImage(contentsOfURL: NSURL(fileURLWithPath: path))!

// apply blur
let blurRadius = 5.0
let blurredImage = blur(blurRadius)(image)

// apply color overlay
let overlayColor = NSColor.redColor().colorWithAlphaComponent(0.2)
let overlaidImage =  colorOverlay(overlayColor)(blurredImage)


// define composition function
func composeFilters(filter1 filter1:Filter, filter2:Filter) -> Filter {
    return { image in filter2(filter1(image)) }
}

let myFilter1 = composeFilters(filter1: blur(blurRadius), filter2: colorOverlay(overlayColor))
let result1 = myFilter1(image)


// define composition operator
infix operator >>> { associativity left }

func >>> (filter1:Filter, filter2:Filter) -> Filter {
    return { img in filter2(filter1(img)) }
}

let myFilter2 = blur(blurRadius) >>> colorOverlay(overlayColor)
let result2 = myFilter2(image)





'Swift' 카테고리의 다른 글

[swift2] 키워드로 보는 Swift  (0) 2015.11.30
[swift2] Currying 함수  (0) 2015.11.25
[swift2] Core Image Filter 예제  (0) 2015.11.10
[swift2.1] Xcode7.1 Playground 변경사항  (0) 2015.10.27
[swift2] Functor 와 Monad  (0) 2015.10.25
[swift2] 타입변환 연산자 (is, as, as?, as!)  (0) 2015.10.18

[swift] Map, Filter, Reduce

Swift 2015. 6. 22. 23:09



함수를 인자로 취하는 함수를 고차원 함수(higher-order function)라고 합니다.
Swift 표준라이브러리에서 array의 메서드로 제공하는 고차원 함수인 map, filter, reduce에 대해서 알아보겠습니다.

1. Map

    func map<U>(transform: (T) -> U) -> Array<U>

Map은 배열 각 요소 x에 변환함수 transform을 적용하고 그 결과값으로 구성된 배열을 반환하는 함수입니다.
배열요소들을 다른 값으로 맵핑하는 함수이지요.

2. Filter

func filter(includeElement: (T) -> Bool) -> Array<T>

Filter는 조건식을 인자로 받아, 조건식이 true를 만족하는 요소들로만 구성된 배열을 반환하는 함수입니다.
쉽게 말하면, 배열요소들을 필터링하는 함수입니다.

3. Reduce

func reduce<U>(initial: U, combine: @noescape (U, T) -> U) -> U

Reduce는 U를 초기값으로 하여,각 배열요소들과 순차적으로 결합연산을 하여 누적된 단일값을 반환하는 함수입니다.


4. 사용예

이제 사용법에 대해서 알아볼까요? 다음과 같은 정수형 배열이 있습니다.

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  1. 1. 정수형 배열의 각 요소에 1을 더한 배열을 얻고 싶다.
  2. 2. 정수형 배열의 각 요소에 2를 곱한 배열을 얻고 싶다.
  3. 3. 정수형 배열에서 짝수로만 구성된 배열을 얻고 싶다.
  4. 4. 정수형 배열 값들의 총합을 얻고 싶다.

map 함수를 사용하여 1,2번 문제의 답을 쉽게 얻을 수 있습니다.

// 1. 정수형 배열의 각 요소에 2를 곱한 배열을 얻고 싶다.  
arr.map { (x) -> Int in
    return x+1
}

// 2. 정수형 배열의 각 요소에 2를 곱한 배열을 얻고 싶다.
arr.map { (x) -> Int in
    return x*2
}

filter를 사용하여 3번을 해결해보겠습니다.

// 3. 정수형 배열에서 짝수로만 구성된 배열을 얻고 싶다. 
arr.filter { (x) -> Bool in
    return x%2 == 0
}  

당연히 4번은 reduce의 몫입니다.

// 4. 정수형 배열 값들의 총합을 얻고 싶다.
arr.reduce(0, combine: { (result, x) -> Int in
   return result + x
})

Trailing Closure 를 사용하여 다소 낯선 형태지만 코드가 너무 짧아 놀라셨죠?   

(Trailing Closure는 함수의 마지막 인자가 클로져(함수)일 경우, 클로져를 함수 호출부의 꼬리(trailing)에 표기하는 방식입니다. )

이러한 고차원 함수들을 복합적으로 사용하여 프로그래밍하는 방식을 함수형 프로그래밍이라고 합니다. 

함수형 프로그래밍은 짧고 명료한 코드를 작성할 수 있게하고, 버그의 가능성 또한 훨씬 줄인다고 합니다.

낯선 함수형 프로그래밍 방식에 익숙해지려면 다소 시간이 걸리겠지요? ^^


5. Map 함수 구현하기

학습한 내용을 좀더 잘 이해하기 위해 직접 map 함수를 만들어 보겠습니다.

일반적인 방법으로 1,2번 문제를 구현하는 코드를 작성했습니다.

var arr = [1, 2, 3, 4, 5]

func pluseOne(arr:[Int]) -> [Int] {
    var result:[Int] = []
    for x in arr {
        result.append(x + 1)
    }
    return result
}

func multiplyTwo(arr:[Int]) -> [Int] {
    var result:[Int] = []
    for x in arr {
        result.append(x * 2)
    }
    return result
}  

두 함수는 연산하는 행위만 제외하고 거의 비슷합니다. 연산하는 행위를 함수로 묶어내면 일반화가 가능해보이네요.

func map(arr:[Int], transform:(Int->Int)) -> [Int] {
    var result:[Int] = []
    for x in arr {
        result.append(transform(x))
    }
    return result
}  

이제 제네릭을 사용하여 범용타입으로 대치하면

func map<T, U>(arr:[T], transform:(T->U)) -> [U] {
    var result:[U] = []
    for x in arr {
        result.append(transform(x))
    }
    return result
}

map 함수가 완성되었습니다. 우리는 글로벌 함수로 구현했지만 표준라이브러리에서는 array의 메소드로 제공한다는 점이 유일한 차이입니다.


5. Filter 함수 구현하기

Map 함수를 구현해보아서 Filter 함수 구현은 좀더 간단합니다.

// 일반적인 방법으로 구현하고
func filter(arr:[Int]) -> [Int] {
    var result:[Int] = []
    for x in arr {
        if x%2 == 0 {
            result.append(x)
        }
    }
    return result
}

// 함수로 공통 행위를 묶어낸 다음
func filter(arr:[Int], includeElement:(Int->Bool)) -> [Int] {
    var result:[Int] = []
    for x in arr {
        if includeElement(x) {
            result.append(x)
        }
    }
    return result
}


// 제네릭 적용
func filter<T>(arr:[T], includeElement:(T->Bool)) -> [T] {
    var result:[T] = []
    for x in arr {
        if includeElement(x) {
            result.append(x)
        }
    }
    return result
}


6. Reduce 함수 구현하기

단계적으로 구현해보니 고차원 함수 구현이 어렵지 않죠? 이제 reduce를 구현해 볼까요?

// 일반적으로 구현하고  
func reduce(arr:[Int]) -> Int {
    var result:Int = 0
    for x in arr {
        result = result + x
    }
    return result
}

// 초기값 할당과 누적하는 행위를 파라미터로 분리하고 
func reduce(arr:[Int], initial:Int, combine:(Int,Int)->Int) -> Int {
    var result:Int = initial
    for x in arr {
        result = combine(result, x)
    }
    return result
}

// 제네릭 적용 
func reduce<T, U>(arr:[T], initial:U, combine:(U, T)->U) -> U {
    var result:U = initial
    for x in arr {
        result = combine(result, x)
    }
    return result
}  

지금까지 Swift 표준라이브러리의 Map, Filter, Reduce에 대해 알아보았습니다.




'Swift' 카테고리의 다른 글

[swift2] Xcode7 beta2 Swift 언어 변경사항  (0) 2015.06.24
[swift] 문자열 Indexing과 Slicing  (0) 2015.06.23
[swift] Map, Filter, Reduce  (0) 2015.06.22
[swift2] New in Swift2.0  (0) 2015.06.19
[swift] Apple Swift Blog  (0) 2015.06.10
[swift] Swift 기초 - 4  (0) 2014.10.29