Добрый вечер товарищи.
Впервые столкнулся с написанием пользовательских ядер Metal для создания фильтра CIFilter.
Задача казалась не сложной. Нужно сделать фильтр для регулировки оттенка, яркости и насыщенности цветов на изображении, ограниченные диапазоном hue +/- 22.5 градусов. Как в таких приложениях как Лайтрум регулирование смещения цветов.
Алгоритм прост до безобразия:
- Передаю в функцию исходный цвет пикселя и значения для диапазона и смещения оттенка, насыщенности и яркости;
- Внутри функции преобразую цвет из схемы RGB в схему HSL;
- Проверяю, попал ли оттенок в целевой диапазон;
- Если не попал - смещения не применяю, если попал - к полученным при преобразовании hue, saturation и luminance прибавляю величины смещения;
- Преобразую цвет пикселя обратно в схему 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 {
// Определяю максимальные и минимальные компоненты цвета
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
// Преобразую в 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
}
// Задаю границы диапазона смещаемого оттенка
let minHue: Float = center - 22.5/(360)
let maxHue: Float = center + 22.5/(360)
// Применяю смещения для оттенка, насыщенности и яркости
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 )
// Преобразую цвет обратно в 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
// Вычисляю красный
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 }
// Вычисляю зелёный
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 }
// Вычисляю синий
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)
// Для визуального восприятия входного цвета
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)
// Для визуального восприятия выходного цвета
let adjustedColor = UIColor(red: CGFloat(rgb.r), green: CGFloat(rgb.g), blue: CGFloat(rgb.b), alpha: 1.0)
Эта же функция, переписанная для Metal kernel в проекте Xcode дает совершенно неожиданный результат.
Изображение после него становится черно-белым. При этом изменение входных параметров слайдерами меняют и само изображение. Только тоже странно: оно покрывается мелкими чёрными или белыми квадратами.
Вот так выглядит код в Metal kernel:
#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) {
// 1: Convert pixel color from RGB to HSL
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) ;
// Apply offsets to Hue, Saturation, Luminance
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;
// Calculate 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; }
// Calculate 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; }
// Calculate 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
}
}
Самое унылое - даже в консоль ничего не вывести. Непонятно как дебажить
Буду благодарен за любые подсказки.