如何用Core Image复刻Photoshop的阴影/中间调/高光色彩平衡?
嘿,我完全懂你现在的困扰——用Core Image复刻Photoshop的色彩平衡,尤其是分阴影、中间调、高光单独调色这块,确实没那么直接。你之前试的CIColorMatrix是全局调整,CIHighlightShadowAdjust又只能调明暗没法碰色彩,肯定达不到PS的效果。
其实核心思路是把图像按亮度分成三个区域,分别给每个区域做色彩偏移,再把它们合成回去,这和Photoshop内部的逻辑是一致的。我给你两种可行的方案,你可以根据需求选:
方案一:用现有Core Image滤镜组合实现(无需自定义Shader)
这个方案全靠系统自带滤镜拼出效果,适合快速上手:
步骤1:生成三个区域的蒙版
首先得把阴影、中间调、高光的区域提取出来做蒙版,这样才能只调整对应区域的色彩。这里用亮度阈值划分区域,还加了模糊让过渡更自然(避免生硬的边界):
func createBrightnessMask(from image: CIImage, threshold: CGFloat, isInverted: Bool = false) -> CIImage { // 转灰度亮度图 let grayscaleFilter = CIFilter(name: "CIColorControls")! grayscaleFilter.setValue(image, forKey: kCIInputImageKey) grayscaleFilter.setValue(0, forKey: kCIInputSaturationKey) let grayscale = grayscaleFilter.outputImage! // 阈值提取蒙版 let thresholdFilter = CIFilter(name: "CIThresholdAdjust")! thresholdFilter.setValue(grayscale, forKey: kCIInputImageKey) thresholdFilter.setValue(threshold, forKey: kCIInputThresholdKey) var mask = thresholdFilter.outputImage! // 按需反相蒙版 if isInverted { let invertFilter = CIFilter(name: "CIColorInvert")! invertFilter.setValue(mask, forKey: kCIInputImageKey) mask = invertFilter.outputImage! } // 模糊蒙版让过渡柔和 let blurFilter = CIFilter(name: "CIGaussianBlur")! blurFilter.setValue(mask, forKey: kCIInputImageKey) blurFilter.setValue(2, forKey: kCIInputRadiusKey) return blurFilter.outputImage! } // 生成三个蒙版:阴影(亮度<0.3)、高光(亮度>0.7)、中间调(剩下的区域) let inputImage = gammaOutput // 你的输入图像 let shadowMask = createBrightnessMask(from: inputImage, threshold: 0.3) let highlightMask = createBrightnessMask(from: inputImage, threshold: 0.7, isInverted: true) let midtonesMask = CIFilter(name: "CIAdditionCompositing")! midtonesMask.setValue(shadowMask, forKey: kCIInputImageKey) midtonesMask.setValue(highlightMask, forKey: kCIInputBackgroundImageKey) let invertedCombined = midtonesMask.outputImage!.applyingFilter("CIColorInvert") let midtonesMaskFinal = invertedCombined.applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: 2])
步骤2:给每个区域做色彩平衡调整
PS的色彩平衡是基于互补色滑块(红-青、黄-蓝、品红-绿),我们用CIColorMatrix的inputBiasVector实现色彩偏移,把PS的滑块值(-100到+100)转成合适的偏移范围:
func applyColorBalance(to image: CIImage, redCyan: CGFloat, yellowBlue: CGFloat, magentaGreen: CGFloat) -> CIImage { let colorMatrix = CIFilter(name: "CIColorMatrix")! colorMatrix.setValue(image, forKey: kCIInputImageKey) // 转换PS滑块值到Core Image的bias范围(这里用-0.5到+0.5,可根据效果调整比例) let redBias = redCyan / 200 // 品红是减绿,绿是加绿,所以取反 let greenBias = magentaGreen / -200 // 黄是减蓝,蓝是加蓝,取反 let blueBias = yellowBlue / -200 colorMatrix.setValue(CIVector(x: 1, y: 0, z: 0, w: 0), forKey: "inputRVector") colorMatrix.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputGVector") colorMatrix.setValue(CIVector(x: 0, y: 0, z: 1, w: 0), forKey: "inputBVector") colorMatrix.setValue(CIVector(x: redBias, y: greenBias, z: blueBias, w: 0), forKey: "inputBiasVector") return colorMatrix.outputImage! } // 分别调整三个区域的色彩(参数对应PS滑块数值) let shadowAdjusted = applyColorBalance(to: inputImage, redCyan: 50, yellowBlue: -30, magentaGreen: 20) let midtonesAdjusted = applyColorBalance(to: inputImage, redCyan: 20, yellowBlue: 10, magentaGreen: -10) let highlightAdjusted = applyColorBalance(to: inputImage, redCyan: -20, yellowBlue: 40, magentaGreen: 0)
步骤3:合成三个调整后的图层
用蒙版把每个调整后的区域叠加回原图,保证只有对应区域的色彩被修改:
// 合成调整层到基础图的通用函数 func compositeAdjustedLayer(base: CIImage, adjusted: CIImage, mask: CIImage) -> CIImage { let composite = CIFilter(name: "CISourceAtopCompositing")! composite.setValue(adjusted, forKey: kCIInputImageKey) composite.setValue(mask, forKey: kCIInputMaskImageKey) composite.setValue(base, forKey: kCIInputBackgroundImageKey) return composite.outputImage! } let withShadow = compositeAdjustedLayer(base: inputImage, adjusted: shadowAdjusted, mask: shadowMask) let withMidtones = compositeAdjustedLayer(base: withShadow, adjusted: midtonesAdjusted, mask: midtonesMaskFinal) let finalImage = compositeAdjustedLayer(base: withMidtones, adjusted: highlightAdjusted, mask: highlightMask)
方案二:自定义Metal Shader实现(更高效更可控)
如果你的应用对性能要求高,或者想要更平滑的区域过渡,自定义Shader是更好的选择——它能在一次渲染中完成所有逻辑,还能直接实现PS的“保持明度”功能:
步骤1:编写Metal Kernel代码
这个kernel会计算每个像素的亮度,判断属于哪个区域,然后应用对应的色彩偏移,同时保持原图像的明度:
#include <CoreImage/CoreImage.h> using namespace metal; kernel void colorBalanceWithRanges( sampler inputImage, constant float &shadowThreshold, constant float &highlightThreshold, constant float3 &shadowOffset, constant float3 &midtonesOffset, constant float3 &highlightOffset, destination dest ) { float2 coords = dest.coord(); float4 color = inputImage.sample(coords); // 用BT.709标准计算亮度(和PS的明度计算一致) float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; float3 offset; // 平滑过渡阴影、中间调、高光的偏移 if (luminance <= shadowThreshold) { offset = shadowOffset; } else if (luminance >= highlightThreshold) { offset = highlightOffset; } else { float midPoint = (shadowThreshold + highlightThreshold) / 2.0; if (luminance < midPoint) { float t = smoothstep(shadowThreshold, midPoint, luminance); offset = mix(shadowOffset, midtonesOffset, t); } else { float t = smoothstep(midPoint, highlightThreshold, luminance); offset = mix(midtonesOffset, highlightOffset, t); } } // 应用色彩偏移 float4 newColor = color + float4(offset, 0.0); // 保持明度:把新图像的亮度调整回原亮度 float newLuminance = 0.2126 * newColor.r + 0.7152 * newColor.g + 0.0722 * newColor.b; float luminanceScale = luminance / max(newLuminance, 0.001); // 避免除以0 newColor.rgb *= luminanceScale; dest.write(newColor); }
步骤2:在Swift中注册自定义滤镜
把上面的Metal代码包装成Core Image滤镜,方便调用:
class PSColorBalanceFilter: CIFilter { private let kernel: CIKernel? // 输入参数 @objc var inputImage: CIImage? @objc var shadowThreshold: CGFloat = 0.3 // 阴影亮度阈值 @objc var highlightThreshold: CGFloat = 0.7 // 高光亮阈值 @objc var shadowOffset: CIVector = CIVector(x: 0, y: 0, z: 0) // 阴影的RGB偏移(对应PS滑块) @objc var midtonesOffset: CIVector = CIVector(x: 0, y: 0, z: 0) // 中间调偏移 @objc var highlightOffset: CIVector = CIVector(x: 0, y: 0, z: 0) // 高光偏移 override init() { let source = """ // 上面的Metal Kernel代码 """ kernel = CIKernel(source: source) super.init() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var outputImage: CIImage? { guard let inputImage = inputImage, let kernel = kernel else { return nil } let args: [Any] = [ shadowThreshold, highlightThreshold, SIMD3<Float>(x: Float(shadowOffset.x), y: Float(shadowOffset.y), z: Float(shadowOffset.z)), SIMD3<Float>(x: Float(midtonesOffset.x), y: Float(midtonesOffset.y), z: Float(midtonesOffset.z)), SIMD3<Float>(x: Float(highlightOffset.x), y: Float(highlightOffset.y), z: Float(highlightOffset.z)) ] return kernel.apply( extent: inputImage.extent, roiCallback: { _, rect in rect }, arguments: args, inputImage: inputImage ) } }
使用自定义滤镜
let colorBalanceFilter = PSColorBalanceFilter() colorBalanceFilter.inputImage = gammaOutput // 设置参数,比如阴影加红(0.2)、减蓝(-0.1) colorBalanceFilter.shadowOffset = CIVector(x: 0.2, y: 0, z: -0.1) // 中间调加黄(0.1即加绿减蓝) colorBalanceFilter.midtonesOffset = CIVector(x: 0, y: 0.1, z: -0.1) // 高光加蓝(0.2) colorBalanceFilter.highlightOffset = CIVector(x: 0, y: 0, z: 0.2) let finalImage = colorBalanceFilter.outputImage!
关键说明
- 蒙版的阈值(0.3和0.7)可以根据需求调整,对应PS色彩平衡里的阴影、中间调、高光范围。
- 如果要完全匹配PS的“保持明度”功能,方案二的自定义Shader已经实现,方案一可以额外把原图的亮度通道合成回去。
- 色彩偏移的比例可以根据你的滑块范围调整,比如PS滑块是-100到+100,你可以把偏移值设置为
sliderValue / 200,这样范围是-0.5到+0.5,效果更接近PS。
内容的提问来源于stack exchange,提问作者Spenciefy
相关产品推荐
相关产品推荐

