如何在Swift中通过CIFilter实现多通道曲线控制?
实现全通道自定义曲线调整的CoreImage方案
问题分析
CIToneCurve最多支持5个控制点,无法满足复杂曲线需求- 用
CIColorMatrix拆分通道后用composited(over)合成是错误逻辑:单通道灰度图的合成不是通道重组,应该将三个通道的灰度值重新组合为RGB颜色,这是之前出现瑕疵的核心原因 CIColorCurve缺乏官方文档,参数控制不灵活
最佳方案:自定义CIKernel
直接通过自定义CIKernel对每个像素的RGB通道值分别应用对应曲线,无需拆分通道,避免合成瑕疵,同时支持任意数量的控制点。
1. 编写CIKernel代码
我们需要一个Kernel,接收原始图像、四个曲线的查找表(复合通道、红、绿、蓝),对每个像素的颜色值进行曲线映射:
kernel vec4 customCurve(sampler image, array<float, 256> compositeCurve, array<float, 256> redCurve, array<float, 256> greenCurve, array<float, 256> blueCurve) { vec4 color = sample(image, samplerCoord(image)); // 先对RGB应用复合通道曲线 float rComposite = compositeCurve[int(color.r * 255.0)]; float gComposite = compositeCurve[int(color.g * 255.0)]; float bComposite = compositeCurve[int(color.b * 255.0)]; // 再分别应用各通道独立曲线 float rFinal = redCurve[int(rComposite * 255.0)]; float gFinal = greenCurve[int(gComposite * 255.0)]; float bFinal = blueCurve[int(bComposite * 255.0)]; return vec4(rFinal, gFinal, bFinal, color.a); }
预先生成256阶的曲线查找表,避免在Kernel中实时插值,大幅提升性能;若需要更高精度,可调整为1024阶。
2. Swift端实现曲线查找表生成
将用户提供的曲线点数组(输入值→输出值)转换为256阶查找表,用线性插值填充中间值:
private func generateCurveLookupTable(points: [(Double, Double)]) -> [Float] { let sortedPoints = points.sorted { $0.0 < $1.0 } var lookupTable = [Float](repeating: 0, count: 256) for i in 0..<256 { let inputValue = Double(i) / 255.0 var found = false for j in 0..<sortedPoints.count-1 { let p1 = sortedPoints[j] let p2 = sortedPoints[j+1] guard inputValue >= p1.0 && inputValue <= p2.0 else { continue } let t = (inputValue - p1.0) / (p2.0 - p1.0) let outputValue = p1.1 + t * (p2.1 - p1.1) lookupTable[i] = Float(max(0.0, min(1.0, outputValue))) found = true break } if !found { if let last = sortedPoints.last, inputValue >= last.0 { lookupTable[i] = Float(max(0.0, min(1.0, last.1))) } else if let first = sortedPoints.first, inputValue <= first.0 { lookupTable[i] = Float(max(0.0, min(1.0, first.1))) } else { lookupTable[i] = Float(inputValue) // 默认线性映射 } } } return lookupTable }
3. 完整的Curve调整结构体实现
整合Kernel和查找表生成,实现全通道控制:
import UIKit import CoreImage struct CurveAdjustment { private let customCurveKernel: CIKernel // 通道ID: 0-复合通道, 1-红通道, 2-绿通道, 3-蓝通道 var channelCurves: [Int : [(Double, Double)]] = [:] var inputImage: CIImage? init() { let kernelString = """ kernel vec4 customCurve(sampler image, array<float, 256> compositeCurve, array<float, 256> redCurve, array<float, 256> greenCurve, array<float, 256> blueCurve) { vec4 color = sample(image, samplerCoord(image)); float rComposite = compositeCurve[int(color.r * 255.0)]; float gComposite = compositeCurve[int(color.g * 255.0)]; float bComposite = compositeCurve[int(color.b * 255.0)]; float rFinal = redCurve[int(rComposite * 255.0)]; float gFinal = greenCurve[int(gComposite * 255.0)]; float bFinal = blueCurve[int(bComposite * 255.0)]; return vec4(rFinal, gFinal, bFinal, color.a); } """ guard let kernel = CIKernel(source: kernelString) else { fatalError("Failed to create custom curve kernel") } self.customCurveKernel = kernel } var outputImage: CIImage? { guard let input = inputImage else { return nil } // 默认线性曲线(输入=输出) let defaultPoints = [(0.0, 0.0), (1.0, 1.0)] let compositeLookup = generateCurveLookupTable(points: channelCurves[0] ?? defaultPoints) let redLookup = generateCurveLookupTable(points: channelCurves[1] ?? defaultPoints) let greenLookup = generateCurveLookupTable(points: channelCurves[2] ?? defaultPoints) let blueLookup = generateCurveLookupTable(points: channelCurves[3] ?? defaultPoints) // 应用自定义Kernel return customCurveKernel.apply( extent: input.extent, roiCallback: { _, rect in rect }, arguments: [input, compositeLookup, redLookup, greenLookup, blueLookup] ) } private func generateCurveLookupTable(points: [(Double, Double)]) -> [Float] { let sortedPoints = points.sorted { $0.0 < $1.0 } var lookupTable = [Float](repeating: 0, count: 256) for i in 0..<256 { let inputValue = Double(i) / 255.0 var found = false for j in 0..<sortedPoints.count-1 { let p1 = sortedPoints[j] let p2 = sortedPoints[j+1] guard inputValue >= p1.0 && inputValue <= p2.0 else { continue } let t = (inputValue - p1.0) / (p2.0 - p1.0) let outputValue = p1.1 + t * (p2.1 - p1.1) lookupTable[i] = Float(max(0.0, min(1.0, outputValue))) found = true break } if !found { if let last = sortedPoints.last, inputValue >= last.0 { lookupTable[i] = Float(max(0.0, min(1.0, last.1))) } else if let first = sortedPoints.first, inputValue <= first.0 { lookupTable[i] = Float(max(0.0, min(1.0, first.1))) } else { lookupTable[i] = Float(inputValue) } } } return lookupTable } }
4. 使用示例
// 创建曲线调整实例 let curveAdjustment = CurveAdjustment() // 设置各通道曲线点 // 复合通道:提亮中间调 curveAdjustment.channelCurves[0] = [(0.0, 0.0), (0.5, 0.6), (1.0, 1.0)] // 红通道:加深暗部 curveAdjustment.channelCurves[1] = [(0.0, 0.1), (1.0, 1.0)] // 绿通道:降低高光 curveAdjustment.channelCurves[2] = [(0.0, 0.0), (0.8, 0.8), (1.0, 0.7)] // 蓝通道:保持线性默认 curveAdjustment.channelCurves[3] = [(0.0, 0.0), (1.0, 1.0)] // 输入图像 guard let inputImage = UIImage(named: "testImage")?.ciImage else { return } curveAdjustment.inputImage = inputImage // 获取输出图像并转换为UIImage if let outputImage = curveAdjustment.outputImage { let context = CIContext(options: [.useSoftwareRenderer: false]) if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { let resultImage = UIImage(cgImage: cgImage) // 显示或保存resultImage } }
关键说明
- 复合通道逻辑与Photoshop一致:先对RGB三个通道应用复合曲线,再分别应用各通道独立曲线
- 若需要平滑曲线(类似Photoshop的贝塞尔曲线),可修改
generateCurveLookupTable方法,用Catmull-Rom样条或贝塞尔曲线插值替代线性插值
内容的提问来源于stack exchange,提问作者Ihor M.
相关产品推荐
相关产品推荐

