【发布时间】:2025-11-30 09:00:01
【问题描述】:
开发者朋友们好。
我第一次在 * 上提问。
我第一次遇到编写自定义金属内核来创建核心图像过滤器。
任务看起来很简单。您需要制作一个滤镜来调整图像中颜色的色调、饱和度和亮度,受色调范围 +/- 22.5 度的限制。与 Lightroom 颜色偏移调整等应用程序一样。
算法非常简单:
- 我将原始像素颜色以及色调、饱和度和亮度的范围和偏移值传递给函数;
- 在函数内部,我将颜色从 RGB 方案转换为 HSL 方案;
- 我检查阴影是否在目标范围内; 如果我没有打到,我不应用偏移,如果我打到,我将偏移值添加到转换过程中获得的色调、饱和度和亮度;
- 我会将像素颜色转换回 RGB 方案;
- 我返回结果。
结果证明这是一个很棒的算法,在 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 生命周期。
【问题讨论】:
-
很好的问题 - 并且赞成。我的经验(目前)主要是使用 OpenGL 内核和 UIKit。我注意到你的问题中有两点。首先,最后三个字“SwiftUI 生命周期”。您认为这是原因,还是实际上只是对实际问题的“噪音”?其次,由于这是一个颜色内核,请尝试一些东西。这是一个例子:*.com/questions/45968561/… 它可能会消除 Playgrounds、UIKit,并指出正在发生的事情。
-
这里不干涉 SwiftUI 的生命周期。我试图暂时从 Metal Kernel 中删除所有代码并返回输入颜色。结果,图像一切正常。我还尝试交换输入颜色。在这里,也完全可以期待一个适当的结果。最后,我尝试使用输入偏移量返回颜色。也是相当预期的行为。最大的问题是寻找错误。我至少可以调用 print() 函数并在控制台中查看该过程。断点也不会被触发。 Github上的源代码:github.com/VKostin8311/MetalKernelsTestApp
标签: ios swift metal core-image xcode13