Core Image with Metal Kernel: Странное поведение пользовательского фильтра

ios

#1

Добрый вечер товарищи.

Впервые столкнулся с написанием пользовательских ядер Metal для создания фильтра CIFilter.

Задача казалась не сложной. Нужно сделать фильтр для регулировки оттенка, яркости и насыщенности цветов на изображении, ограниченные диапазоном hue +/- 22.5 градусов. Как в таких приложениях как Лайтрум регулирование смещения цветов.

Алгоритм прост до безобразия:

  1. Передаю в функцию исходный цвет пикселя и значения для диапазона и смещения оттенка, насыщенности и яркости;
  2. Внутри функции преобразую цвет из схемы RGB в схему HSL;
  3. Проверяю, попал ли оттенок в целевой диапазон;
  4. Если не попал - смещения не применяю, если попал - к полученным при преобразовании hue, saturation и luminance прибавляю величины смещения;
  5. Преобразую цвет пикселя обратно в схему RGB;
  6. Возвращаю результат.

Получился замечательный алгоритм, который успешно и без всяких проблем отработан в 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
        }
    }

Самое унылое - даже в консоль ничего не вывести. Непонятно как дебажить
Буду благодарен за любые подсказки.