๊ฐœ์š”

Core Image๋Š” Apple์˜ GPU ๊ฐ€์† ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. 200๊ฐœ ์ด์ƒ์˜ ๋‚ด์žฅ ํ•„ํ„ฐ(CIFilter)๋ฅผ ์ฒด์ธ์œผ๋กœ ์—ฐ๊ฒฐํ•ด ์‚ฌ์ง„ ๋ณด์ • ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์„ฑํ•˜๊ฑฐ๋‚˜, AVFoundation์˜ ์นด๋ฉ”๋ผ ํ”ผ๋“œ์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์ถ”์ƒํ™” ๋‘ ๊ฐœ:

// 1) ์ž…๋ ฅ โ†’ 2) ํ•„ํ„ฐ โ†’ 3) ์ถœ๋ ฅ โ†’ 4) ๋ Œ๋”
let input  = CIImage(image: photo)!
let blur   = CIFilter.gaussianBlur()
blur.inputImage = input
blur.radius = 8

let context = CIContext()                                // ํ•œ ๋ฒˆ๋งŒ ๋งŒ๋“ค์–ด ์žฌ์‚ฌ์šฉ
let cgImage = context.createCGImage(blur.outputImage!, from: input.extent)!
let result  = UIImage(cgImage: cgImage)

์ด ๋ชจ๋“ˆ์˜ 4๊ฐœ ๋ฐ๋ชจ๋Š” ์œ„ ํŒจํ„ด์„ ๊ฐค๋Ÿฌ๋ฆฌ โ†’ ์นด๋ฉ”๋ผ ์‹ค์‹œ๊ฐ„ โ†’ ์ฒด์ธ โ†’ ํ•ฉ์„ฑ ์ดํŽ™ํŠธ ์ˆœ์„œ๋กœ ํ™•์žฅํ•ด ๊ฐ‘๋‹ˆ๋‹ค.


๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” ๋‹จ์ผ CIFilter๋ฅผ ์ž…๋ ฅ ์ด๋ฏธ์ง€์— ์ ์šฉํ•˜๊ณ , ํŒŒ๋ผ๋ฏธํ„ฐ(์˜ˆ: ๊ฐ•๋„)๋ฅผ ์Šฌ๋ผ์ด๋”๋กœ ์‹ค์‹œ๊ฐ„ ์กฐ์ ˆํ•˜๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํŒจํ„ด.

@State var intensity: Double = 0.8
let context = CIContext()                       // ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑ

func apply(_ filterName: String, to input: CIImage) -> UIImage? {
    guard let filter = CIFilter(name: filterName) else { return nil }
    filter.setValue(input, forKey: kCIInputImageKey)
    filter.setValue(intensity, forKey: kCIInputIntensityKey)

    guard let output = filter.outputImage,
          let cg = context.createCGImage(output, from: input.extent)
    else { return nil }
    return UIImage(cgImage: cg)
}

๋ฐ๋ชจ๋Š” Sepia, Chrome, Noir, Bloom, Crystallize ๋“ฑ 15๊ฐœ ์ธ๋„ค์ผ์„ ๊ทธ๋ฆฌ๋“œ๋กœ ๋ณด์—ฌ์ฃผ๊ณ , ํƒญํ•˜๋ฉด ํฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + ๊ฐ•๋„ ์Šฌ๋ผ์ด๋”๊ฐ€ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ํƒ€์ž… ์•ˆ์ „ API ๊ถŒ์žฅ: CIFilter(name: "CISepiaTone") ๊ฐ™์€ ๋ฌธ์ž์—ด ๊ธฐ๋ฐ˜ ๋Œ€์‹  CIFilter.sepiaTone()์„ ์“ฐ๋ฉด ์ปดํŒŒ์ผ ํƒ€์ž„์— ๊ฒ€์ฆ๋ฉ๋‹ˆ๋‹ค.


2. Camera Filters

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” AVCaptureSession์œผ๋กœ ์นด๋ฉ”๋ผ ํ”„๋ ˆ์ž„์„ ๋ฐ›์•„ ์‹ค์‹œ๊ฐ„์œผ๋กœ Core Image ํ•„ํ„ฐ๋ฅผ ์ ์šฉํ•˜๋Š” ํŒŒ์ดํ”„๋ผ์ธ.

final class CameraPipeline: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
    let session  = AVCaptureSession()
    let context  = CIContext()
    var filter:  CIFilter?
    var onFrame: (UIImage) -> Void = { _ in }

    func captureOutput(_ output: AVCaptureOutput,
                       didOutput sampleBuffer: CMSampleBuffer,
                       from connection: AVCaptureConnection) {
        guard let pixel = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        var image = CIImage(cvPixelBuffer: pixel)

        if let filter = filter {
            filter.setValue(image, forKey: kCIInputImageKey)
            if let out = filter.outputImage { image = out }
        }
        if let cg = context.createCGImage(image, from: image.extent) {
            DispatchQueue.main.async { self.onFrame(UIImage(cgImage: cg)) }
        }
    }
}

ํ•ต์‹ฌ ํฌ์ธํŠธ:


3. Filter Chain Builder

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” outputImage โ†’ inputImage๋กœ ์—ฌ๋Ÿฌ ํ•„ํ„ฐ๋ฅผ ์ง๋ ฌ ์—ฐ๊ฒฐํ•˜๋Š” ํŒจํ„ด, ๊ทธ๋ฆฌ๊ณ  ์ˆœ์„œ๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”๊พธ๋Š” ์‚ฌ์‹ค์˜ ์‹œ๊ฐํ™”.

struct FilterSlot: Identifiable {
    let id = UUID()
    var filterName: String
    var intensity: Double
}

@State var slots: [FilterSlot] = []

func processChain(input: CIImage) -> CIImage {
    var current = input
    for slot in slots {
        guard let filter = CIFilter(name: slot.filterName) else { continue }
        filter.setValue(current, forKey: kCIInputImageKey)
        filter.setValue(slot.intensity, forKey: kCIInputIntensityKey)
        if let out = filter.outputImage { current = out }
    }
    return current
}

๊ฐ™์€ ๋‘ ํ•„ํ„ฐ๋ผ๋„:

๋ฐ๋ชจ์—์„œ๋Š” ์Šฌ๋กฏ์„ ์ตœ๋Œ€ 5๊ฐœ๊นŒ์ง€ ์Œ“๊ณ , ์Šฌ๋กฏ ์ˆœ์„œ๋ฅผ ๋“œ๋ž˜๊ทธ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


4. Custom Effects

๋ฌด์—‡์„ ๋ฐฐ์šฐ๋‚˜ โ€” ์—ฌ๋Ÿฌ ํ•„ํ„ฐ๋ฅผ ์กฐํ•ฉํ•ด ์™„์„ฑ๋œ ํ•œ ์žฅ์˜ ๋ฃฉ(look)์„ ๋งŒ๋“ค๊ณ , before/after ์Šฌ๋ผ์ด๋”๋กœ ๋น„๊ตํ•˜๋Š” ํŒจํ„ด.

๋ฐ๋ชจ๋Š” 4๊ฐ€์ง€ ํ”„๋ฆฌ์…‹(๊ธ€๋ฆฌ์น˜, ๋นˆํ‹ฐ์ง€, ํŒ์•„ํŠธ, ๋„ค์˜จ ๊ธ€๋กœ์šฐ)์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํ”„๋ฆฌ์…‹์€ ๋‹จ์ผ ํ•„ํ„ฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์—ฌ๋Ÿฌ ํ•„ํ„ฐ์˜ ํ•ฉ์„ฑ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

// ๋นˆํ‹ฐ์ง€ ํ”„๋ฆฌ์…‹ ์˜ˆ์‹œ
func vintage(_ input: CIImage) -> CIImage? {
    let sepia = CIFilter.sepiaTone()
    sepia.inputImage = input
    sepia.intensity  = 0.8

    let vignette = CIFilter.vignette()
    vignette.inputImage = sepia.outputImage
    vignette.intensity  = 1.0
    vignette.radius     = 2.0

    let noise = CIFilter.colorMonochrome()
    // ... ์ถ”๊ฐ€ ํ•ฉ์„ฑ

    return vignette.outputImage
}

before/after ์Šฌ๋ผ์ด๋”๋Š” ๋™์ผ ์œ„์น˜์— ์›๋ณธ๊ณผ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๊ฒน์ณ ๋‘๊ณ , ์Šฌ๋ผ์ด๋”์˜ X ์ขŒํ‘œ ๊ธฐ์ค€์œผ๋กœ ํด๋ฆฌํ•‘(mask)์„ ์ž˜๋ผ ๋ฐ˜๋ฐ˜ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

๐Ÿ’ก CIKernel๋กœ ํ•œ ๋‹จ๊ณ„ ๋”: ๋‚ด์žฅ ํ•„ํ„ฐ ์กฐํ•ฉ์œผ๋กœ๋„ ๋ถ€์กฑํ•˜๋ฉด Metal Shading Language๋กœ ์ง์ ‘ CIKernel์„ ์ž‘์„ฑํ•ด ํ”ฝ์…€๋ณ„ ์ปค์Šคํ…€ ์—ฐ์‚ฐ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์‹ค์ „ ํŒ

Best Practices

์ฃผ์˜ ์‚ฌํ•ญ