核心图像过滤器 CISourceOverCompositing 未按预期显示与 alpha 叠加

Core Image filter CISourceOverCompositing not appearing as expected with alpha overlay

我正在使用 CISourceOverCompositing 在图像上叠加文本,当文本图像不是完全不透明时,我得到了意想不到的结果。输出图像中的深色不够深,浅色太浅。

我在 simple Xcode project 中重现了这个问题。它创建了一个图像,其中包含以 0.3 alpha 绘制的橙色、白色、黑色文本,看起来是正确的。我什至将该图像放入 Sketch 中,将其放在背景图像之上,看起来很棒。屏幕底部的图像显示了它在 Sketch 中的外观。问题是,在使用 CISourceOverCompositing 将文本叠加在背景上之后,白色文本太不透明,就像 alpha 为 0.5 一样,而黑色文本几乎不可见,就像 alpha 为 0.1 一样。顶部图像显示了以编程方式创建的图像。您可以拖动滑块来调整 alpha(默认为 0.3),这将重新创建结果图像。

代码当然包含在项目中,但也包含在这里。这将创建具有 0.3 alpha 的文本叠加层,如预期的那样显示。

let colorSpace = CGColorSpaceCreateDeviceRGB()
let alphaInfo = CGImageAlphaInfo.premultipliedLast.rawValue

let bitmapContext = CGContext(data: nil, width: Int(imageRect.width), height: Int(imageRect.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: alphaInfo)!
bitmapContext.setAlpha(0.3)
bitmapContext.setTextDrawingMode(CGTextDrawingMode.fill)
bitmapContext.textPosition = CGPoint(x: 20, y: 20)

let displayLineTextWhite = CTLineCreateWithAttributedString(NSAttributedString(string: "hello world", attributes: [.foregroundColor: UIColor.white, .font: UIFont.systemFont(ofSize: 50)]))
CTLineDraw(displayLineTextWhite, bitmapContext)

let textCGImage = bitmapContext.makeImage()!
let textImage = CIImage(cgImage: textCGImage)

接下来,文本图像叠加在背景图像之上,但未按预期显示。

let combinedFilter = CIFilter(name: "CISourceOverCompositing")!
combinedFilter.setValue(textImage, forKey: "inputImage")
combinedFilter.setValue(backgroundImage, forKey: "inputBackgroundImage")
let outputImage = combinedFilter.outputImage!

最终答案:CISourceOverCompositing 中的公式很好。这是正确的做法。

但是

它的工作颜色有误space。在图形程序中,您最有可能使用 sRGB 颜色 space。在 iOS 上使用通用 RGB 颜色 space。这就是结果不匹配的原因。

我使用自定义 CIFilter 重新创建了 CISourceOverCompositing 过滤器。
s1是文字图片。
s2 是背景图片。

它的内核是这样的:

 kernel vec4 opacity( __sample s1, __sample s2) {
     vec3 text = s1.rgb;
     float textAlpha = s1.a;
     vec3 background = s2.rgb;

     vec3 res = background * (1.0 - textAlpha) + text;
     return vec4(res, 1.0);
 }

因此,要修复此颜色 'issue',您必须将文本图像从 RGB 转换为 sRGB。我猜你的下一个问题是如何做 ;)

重要提示:iOS 不支持 device-independent 或通用颜色 space。 iOS 应用程序必须改用设备颜色 space。 Apple doc about color spaces

对于 black-and-white 文本

如果您使用 .normal 合成操作,您肯定会得到与使用 .hardLight 不同的结果。您的图片显示了 .hardLight 操作的结果。

.normal运算经典OVER运算公式:(Image1 * A1) + (Image2 * (1 – A1)).

这是一个预乘文本 (RGB*A),因此在这种特殊情况下,RGB 图案取决于 A 的不透明度。文本图像的 RGB 可以包含任何颜色,包括黑色。如果 A=0(黑色 alpha)和 RGB=0(黑色)并且您的图像是预乘的——整个图像是完全透明的,如果 A=1(白色 alpha)和 RGB=0(黑色)——图像是不透明的黑色.

如果你使用.normal操作时你的文本没有alpha,我会得到ADD操作:Image1 + Image2.


要得到你想要的,你需要设置一个合成操作到.hardLight

.hardLight 合成操作相当于 .multiply

if alpha of text image less than 50 percent (A < 0.5, the image is almost transparent)

.multiply 的公式:Image1 * Image2


.hardLight 合成操作相当于 .screen

if alpha of text image greater than or equal to 50 percent (A >= 0.5, the image is semi-opaque)

.screen 的公式 1:(Image1 + Image2) – (Image1 * Image2)

.screen 的公式 2:1 – (1 – Image1) * (1 – Image2)

.screen 操作比 .plus 的结果更柔和,它允许保持 alpha 不大于 1(plus 操作添加 Image1 和 Image2 的 alpha,所以你可能会得到alpha = 2,如果你有两个 alpha)。 .screen 合成操作有利于制作反射。


func editImage() {

    print("Drawing image with \(selectedOpacity) alpha")
    
    let text = "hello world"
    let backgroundCGImage = #imageLiteral(resourceName: "background").cgImage!
    let backgroundImage = CIImage(cgImage: backgroundCGImage)
    let imageRect = backgroundImage.extent
    
    //set up transparent context and draw text on top
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let alphaInfo = CGImageAlphaInfo.premultipliedLast.rawValue
    
    let bitmapContext = CGContext(data: nil, width: Int(imageRect.width), height: Int(imageRect.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: alphaInfo)!
    bitmapContext.draw(backgroundCGImage, in: imageRect)
    
    bitmapContext.setAlpha(CGFloat(selectedOpacity))
    bitmapContext.setTextDrawingMode(.fill)

    //TRY THREE COMPOSITING OPERATIONS HERE 
    bitmapContext.setBlendMode(.hardLight)
    //bitmapContext.setBlendMode(.multiply)
    //bitmapContext.setBlendMode(.screen)
    
    //white text
    bitmapContext.textPosition = CGPoint(x: 15 * UIScreen.main.scale, y: (20 + 60) * UIScreen.main.scale)
    let displayLineTextWhite = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.white, .font: UIFont.systemFont(ofSize: 58 * UIScreen.main.scale)]))
    CTLineDraw(displayLineTextWhite, bitmapContext)
    
    //black text
    bitmapContext.textPosition = CGPoint(x: 15 * UIScreen.main.scale, y: 20 * UIScreen.main.scale)
    let displayLineTextBlack = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 58 * UIScreen.main.scale)]))
    CTLineDraw(displayLineTextBlack, bitmapContext)
    
    let outputImage = bitmapContext.makeImage()!
    
    topImageView.image = UIImage(cgImage: outputImage)
}

因此,要重新创建此合成操作,您需要以下逻辑:

//rgb1 – text image 
//rgb2 - background
//a1   - alpha of text image

if a1 >= 0.5 { 
    //use this formula for compositing: 1–(1–rgb1)*(1–rgb2) 
} else { 
    //use this formula for compositing: rgb1*rgb2 
}

我使用合成应用程序 The Foundry NUKE 11 重新创建了一个图像。Offset=0.5 这里是 Add=0.5。

我使用属性 Offset=0.5 因为transparency=0.5pivot point.hardLight 合成操作。


对于彩色文本

如果除了黑白文本之外还有橙色(或任何其他颜色)文本,则需要使用 .sourceAtop 合成操作。应用 .setBlendMode 方法的 .sourceAtop 案例 Swift 使用背景图像的亮度来确定要显示的内容。或者,您可以使用 CISourceAtopCompositing 核心图像过滤器而不是 CISourceOverCompositing

bitmapContext.setBlendMode(.sourceAtop)

let compositingFilter = CIFilter(name: "CISourceAtopCompositing")

.sourceAtop运算公式如下:(Image1 * A2) + (Image2 * (1 – A1))。如您所见,您需要两个 Alpha 通道:A1 是文本的 Alpha,A2 是背景图像的 Alpha。

bitmapContext.textPosition = CGPoint(x: 15 * UIScreen.main.scale, y: (20 + 60) * UIScreen.main.scale)
let displayLineTextOrange = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.orange, .font: UIFont.systemFont(ofSize: 58 * UIScreen.main.scale)]))
CTLineDraw(displayLineTextOrange, bitmapContext)

经过多次来回尝试不同的事情,(感谢@andy 和@Juraj Antas 将我推向正确的方向)我终于找到了答案。

因此,绘制到 Core Graphics 上下文中会产生正确的外观,但使用该方法绘制图像需要更多资源。看起来问题出在 CISourceOverCompositing,但问题实际上在于,默认情况下,Core Image 过滤器在线性 SRGB space 中工作,而 Core Graphics 在感知 RGB space 中工作,这解释了不同的结果——sRGB 更擅长保留深黑色,而 linearSRGB 更擅长保留亮白色。所以原来的代码是可以的,但是你可以用不同的方式输出图像以获得不同的外观。

您可以使用不执行颜色管理的 Core Image 上下文从 Core Image 过滤器创建 Core Graphics 图像。这实质上导致它将颜色值“错误地”解释为设备 RGB(因为这是没有颜色管理的默认设置),例如,这可能导致标准颜色范围中的红色在宽颜色范围中看起来更红。但这解决了最初对 alpha 合成的担忧。

let ciContext = CIContext(options: [.workingColorSpace: NSNull()])
let outputCGImage = ciContext.createCGImage(outputCIImage, from: outputCIImage.extent)

可能更需要启用颜色管理并将工作颜色 space 指定为 sRGB。这也解决了问题并导致“正确”的颜色解释。请注意,如果正在合成的图像是 Display P3,您需要将 displayP3 指定为工作颜色 space 以保留宽色。

let workingColorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
let ciContext = CIContext(options: [.workingColorSpace: workingColorSpace])
let outputCGImage = ciContext.createCGImage(outputCIImage, from: outputCIImage.extent)