Image Filters
์์ดํฐ ์นด๋ฉ๋ผ์์ DSLR์ฒ๋ผ ๋ ์ฆ๋ฅผ ๊ฒน์ณ ์ธ ์ ์๋ค๊ตฌ์?
๊ฐ์
Core Image๋ Apple์ GPU ๊ฐ์ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ํ๋ ์์ํฌ์
๋๋ค. 200๊ฐ ์ด์์ ๋ด์ฅ ํํฐ(CIFilter)๋ฅผ ์ฒด์ธ์ผ๋ก ์ฐ๊ฒฐํด ์ฌ์ง ๋ณด์ ํ์ดํ๋ผ์ธ์ ๊ตฌ์ฑํ๊ฑฐ๋, AVFoundation์ ์นด๋ฉ๋ผ ํผ๋์ ์ค์๊ฐ์ผ๋ก ์ ์ฉํ ์ ์์ต๋๋ค.
ํต์ฌ ์ถ์ํ ๋ ๊ฐ:
CIImageโ ํฝ์ ๋ฐ์ดํฐ๊ฐ ์๋ โ์ด๋ฏธ์ง๋ฅผ ๋ง๋๋ ๋ ์ํผโ. ํํฐ๋ฅผ ์ฐ๊ฒฐํด๋ ์ฆ์ ์ฒ๋ฆฌํ์ง ์์.CIContextโ GPU ๋ฆฌ์์ค๋ฅผ ๊ด๋ฆฌํ๋ ๋ฌด๊ฑฐ์ด ๊ฐ์ฒด. ์ฑ ์๋ช ๋์ 1๊ฐ๋ง ์์ฑยท์ฌ์ฌ์ฉํ๋ ๊ฒ ์ ์.
// 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๊ฐ ๋ฐ๋ชจ๋ ์ ํจํด์ ๊ฐค๋ฌ๋ฆฌ โ ์นด๋ฉ๋ผ ์ค์๊ฐ โ ์ฒด์ธ โ ํฉ์ฑ ์ดํํธ ์์๋ก ํ์ฅํด ๊ฐ๋๋ค.
1. Filter Gallery
๋ฌด์์ ๋ฐฐ์ฐ๋ โ ๋จ์ผ
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)) }
}
}
}
ํต์ฌ ํฌ์ธํธ:
AVCaptureVideoDataOutputSampleBufferDelegate์ ์ฝ๋ฐฑ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ํ์์ ํธ์ถ โ UI ๊ฐฑ์ ์ main์ผ๋ก dispatch.CIContext๋ ๋ฐ๋์ ์ฌ์ฌ์ฉ. ๋งค ํ๋ ์ ์์ฑํ๋ฉด GPU ๋ฆฌ์์ค ํญ๋ฐ.- ์๋ฎฌ๋ ์ดํฐ์๋ ์นด๋ฉ๋ผ๊ฐ ์์ผ๋ฏ๋ก, ๋ฐ๋ชจ์์๋ ์ ์ฐจ์ ์ผ๋ก ๋ง๋ ์ ๋๋ฉ์ด์ ์ด๋ฏธ์ง๋ฅผ ์ ๋ ฅ์ผ๋ก ํด๋ฐฑํฉ๋๋ค.
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
CIContext๋ ์ฑ๋น 1๊ฐ. ์ ์์ ๋ ๋ง๋ค๊ณ , ๋งค ํ๋ ์ ๋ ๋ง๋ค๋ฉด ์ฆ์ ์ฑ๋ฅ ํญ๋ฝ.- ์นด๋ฉ๋ผ ํ์ดํ๋ผ์ธ์์๋
CIContext(options: [.useSoftwareRenderer: false])๋ก GPU ๊ฐ์ . - Swift ํ์
์์ API(
CIFilter.sepiaTone())๋ฅผ ์ฐ๋ฉด ํค ์ด๋ฆ ์คํ๋ก ์ธํ ๋ฐํ์ ์คํจ๊ฐ ์์ด์ง๋๋ค. - ํํฐ ์ฒด์ธ ์์๋ ๊ฒฐ๊ณผ๋ฅผ ๊ฒฐ์ ํ๋ ๋ณ์. ๊ฐ์ ๋ ํํฐ๋ผ๋ ์์๊ฐ ๋ฐ๋๋ฉด ๋ค๋ฅธ ๊ฒฐ๊ณผ.
์ฃผ์ ์ฌํญ
CIImage๋ ํฝ์ ์ด ์๋ ๋ ์ํผ์ ๋๋ค. ์ค์ ํฝ์ ์ด ํ์ํ๋ฉด ๋ฐ๋์CIContext.createCGImage(_:from:)๋ก ๋ ๋๋ง.- ์นด๋ฉ๋ผ ํ๋ ์์ ๋ฐฑ๊ทธ๋ผ์ด๋ ํ โ UI ๊ฐฑ์ ์ ๋ฐ๋์ main dispatch.
- ํํฐ๋ฅผ ๊ณผ๋ํ๊ฒ ์ฒด์ด๋ํ๋ฉด GPU ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ๋น ๋ฅด๊ฒ ์ฆ๊ฐ. ๊ฐ๋ฅํ๋ฉด ์ค๊ฐ ๊ฒฐ๊ณผ๋ฅผ ํฉ์ณ ํ ๋ฒ์ ์ฒ๋ฆฌ.
CIKernel์์ฑ ์ Metal Shading Language์ ์ขํ๊ณ์ Core Image์ ์ ๊ทํ ์ขํ ์ฐจ์ด์ ์ฃผ์.