在iOS图像开发中,将灰度图像按照亮度映射为彩色渐变,或者把画面压缩成有限的几种色调以实现色调分离,都可以借助Core Image框架完成。Core Image本身并没有一个名为“渐变映射”的独立滤镜,但我们可以通过构建颜色查找表(LUT)并配合CIColorCube滤镜,或者使用多个CIColorMatrix组合来逼近效果。理解亮度到颜色的映射关系,是写出稳定滤镜代码的前提。

渐变映射的底层原理与查找表构造
渐变映射的本质,是把输入图像的灰度值(或者亮度分量)作为索引,到一张预设的“颜色条”里取出对应的颜色。假设图像某像素的红色、绿色、蓝色分量经过灰度公式计算后得到亮度L(范围0到1),我们忽略原色,直接用L去渐变表中取色:L为0时取起始色,L为1时取结束色,中间按线性或曲线插值。这种方式能让整张图呈现出从冷到暖、从暗到亮的统一色调。
在Core Image里最规范的做法是构造一个立体颜色查找表,也就是CIColorCube所需的立方体数据。立方体边长通常取32或64,代表把0到1的区间等分成若干份。我们将每一份对应的输入RGB(这里输入RGB相等,因为是灰度)映射成目标RGB,写进浮点数组,再包装成NSData交给滤镜。这样GPU在渲染时只是一次纹理采样,性能远高于CPU遍历像素。
如果不想自己算立方体,也可以先用CIColorMonochrome把图变成纯灰度,再用CILinearGradient生成一张渐变图,最后用CIBlendWithMask把两者以亮度为遮罩混合。不过这种混合方式在色调分离时不够精确,且多色控制点多了之后链路复杂,因此生产环境更推荐查找表方案。
双色与多色渐变的代码实现
下面示例展示如何用Swift生成一张三色渐变查找表并应用CIColorCube。我们以黑、紫、橙三色为例,亮度0到0.5从黑到紫,0.5到1从紫到橙。代码中的cubeSize为32,代表每个通道32级。
import CoreImage
func makeGradientCubeData() -> NSData {
let cubeSize = 32
let count = cubeSize * cubeSize * cubeSize * 4
var data = [Float](repeating: 0, count: count)
let black: (Float, Float, Float) = (0, 0, 0)
let purple: (Float, Float, Float) = (0.5, 0, 0.5)
let orange: (Float, Float, Float) = (1.0, 0.5, 0)
for b in 0..<cubeSize {
for g in 0..<cubeSize {
for r in 0..<cubeSize {
// 输入为灰度,r=g=b,取r作为亮度
let lum = Float(r) / Float(cubeSize - 1)
var outR: Float = 0, outG: Float = 0, outB: Float = 0
if lum < 0.5 {
let t = lum / 0.5
outR = black.0 + (purple.0 - black.0) * t
outG = black.1 + (purple.1 - black.1) * t
outB = black.2 + (purple.2 - black.2) * t
} else {
let t = (lum - 0.5) / 0.5
outR = purple.0 + (orange.0 - purple.0) * t
outG = purple.1 + (orange.1 - purple.1) * t
outB = purple.2 + (orange.2 - purple.2) * t
}
let offset = (b * cubeSize * cubeSize + g * cubeSize + r) * 4
data[offset] = outR
data[offset + 1] = outG
data[offset + 2] = outB
data[offset + 3] = 1.0
}
}
}
return NSData(bytes: data, length: count * MemoryLayout<Float>.size)
}
let cube = CIFilter(name: "CIColorCube")!
cube.setValue(32, forKey: "inputCubeSize")
cube.setValue(makeGradientCubeData(), forKey: "inputCubeData")
cube.setValue(inputImage, forKey: kCIInputImageKey)
let output = cube.outputImage
上述代码先把三色关系写成插值逻辑,再填进立方体。因为输入图像可能是彩色,我们在映射时仅使用了红色通道近似亮度;若希望更准确,应在之前插入CIColorMonochrome或者自己用CIColorMatrix做亮度提取。双色映射只需把中间purple段删掉,直接从black到orange插值即可,代码量减少但结构完全一致。
多色渐变(如五色)则可以把颜色数组化,用循环找到lum所在的区间再做局部插值。需要注意立方体边长越大,内存和初始化时间越长,32通常在视觉和性能间平衡较好。如果要在相机实时预览里用,建议把makeGradientCubeData的结果缓存起来,避免每帧重建。
色调分离与渐变映射的结合及方案对比
色调分离(Posterize)是指把连续亮度切成有限个台阶。我们可以在查找表生成阶段,故意把lum量化成几段,例如只保留0、0.33、0.66、1四档,再对每档映射颜色,这样就同时实现了“多色渐变”和“色调分离”。相比单独使用CIColorPosterize后再叠渐变,这种一步到位的方式不会出现边界色偏,也少一次滤镜调用。
另一种常见思路是用CIColorMatrix把RGB乘以系数并四舍五入,但CIColorMatrix只支持线性变换,无法表达“亮度0.2和0.8映射到不同颜色”的非单调映射,因此面对复杂渐变就无能为力。而CIColorCube因为是查表,可以表达任意曲线甚至离散跳变,灵活性最高。
从工程落地看,若你的App只需要双色且不需要分离,用CILinearGradient加混合也能凑合,但一旦设计稿出现多色或台阶感,尽早切到查找表方案最省心。测试时建议拿一张纯灰度渐变条图像验证,确认暗部不死黑、亮部不过曝,再接到真实业务图上。
Core_Image渐变映射色调分离修改时间:2026-08-13 11:55:03