iOS 中的 Swift:使用 Metal Kernel 的 Core Image:奇怪的内核行为。

4

大家好,开发者朋友们。

这是我第一次在Stackoverflow上提问。

我第一次接触到编写自定义Metal内核以创建Core Image过滤器。

任务看起来很简单。您需要制作一个过滤器来调整图像中的颜色色相、饱和度和亮度,受到色相范围+/-22.5度的限制。就像Lightroom颜色偏移调整之类的应用程序。

算法非常简单:

  1. 我将原始像素颜色和色调、饱和度和亮度范围和偏移值传递给函数;
  2. 在函数内部,我将颜色从RGB模式转换为HSL模式;
  3. 我检查阴影是否处于目标范围内;如果没有击中它,我不应用偏移值,如果击中了,我将偏移值添加到转换过程中获得的色调、饱和度和亮度上;
  4. 我将像素颜色转换回RGB模式;
  5. 我返回结果。

结果证明这是一个出色的算法,在PlayGround中成功地并且没有任何问题地完成了:

以下是源代码:

struct RGB {
    let r: Float
    let g: Float
    let b: Float
}

struct HSL {
    let hue: Float
    let sat: Float
    let lum: Float
}

func adjustingHSL(_ s: RGB, center: Float, hueOffset: Float, satOffset: Float, lumOffset: Float) -> RGB {
    // Determine the maximum and minimum color components
    let maxComp = (s.r > s.g && s.r > s.b) ? s.r : (s.g > s.b) ? s.g : s.b
    let minComp = (s.r < s.g && s.r < s.b) ? s.r : (s.g < s.b) ? s.g : s.b
    
    // Convert to HSL
    var inputHue: Float = (maxComp + minComp)/2
    var inputSat: Float = (maxComp + minComp)/2
    let inputLum: Float = (maxComp + minComp)/2
    
    if maxComp == minComp {
        inputHue = 0
        inputSat = 0
    } else {
        let delta: Float = maxComp - minComp
        
        inputSat = inputLum > 0.5 ? delta/(2.0 - maxComp - minComp) : delta/(maxComp + minComp)
        if (s.r > s.g && s.r > s.b) {inputHue = (s.g - s.b)/delta + (s.g < s.b ? 6.0 : 0.0) }
        else if (s.g > s.b) {inputHue = (s.b - s.r)/delta + 2.0}
        else {inputHue = (s.r - s.g)/delta + 4.0 }
        inputHue = inputHue/6
    }
    // Setting the boundaries of the offset hue range
    let minHue: Float = center - 22.5/(360)
    let maxHue: Float = center + 22.5/(360)
    
    // I apply offsets for hue, saturation and lightness 
    let adjustedHue: Float = inputHue + ((inputHue > minHue && inputHue < maxHue) ? hueOffset : 0 )
    let adjustedSat: Float = inputSat + ((inputHue > minHue && inputHue < maxHue) ? satOffset : 0 )
    let adjustedLum: Float = inputLum + ((inputHue > minHue && inputHue < maxHue) ? lumOffset : 0 )
    
    // Convert color to RGB
    var red: Float = 0
    var green: Float = 0
    var blue: Float = 0
    
    if adjustedSat == 0 {
        red = adjustedLum
        green = adjustedLum
        blue = adjustedLum
    } else {
        let q = adjustedLum < 0.5 ? adjustedLum*(1+adjustedSat) : adjustedLum + adjustedSat - (adjustedLum*adjustedSat)
        let p = 2*adjustedLum - q
        
        var t: Float = 0
        // Calculating red
        t = adjustedHue + 1/3
        if t < 0 { t += 1 }
        if t > 1 { t -= 1 }
        
        if t < 1/6 { red = p + (q - p)*6*t }
        else if t < 1/2 { red = q }
        else if t < 2/3 { red = p + (q - p)*(2/3 - t)*6 }
        else { red = p }
        
        // Calculating green
        t = adjustedHue
        if t < 0 { t += 1 }
        if t > 1 { t -= 1 }
        
        if t < 1/6 { green = p + (q - p)*6*t }
        else if t < 1/2 { green = q }
        else if t < 2/3 { green = p + (q - p)*(2/3 - t)*6 }
        else { green = p }
        
        // Calculating blue
        t = adjustedHue - 1/3
        if t < 0 { t += 1 }
        if t > 1 { t -= 1 }
        
        if t < 1/6 { blue = p + (q - p)*6*t }
        else if t < 1/2 { blue = q }
        else if t < 2/3 { blue = p + (q - p)*(2/3 - t)*6 }
        else { blue = p }
        
    }
    
    return RGB(r: red, g: green, b: blue)
}

在PlayGround中的应用举例,就像这样:

let inputColor = RGB(r: 255/255, g: 120/255, b: 0/255)
   
 // For visual perception of the input color
let initColor = UIColor(red: CGFloat(inputColor.r), green: CGFloat(inputColor.g), blue: CGFloat(inputColor.b), alpha: 1.0)

let rgb = adjustingHSL(inputColor, center: 45/360, hueOffset: 0, satOffset: 0, lumOffset: -0.2)

// For visual perception of the output color
let adjustedColor = UIColor(red: CGFloat(rgb.r), green: CGFloat(rgb.g), blue: CGFloat(rgb.b), alpha: 1.0)

相同的函数,在Xcode项目中为Metal内核重写后,给出了完全出乎意料的结果。
图像在变成黑白之后。同时,通过滑块更改输入参数也会改变图像本身。但它也很奇怪:它覆盖着小黑色或白色的正方形。
以下是在Metal内核中的源代码:
#include <metal_stdlib>

using namespace metal;

#include <CoreImage/CoreImage.h>

extern "C" {
    namespace coreimage {
        
        float4 hslFilterKernel(sample_t s, float center, float hueOffset, float satOffset, float lumOffset) {
            // Convert pixel color from RGB to HSL
            // Determine the maximum and minimum color components
            float maxComp = (s.r > s.g && s.r > s.b) ? s.r : (s.g > s.b) ? s.g : s.b ;
            float minComp = (s.r < s.g && s.r < s.b) ? s.r : (s.g < s.b) ? s.g : s.b ;
            
            float inputHue = (maxComp + minComp)/2 ;
            float inputSat = (maxComp + minComp)/2 ;
            float inputLum = (maxComp + minComp)/2 ;
            
            if (maxComp == minComp) {
                
                inputHue = 0 ;
                inputSat = 0 ;
            } else {
                float delta = maxComp - minComp ;
                
                inputSat = inputLum > 0.5 ? delta/(2.0 - maxComp - minComp) : delta/(maxComp + minComp);
                
                if (s.r > s.g && s.r > s.b) {
                    inputHue = (s.g - s.b)/delta + (s.g < s.b ? 6.0 : 0.0);
                } else if (s.g > s.b) {
                    inputHue = (s.b - s.r)/delta + 2.0;
                }
                else {
                    inputHue = (s.r - s.g)/delta + 4.0;
                }
                inputHue = inputHue/6 ;
            }
            
            float minHue = center - 22.5/(360) ;
            float maxHue = center + 22.5/(360) ;

            //I apply offsets for hue, saturation and lightness 
            
            float adjustedHue = inputHue + ((inputHue > minHue && inputHue < maxHue) ? hueOffset : 0 );
            float adjustedSat = inputSat + ((inputHue > minHue && inputHue < maxHue) ? satOffset : 0 );
            float adjustedLum = inputLum + ((inputHue > minHue && inputHue < maxHue) ? lumOffset : 0 );
            
            // Convert pixel color from HSL to RGB
            
            float red = 0 ;
            float green = 0 ;
            float blue = 0 ;
            
            if (adjustedSat == 0) {
                red = adjustedLum;
                green = adjustedLum;
                blue = adjustedLum;
            } else {
                
                float q = adjustedLum < 0.5 ? adjustedLum*(1+adjustedSat) : adjustedLum + adjustedSat - (adjustedLum*adjustedSat);
                float p = 2*adjustedLum - q;
                
                // Calculating Red color
                float t = adjustedHue + 1/3;
                if (t < 0) { t += 1; }
                if (t > 1) { t -= 1; }
                
                if (t < 1/6) { red = p + (q - p)*6*t; }
                else if (t < 1/2) { red = q; }
                else if (t < 2/3) { red = p + (q - p)*(2/3 - t)*6; }
                else { red = p; }
                
                // Calculating Green color
                t = adjustedHue;
                if (t < 0) { t += 1; }
                if (t > 1) { t -= 1; }
                
                if (t < 1/6) { green = p + (q - p)*6*t; }
                else if (t < 1/2) { green = q ;}
                else if (t < 2/3) { green = p + (q - p)*(2/3 - t)*6; }
                else { green = p; }
                
                // Calculating Blue color
                
                t = adjustedHue - 1/3;
                if (t < 0) { t += 1; }
                if (t > 1) { t -= 1; }
                
                if (t < 1/6) { blue = p + (q - p)*6*t; }
                else if (t < 1/2) { blue = q; }
                else if (t < 2/3) { blue = p + (q - p)*(2/3 - t)*6;}
                else { blue = p; }
                
            }

            float4 outColor;
            outColor.r = red;
            outColor.g = green;
            outColor.b = blue;
            outColor.a = s.a;
            
            return outColor;
            
        }
    }
}

我想不出我可能犯了什么错误。

以防万一,我附上一个过滤器类(但它似乎运行正常):

class HSLAdjustFilter: CIFilter {
    
    var inputImage: CIImage?
    var center: CGFloat?
    var hueOffset: CGFloat?
    var satOffset: CGFloat?
    var lumOffset: CGFloat?
   
    static var kernel: CIKernel = { () -> CIColorKernel in
        guard let url = Bundle.main.url(forResource: "HSLAdjustKernel.ci", withExtension: "metallib"),
              let data = try? Data(contentsOf: url)
        else { fatalError("Unable to load metallib") }
        
        guard let kernel = try? CIColorKernel(functionName: "hslFilterKernel", fromMetalLibraryData: data)
        else { fatalError("Unable to create color kernel") }
        
        return kernel
    }()
    
    
    override var outputImage: CIImage? {
        guard let inputImage = self.inputImage else { return nil }
  
        return HSLAdjustFilter.kernel.apply(extent: inputImage.extent, roiCallback: { _, rect in return rect }, arguments: [inputImage, self.center ?? 0, self.hueOffset ?? 0, self.satOffset ?? 0, self.lumOffset ?? 0])
    }
    
}

另外,调用过滤器的功能:

func imageProcessing(_ inputImage: CIImage) -> CIImage {

        let filter = HSLAdjustFilter()
        
        filter.inputImage = inputImage
        filter.center = 180/360
        filter.hueOffset = CGFloat(hue)
        filter.satOffset = CGFloat(saturation)
        filter.lumOffset = CGFloat(luminance)
        
        if let outputImage = filter.outputImage {
            return outputImage
        } else {
            return inputImage
        }
    }

最令人沮丧的是,您甚至无法向控制台输出任何内容。不清楚如何查找错误。 对于任何提示,我都将不胜感激。
PS:Xcode 13.1,iOS 14-15。SwiftUI生命周期。
GitHub:https://github.com/VKostin8311/MetalKernelsTestApp

好问题 - 已点赞。我的经验(目前)主要是OpenGL内核和UIKit。我在你的问题中注意到了两件事。首先,最后三个单词“SwiftUI生命周期”。你认为这是原因,还是实际上只是“噪音”?其次,由于这是一个颜色内核,请尝试一些东西。这里有一个例子:https://dev59.com/IaTja4cB1Zd3GeqPHMcV#45969446 它可能会消除playgrounds、UIKit,并指出发生了什么。 - user7014451
SwiftUI的生命周期在这里不会产生干扰。我尝试暂时从Metal Kernel中删除所有代码,只返回输入颜色。结果,图像一切正常。我还尝试交换输入颜色。在这里,也可以得到一个合理的结果。最后,我尝试使用输入偏移返回颜色。行为也是可以预期的。大问题在于找到错误。我至少可以调用print()函数并在控制台中查看过程。断点也没有触发。源代码在Github上:https://github.com/VKostin8311/MetalKernelsTestApp - diclonius9
2个回答

4

欢迎!

内核代码的主要问题在于使用整数字面量:

Metal Shading Language基于C ++,它没有与Swift相同的类型推断系统。因此,当您编写1/3时,实际上会执行整数除法,这样像float t = adjustedHue + 1/3这样的东西将等于adjustedHue + 0。在这里必须使用浮点字面量:adjustedHue + 1.0/3.0

我在您的示例项目上创建了一个拉取请求,其中包含各种修复和改进。如果有什么不清楚的,请告诉我。

至于调试:遗憾的是,无法使用断点和直接打印语句来调试内核代码。我通常使用像这样的像素颜色进行printf调试:

if (<condition I want to check>) { return float4(1.0, 0.0, 0.0, 1.0); }

所有符合条件的像素在本例中都变成了红色,您可以使用它来验证假设等等。

谢谢你的帮助!图片看起来好多了。我们只需要再看一下。一些照片中会出现不同颜色的矩形,大多是黑色或白色。可能是因为摄影材料质量不太好。 - diclonius9
很高兴能帮到你。你是否有一个实现你想要达到效果的参考实例? - Frank Rupprecht
Swift目前只有CPU的实现,而且在性能和范围方面也正在进行调试。目标是超越Lightroom。 - diclonius9

0
最令人沮丧的是,您甚至无法将任何内容输出到控制台。不清楚如何查找错误。我会感激任何提示。
为了开发和调试金属着色器,您应该使用Xcode着色器分析器
我正在用手机写信,从我现在所看到的来看,您的着色器代码中有一个错误:
if(maxComp == minComp)

浮点数应该始终与一个epsilon值进行比较。


1
很遗憾,金属框架调试器和其他工具不能用于核心图像内核,因为它们会被运行时重新编译。 - Frank Rupprecht
@FrankSchlegel 我之前不知道这个,因为我没有使用核心图像框架,但正如你已经提到的,核心图像内核语言基于Metal着色语言。提供的着色器内核实际上与片段着色器函数几乎相同,因此您可以在Xcode着色器分析器中轻松调试此着色器。PS:点赞 - 为您的提交。 - Hamid Yusifli
1
哎呀,我无法让着色器分析器与 Core Image 协同工作。我也与苹果的工程师进行了交流,他们表示目前还不可能实现。尽管他们或许可以通过新的 [[ stitchable ]] CI 核心添加调试支持。 - Frank Rupprecht
Core Image Metal管道非常方便...除了调试时的巨大困扰。即使是[[stitachables]]也只适用于iOS 15及以上版本。我尝试手动从我的.metallib中提取调试符号以导入到Xcode,虽然它最初能够识别它,但最终无法用于调试。@FrankRupprecht,你在2023年找到了一种调试Core Image Metal着色器的方法吗? - undefined
@DigitalSolomon 我还没有进一步研究过。一旦我将所有的内核移植到[[可拼接]],我会立即开始研究的。 - undefined
@FrankRupprecht 我无法让CIKernel与stitchable一起工作,看起来只有CIColorKernel支持该属性? - undefined

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