【问题标题】:Swift iOS: Core Image with Metal Kernel: Strange kernel behaviorSwift iOS:带有金属内核的核心映像:奇怪的内核行为
【发布时间】:2025-11-30 09:00:01
【问题描述】:

开发者朋友们好。

我第一次在 * 上提问。

我第一次遇到编写自定义金属内核来创建核心图像过滤器。

任务看起来很简单。您需要制作一个滤镜来调整图像中颜色的色调、饱和度和亮度,受色调范围 +/- 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.1iOS 14-15。 SwiftUI 生命周期。

GitHub:https://github.com/VKostin8311/MetalKernelsTestApp

【问题讨论】:

  • 很好的问题 - 并且赞成。我的经验(目前)主要是使用 OpenGL 内核和 UIKit。我注意到你的问题中有两点。首先,最后三个字“SwiftUI 生命周期”。您认为这是原因,还是实际上只是对实际问题的“噪音”?其次,由于这是一个颜色内核,请尝试一些东西。这是一个例子:*.com/questions/45968561/… 它可能会消除 Playgrounds、UIKit,并指出正在发生的事情。
  • 这里不干涉 SwiftUI 的生命周期。我试图暂时从 Metal Kernel 中删除所有代码并返回输入颜色。结果,图像一切正常。我还尝试交换输入颜色。在这里,也完全可以期待一个适当的结果。最后,我尝试使用输入偏移量返回颜色。也是相当预期的行为。最大的问题是寻找错误。我至少可以调用 print() 函数并在控制台中查看该过程。断点也不会被触发。 Github上的源代码:github.com/VKostin8311/MetalKernelsTestApp

标签: ios swift metal core-image xcode13


【解决方案1】:

最郁闷的是,你甚至不能输出任何东西到 控制台。目前还不清楚如何查找错误。我会很感激 任何提示。

要开发和调试金属着色器,您应该使用Xcode shader profiler

我现在正在用手机写信,据我所知,你的着色器代码有一个错误:

if(maxComp == minComp)

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

【讨论】:

  • Metal Frame Debugger 和其他工具很遗憾不能用于 Core Image 内核,因为它们是由运行时重新编译的。
  • @FrankSchlegel 我不知道这一点,因为我不使用核心图像框架,但正如你已经提到的核心图像内核语言基于金属着色语言。提供的着色器内核实际上与片段着色器功能相同,因此您可以在 Xcode 着色器分析器中轻松调试此着色器。 PS:upvote - 为您的提交。
  • 嗯,我无法让着色器分析器与 Core Image 一起使用。我也和苹果的工程师谈过这个问题,他们说现在不可能。尽管他们可能能够使用新的[[ stitchable ]] CI 内核添加调试支持。
【解决方案2】:

欢迎!

内核代码的主要问题是整数文字的使用:

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

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

至于调试:很遗憾,使用断点和直接打印语句无法调试内核代码。我通常使用像素颜色进行printf-debugging,如下所示:

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

这种情况下所有满足条件的像素都会变成红色,可以用来验证假设等。

【讨论】:

  • 感谢您的帮助!图像看起来好多了。我们只需要再看一遍。一些照片中出现不同颜色的矩形,大多是黑色或白色。也许是因为摄影材料的质量不是很好
  • 很高兴我能帮上忙。您是否有想要达到的效果的参考实现?
  • 在 Swift 上只有 CPU 的实现。它还在性能和范围方面进行调试。目标是超越 Lightroom。