核心图像滤镜CISourceOverCompositing与alpha叠加预期不符

13

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

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

enter image description here

当然,代码包含在项目中,但也在此处包含。这创建了具有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!

希望我能帮到你。这个问题确实引起了我的注意。苹果文档建议查看它使用的公式(http://keithp.com/~keithp/porterduff/p253-porter.pdf)。你有检查过吗?特别是第4页,第4.3节?对于我目前使用的CI来说,它有点“希腊”,但也许它可以帮助你?似乎*意外地*发生了alpha值的乘法运算? - user7014451
谢谢@dfd,这是个好想法,但我不知道怎么做。如果你有什么发现的话,我已经更新了问题并提供了更多细节和示例项目! - Jordan H
@Joey,你不会相信,我也遇到了同样的问题,我尝试了很多方法,但都没有得到合适的解决方案。后来我尝试了一个小技巧,就是在图片后面加了一个白色的UIView,这样它就完美地工作了。你也可以试试,也许会有帮助 :) - Ravi Panchal
3个回答

7
经过多次尝试(感谢@andy和@Juraj Antas的帮助),我终于找到了答案。在Core Graphics上下文中进行绘制可以得到正确的外观,但使用该方法绘制图像需要更多资源。看起来问题出在上,但问题实际上在于默认情况下Core Image滤镜在线性SRGB空间中工作,而Core Graphics在知觉RGB空间中工作,这解释了不同的结果 - sRGB更擅长保留深黑色,而linearSRGB更擅长保留明亮的白色。因此原始代码没有问题,但你可以以不同的方式输出图像以获得不同的外观。
你可以使用执行不进行颜色管理的Core Image上下文从Core Image滤镜创建Core Graphics图像。这基本上会导致它将颜色值“错误地”解释为设备RGB(因为这是无颜色管理的默认值),例如标准颜色范围中的红色可能会出现更多来自广色域的红色。但这解决了与alpha合成相关的最初问题。
let ciContext = CIContext(options: [.workingColorSpace: NSNull()])
let outputCGImage = ciContext.createCGImage(outputCIImage, from: outputCIImage.extent)

保持颜色管理启用并指定工作的颜色空间为sRGB可能更为理想。这也能解决问题并使颜色解释“正确”。请注意,如果要合成的图像是Display P3,则需要将displayP3指定为工作颜色空间以保留广色域。

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

1
我还添加了 outputColorSpace。对于 Swift 4:let ciContext = CIContext(options: [CIContextOption.outputColorSpace: NSNull(), CIContextOption.workingColorSpace: NSNull()]) - lenooh

4

对于黑白文本

如果您使用的是.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(黑色)-图像是不透明的黑色。

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


要获得所需的结果,您需要将合成操作设置为.hardLight

.hardLight合成操作作为.multiply工作

如果文本图像的alpha小于50%(A <0.5,图像几乎透明)

.multiply的公式:Image1 * Image2


.hardLight合成操作作为.screen工作

如果文本图像的alpha大于或等于50%(A≥0.5,图像是半透明的)

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

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

.screen操作的结果比.plus更柔和,并且允许保持不大于1的alpha(plus操作会添加Image1和Image2的alpha,因此如果您有两个alpha,则可能会得到alpha = 2)。.screen合成操作非常适合制作反射。


enter image description here

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重新创建了一张图片。这里的偏移量是0.5,添加值为0.5。

我使用属性Offset=0.5,因为transparency=0.5.hardLight合成操作的pivot point(枢轴点)

enter image description here

enter image description here


对于彩色文本

如果你除了黑白文本还有橙色(或其他颜色)文本,你需要使用.sourceAtop合成操作。通过应用.setBlendMode方法中的.sourceAtop情况,你可以让Swift使用背景图像的亮度来确定要显示的内容。或者,你可以使用CISourceAtopCompositing核心图像过滤器来代替CISourceOverCompositing

bitmapContext.setBlendMode(.sourceAtop)

或者

let compositingFilter = CIFilter(name: "CISourceAtopCompositing")

.sourceAtop 操作的公式如下:(Image1 * A2) + (Image2 * (1 – A1))。可以看到,您需要两个阿尔法通道:A1 是文本的阿尔法值,A2 是背景图像的阿尔法值。

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)

enter image description here

enter image description here


在这种情况下,它取决于RGB和A。数学应用于RGB,alpha控制不透明度。因为这里是RGB*A模式(预乘图像)。 - Andy Jazz
你知道在你的数学中A是如何计算的吗? - Juraj Antas
你介意在这里写吗?我正在测试自定义的CIFilter。 - Juraj Antas
https://developer.apple.com/documentation/coregraphics/cgblendmode/1455901-hardlight - Andy Jazz
这绝对是由alpha控制的。 - Andy Jazz
显示剩余39条评论

2
最终答案:CISourceOverCompositing中的公式是正确的,这是正确的做法。
但是,在错误的颜色空间中运行。在图形程序中,您最有可能拥有sRGB颜色空间。在iOS通用RGB颜色空间中使用。这就是为什么结果不匹配的原因。
使用自定义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);
 }

要解决这个颜色“问题”,您必须将文本图像从RGB转换为sRGB。我猜您的下一个问题会是如何做到这一点;)
重要提示:iOS不支持设备无关或通用颜色空间。iOS应用程序必须使用设备颜色空间。 Apple有关颜色空间的文档

test image with RGB and sRGB color spaces


尝试使用 .hardLight 替换 .overlay。结果是否符合您的要求?如果不是,唯一的其他方法就是自定义 CIFilter。 - Juraj Antas
1
我正在测试自定义的CIFilter,我需要找到适用于“normal”混合模式的好数学公式。使用以下网址中的公式:http://www.simplefilter.de/en/basics/mixmods.html - Juraj Antas
1
使用自定义的CIFilter,我得到了与CISourceOverCompositing相同的结果。CISourceOverCompositing公式为:vec3 res = background *(1.0-textAlpha)+ text; vec3是RGB颜色向量。现在仍然有一个问题,即图形程序用于“正常”混合模式的组合公式是什么。很难找到它。 - Juraj Antas
嗯,我尝试用CGColorSpace(name: CGColorSpace.sRGB)替换CGColorSpaceCreateDeviceRGB(),但结果还是一样。 - Jordan H
换句话说,苹果是在说你需要自己动手。这并不难(但也不容易,颜色空间很棘手)http://www.ryanjuckett.com/programming/rgb-color-space-conversion/?start=2 - Juraj Antas
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接