https://rongios.tistory.com/14
[Core Image] CIImage, CIContext, CIFilter
https://rongios.tistory.com/1 Swift의 다양한 이미지 - UIImage, CGImage, CIImageCIImage..? CGImage...?iOS 개발을 하면서 이미지를 다루다 보면 다양한 이미지 타입들을 보게 된다. UIImage, CGImage, CIImage 그리고 SwiftUI
rongios.tistory.com
앞선 포스팅에서 우린 CIImage에 CIFilter를 체이닝 방식으로 적용하는 걸 배웠다
코드는 다음과 같다
func applyFilters(to image: CIImage) -> CIImage? {
// 1️⃣ 세피아 필터 적용
let sepiaFilter = CIFilter(name: "CISepiaTone")
sepiaFilter?.setValue(image, forKey: kCIInputImageKey)
sepiaFilter?.setValue(0.8, forKey: kCIInputIntensityKey)
guard let sepiaOutput = sepiaFilter?.outputImage else { return nil }
// 2️⃣ 비네트 효과 추가
let vignetteFilter = CIFilter(name: "CIVignette")
vignetteFilter?.setValue(sepiaOutput, forKey: kCIInputImageKey)
vignetteFilter?.setValue(1.5, forKey: kCIInputIntensityKey)
vignetteFilter?.setValue(2.0, forKey: kCIInputRadiusKey)
return vignetteFilter?.outputImage // 🎨 최종 CIImage (레시피)
}
어음... 솔직히 말하면 처음 봤을 때 이게 체이닝으로 이어진거라고? 라는 생각이 들었다...
그렇게 연결됐다는 느낌이 크게 오지 않았다..
물론 지극히 나의 개인적인 생각일 뿐이다!
하지만 내가 불편함을 느끼고 있으니 적어도 개선해보려는 시도는 있어야한다고 생각해 한 번 도전해봤다!
어떤 방법을 쓰면 좀 더 명확하게 보일까 생각해봤다.
그 때 딱 마침 예전에 ViewBuilder를 포스팅하면서 공부했던 resultBuilder가 생각났다
ViewBuilder(feat. resultBuilder)
🤓 학습배경SwiftUI로 뷰를 만들다보면 ViewBuilder라는 프로퍼티 래퍼를 만나게 된다. 이 친구의 역할은 뭐고 어떻게 다양한 뷰들을 하나로 묶어주는 걸까라는 의문이 생겼다. 그래서 해당 내용에
rongios.tistory.com
어! SwiftUI의 ViewBuilder가 여러 View들을 하나의 View로 연결해서 만들어주는 것처럼 CIFilter도 resultBuilder를 이용해 FilterBuilder를 만들면 가독성 좋게 체이닝을 수행할 수 있지 않을까?
좋아 그럼 해보자!
FilterBuilder
우선 resultBuilder를 채택해서 FilterBuilder를 구현했다
@resultBuilder
struct FilterBuilder {
static func buildBlock(_ components: CIFilter...) -> CIImage? {
guard let first = components.first,
var image = first.outputImage else { return nil }
for component in components.dropFirst() {
component.setValue(image, forKey: kCIInputImageKey)
if let output = component.outputImage {
image = output
}
}
return image
}
}
동작 순서는 다음과 같다
1. 코드 블럭 내부에 있는 CIFilter 객체 중 가장 첫번째와 첫번째 필터의 output을 각각 first와 image에 언래핑하여 보관한다
2. 그 다음 CIFilter 객체부터 순회하면서 이전 image를 CIFilter에 적용해준다
3. 적용한 CIFilter의 output을 기존 image에 할당시켜준다
4. 순회가 끝나면 마지막 output이 담긴 image 변수를 반환한다
이론상 가능하다! 이제 이를 적용해서 이전 방식과 비교해보자!
먼저 기존 체이닝 방식을 보자
기존
// 기존 체이닝
func applyFilters(to image: CIImage) -> CIImage? {
// 1️⃣ 세피아 필터 적용
let sepiaFilter = CIFilter(name: "CISepiaTone")
sepiaFilter?.setValue(image, forKey: kCIInputImageKey)
guard let sepiaOutput = sepiaFilter?.outputImage else { return nil }
// 2️⃣ 비네트 효과 추가
let vignetteFilter = CIFilter(name: "CIVignette")
vignetteFilter?.setValue(sepiaOutput, forKey: kCIInputImageKey)
vignetteFilter?.setValue(1.5, forKey: kCIInputIntensityKey)
vignetteFilter?.setValue(2.0, forKey: kCIInputRadiusKey)
return vignetteFilter?.outputImage // 🎨 최종 CIImage (레시피)
}
이게 기존의 방식이다.
이전의 필터에 이미지를 집어넣고 output을 다음 필터에 전달한다.
그럼 위에서 구현한 FilterBuilder를 적용하면 어떨까?
개선 방식
func applyFilters(to image: CIImage) -> CIImage? {
@FilterBuilder
var filteredImage: CIImage? {
sepiaFilter(ciImage: image)
vignetteFilter(intensity: 1.2, radius: 3.0)
}
return filteredImage
}
// SepiaTone
private func sepiaFilter(ciImage: CIImage = CIImage()) -> CIFilter {
let filter = Filter.sepiaTone.ciFilter
filter.setValue(ciImage, forKey: kCIInputImageKey)
return filter
}
// 이하 생략...
개선 방식의 applyFilter 함수가 좀 더 깔끔해졌다!
그리고 기존에 함수 내부에서 필터를 생성하고 옵션을 설정하던 것을 메서드로 따로 분리했다!
좀더 부가적으로 설명하자면 @FilterBuilder를 채택한 계산 속성 내부에서 첫번째 필터 메서드는 원본 CIImage 타입을 파라미터로 넣어줘야한다.
두번째부터 오는 필터들은 이전 필터로부터 output을 받아오기 때문에 굳이 이미지를 직접 넣어줄 필요는 없다
개인적으로 이전 방식보다 깔끔해지고 가독성도 좋아졌다고 생각한다(뿌듯)
@FilterBuilder 동작 확인
하지만 코드만 작성해서는 의미가 없다 실제로 적용되는지 봐야지!!
해당 코드 방식을 테스트 하기 위해 다음과 같은 이미지 변환 매니저를 생성했다
final class ImageFilter {
let context = CIContext()
func applyVintageFilter(ciImage: CIImage) -> CGImage? {
@FilterBuilder
var filteredImage: CIImage? {
photoEffectTransfer(ciImage: ciImage)
noiseFilter(rect: ciImage.extent)
vignetteFilter(intensity: 1.2, radius: 3.0)
}
guard let filteredImage else { return nil }
return context.createCGImage(filteredImage, from: filteredImage.extent)
}
}
기존 이미지에 빈티지한 느낌을 주는 필터를 만들어봤다
사용한 CIFilter는 다음과 같다
1. CIPhotoEffectTransfer: 전체적으로 색상을 따뜻한 톤으로 변경해 빈티지 느낌을 줌
2. CIRandomGenerator: 노이즈를 구현하기 위해 랜덤으로 노이즈를 생성하는 필터(원본 이미지에 영향 없음)
3. CIMultiplyBlendMode: 2번에서 생성한 노이즈 이미지를 원본 이미지에 블렌딩하는 필터(2번 + 3번이 noiseFilter)
4. CIVignette: 이미지의 가장자리를 어둡게 하여 중앙을 강조하는 필터
위의 필터들을 특정 이미지에 적용하면 다음과 같이 된다!
아주 만족스럽다!
이외에도 다양한 필터가 있던데 오늘 구현한 @FilterBuilder를 이용하면 더 편하게 필터 적용을 해볼 수 있을 것 같다!
추가
CIImage의 메서드들을 살펴보다보니 다음과 같은 메서드를 찾을 수 있었다!
https://developer.apple.com/documentation/coreimage/ciimage/1437589-applyingfilter
applyingFilter(_:parameters:) | Apple Developer Documentation
Returns a new image created by applying a filter to the original image with the specified name and parameters.
developer.apple.com
오호라...CIImage에 바로 특정 필터를 적용해주고 심지어 return 값이 CIImage이기 때문에 메서드 체이닝 방식으로 적용해볼 수 있다..!
예시코드로 보면
let image = CIImage(image: UIImage(named: "example")!)!
let outputImage = image
.applyingFilter("CISepiaTone", parameters: [kCIInputIntensityKey: 0.8])
.applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: 5])
이런 식으로 CIImage에다가 바로 필터를 적용할 수 있다!
이 방식이 훨씬 내가 상단에 구현한 FilterBuilder에 비해 가독성이 좋아보인다!
하지만 공식문서에 유의할 점이 적혀있었으니..
This method, though convenient, is inefficient if used multiple times in succession. Achieve better performance by chaining filters without asking for the outputs of individual filters.
대충 정리하자면 이거 편하긴한데 연속으로 사용하면 성능 그닥이니까 성능 생각할거면 개개 필터에서 output 요청하지 말고 필터 체이닝하는 방식 써라
applyingFilter 메서드는 편리하지만, 연속해서 사용할 경우 성능이 떨어질 수 있다.
그 이유는 해당 메서드를 호출할 때마다 새로운 CIFilter 객체가 생성되고, 필터 옵션을 다시 설정하는 과정이 반복되기 때문이다.
반면, CIFilter는 mutable 객체이므로 여러 스레드에서 동시에 사용할 경우 안전하지 않지만, 단일 스레드 내에서는 재사용할 수 있다.
즉, 성능 최적화를 고려한다면 applyingFilter를 여러 번 호출하는 대신, CIFilter 객체를 미리 생성하여 재사용하는 방식이 더 효율적이다.
다만, applyingFilter는 코드의 가독성이 좋고 간결하기 때문에, 적은 수의 필터를 빠르게 적용할 경우에는 여전히 좋은 선택이 될 수 있다!
'iOS > Image' 카테고리의 다른 글
[Core Image]Tip: CIFilter 편하게 불러오기 (0) | 2025.03.24 |
---|---|
[Core Image] CIImage, CIContext, CIFilter (1) | 2025.03.24 |
[Core Graphics]CGAffineTransform 2편 - 기울기와 회전 (0) | 2025.03.20 |
[Core Graphics]CGAffineTransform 1편 - Scale, AnchorPoint (0) | 2025.03.19 |
[Swift]픽셀 데이터 다루기 3편 - 다운샘플링, 이미지 자르기 (0) | 2025.03.19 |