为什么CILinearGradient产生的渐变非常非线性?

5
我是一名相对较新的Swift开发者,我正在使用CILinearGradient CIFilter生成渐变,然后将其用作背景和纹理。一开始我对它的工作方式感到非常满意,直到我意识到它生成的渐变似乎严重偏向光谱的黑色端。

起初我以为自己疯了,但后来我创建了纯黑到白和白到黑的渐变,并将它们并排放在屏幕上。我截了一张屏幕截图并将其带入Photoshop。然后我查看了颜色值。你可以看到每个渐变的末端对齐(一个末端是纯黑色覆盖纯白色,另一个末端则相反),但每个渐变的中点明显偏向黑色端。

enter image description here

这是CIFilter的问题还是我的操作有误?感谢任何了解此事的人提供帮助!
以下是我的代码:
func gradient2colorIMG(UIcolor1: UIColor, UIcolor2: UIColor, width: CGFloat, height: CGFloat) -> CGImage? {
    if let gradientFilter = CIFilter(name: "CILinearGradient") {   
        let startVector:CIVector = CIVector(x: 0 + 10, y: 0)
        let endVector:CIVector = CIVector(x: width - 10, y: 0)
        let color1 = CIColor(color: UIcolor1)
        let color2 = CIColor(color: UIcolor2)
        let context = CIContext(options: nil)
        if let currentFilter = CIFilter(name: "CILinearGradient") {
            currentFilter.setValue(startVector, forKey: "inputPoint0")
            currentFilter.setValue(endVector, forKey: "inputPoint1")
            currentFilter.setValue(color1, forKey: "inputColor0")
            currentFilter.setValue(color2, forKey: "inputColor1")
            if let output = currentFilter.outputImage {
                if let cgimg = context.createCGImage(output, from: CGRect(x: 0, y: 0, width: width, height: height)) {
                    let gradImage = cgimg
                    return gradImage
                }
            }
        }
    }
    return nil
}

然后我使用以下代码在SpriteKit中调用它(但这只是为了让我在屏幕上看到它们,以便比较函数输出的CGImages)...

if let gradImage = gradient2colorIMG(UIcolor1: UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0), UIcolor2: UIColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0), width: 250, height: 80) {        
    let sampleback = SKShapeNode(path: CGPath(roundedRect: CGRect(x: 0, y: 0, width: 250, height: 80), cornerWidth: 5, cornerHeight: 5, transform: nil))
    sampleback.fillColor = .white
    sampleback.fillTexture = SKTexture(cgImage: gradImage)
    sampleback.zPosition = 200
    sampleback.position = CGPoint(x: 150, y: 50)
    self.addChild(sampleback)
}    
if let gradImage2 = gradient2colorIMG(UIcolor1: UIColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0), UIcolor2: UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0), width: 250, height: 80) {    
    let sampleback2 = SKShapeNode(path: CGPath(roundedRect: CGRect(x: 0, y: 0, width: 250, height: 80), cornerWidth: 5, cornerHeight: 5, transform: nil))
    sampleback2.fillColor = .white
    sampleback2.fillTexture = SKTexture(cgImage: gradImage2)
    sampleback2.zPosition = 200
    sampleback2.position = CGPoint(x: 150, y: 150)
    self.addChild(sampleback2)
}

作为另一个后续,我尝试做了一个红蓝渐变(仅改变色调),它是完全线性的(如下所示)。问题似乎在于亮度。

红蓝渐变确实以完全线性的方式增加其色调。


请编辑您的问题并发布一个[mcve]。您在gradient2colorIMG方法中创建了两个上下文。请发布您的实际代码。 - Leo Dabus
尝试使用从0到1的相对坐标定义CIVector输入点。 - Eugene Dudnyk
感谢@LeoDabus。那个额外的CGcontext是之前尝试中留下的错误。我希望我的编辑已经满足了你的要求。 - Singular20
1
我不确定它是如何确定颜色分布的,但我发现有趣的是,你上面图片的中点(使用Photoshop吸管工具)的RGB值大约为188, 188, 188,看起来像是middle gray的“绝对白色”表现。此外,请注意,将RGB值转换为相对亮度并不是RGB的简单算术平均值,而是0.2126 * 红色 + 0.7152 * 绿色 + 0.0722 * 蓝色。在将RGB转换为灰度图像时,我们经常使用该公式。 - Rob
这是CIContext的颜色空间。我已经添加了一个演示答案。 - matt
2个回答

3
假设黑色为0,白色为1。那么这里的问题是我们直觉上认为50%的黑色“是”灰度值0.5——但事实并非如此。为了理解这一点,请考虑以下核心图像实验:
let con = CIContext(options: nil)
let white = CIFilter(name:"CIConstantColorGenerator")!
white.setValue(CIColor(color:.white), forKey:"inputColor")
let black = CIFilter(name:"CIConstantColorGenerator")!
black.setValue(CIColor(color:UIColor.black.withAlphaComponent(0.5)),
    forKey:"inputColor")
let atop = CIFilter(name:"CISourceAtopCompositing")!
atop.setValue(white.outputImage!, forKey:"inputBackgroundImage")
atop.setValue(black.outputImage!, forKey:"inputImage")
let cgim = con.createCGImage(atop.outputImage!, 
    from: CGRect(x: 0, y: 0, width: 201, height: 50))!
let image = UIImage(cgImage: cgim)
let iv = UIImageView(image:image)
self.view.addSubview(iv)
iv.frame.origin = CGPoint(x: 100, y: 150)

这里所做的是在白色样本上放置一个50%透明度的黑色样本。我们直观地想象结果将为0.5的样本。但它不是;它是0.737,与您渐变中点出现的完全相同的颜色。

enter image description here

原因在于这里发生的一切都不是在某种数学真空中进行的,而是在调整了特定伽马值的色彩空间中进行的。
现在,您可能会公正地问:“但我在哪里指定了这个颜色空间?这不是我想要的!”啊哈。当您创建一个CIContext而没有覆盖默认的工作色彩空间时,在第一行中就已经指定了它。
让我们来修复一下。将第一行更改为以下内容:
let con = CIContext(options: [.workingColorSpace : NSNull()])

现在输出结果是这样的:

enter image description here

完成,这就是你的0.5灰度!

我的意思是,如果你像那样创建CIContext,你将获得你想要的渐变,中间点为0.5灰度。我并不是说这比你得到的结果更“正确”,但至少它展示了如何使用你已经有的代码来获得特定的结果。

(实际上,我认为你最初得到的结果更“正确”,因为它已经调整了人类的感知。)


谢谢!这绝对解决了我遇到的问题。考虑到这些信息,我同意您关于“正确”与“不正确”的看法,并且知道在哪里进行调整才是真正重要的。 - Singular20
没错,我只是在试图解释这个现象;我并不是在建议你这样做,我只是展示如果你这样做会发生什么。 - matt

2

CILinearGradient的中点似乎对应于188,188,188,看起来像是中灰色的“绝对白度”表现,这并不完全不合理。(CISmoothLinearGradient提供了更平滑的过渡,但它的中点也不是0.5,0.5,0.5。)顺便说一下,在CILinearGradientCISmoothLinearGradient中,“线性”指的是渐变的形状(以区别于“径向”渐变),而不是渐变内颜色过渡的性质。

然而,如果您想要中点为0.5,0.5,0.5的渐变,可以使用CGGradient

func simpleGradient(in rect: CGRect) -> UIImage {
    return UIGraphicsImageRenderer(bounds: rect).image { context in
        let colors = [UIColor.white.cgColor, UIColor.black.cgColor]
        let colorSpace = CGColorSpaceCreateDeviceGray() // or RGB works, too
        guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: nil) else { return }
        context.cgContext.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: rect.maxX, y: 0), options: [])
    }
}

渐变图像


或者,如果您想要一个渐变背景,您可以定义一个UIView子类,使用CAGradientLayer作为其支持层:

class GradientView: UIView {
    override class var layerClass: AnyClass { return CAGradientLayer.self }
    var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configure()
    }

    func configure() {
        gradientLayer.colors = [UIColor.white.cgColor, UIColor.black.cgColor]
        gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
    }
}

这段内容与问题无关,但是你是否忘记在layoutSubviews或traitCollectionDidChange上更新梯度了? - Leo Dabus
@LeoDabus - 不需要这样做,使用layerClass设置背景层就可以了。实际上,这种方法更可取,因为它在动画过程中也能得到适当的渲染,不像layoutSubviews和类似方法那样会从一个大小跳到另一个大小。 - Rob
1
是的。或者当我需要根据更新后的边界手动更新路径时,我会使用这些方法。但对于像这样简单的渐变,最好使用layerClass并进行设置和忘记。 - Rob
在没有关于CIFilter内部算法的明确文档的情况下,我认为“为什么”问题没有好的答案。但是我已经在答案中加入了我的“中间灰色”假设。希望展示如何实现所需行为会有所帮助。 - Rob
顺带一提,“CILinearGradient” 和 “CISmoothLinearGradient” 中的“线性”指的是渐变的形状(与“径向”渐变区分开来),而不是渐变内颜色过渡的性质。这是对的,但也有点不对。因为文档中说混合也是线性的:“CILinearGradient” 滤镜通过线性混合颜色(也就是,在第一个点和第二个点之间的线中,25% 处的颜色是 25% 的颜色 1 和 75% 的颜色 2)。这就是我们正在探讨的问题。50% 的位置并不是黑白各占 50%。 - matt
显示剩余5条评论

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