You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 21:57:36