使用 vImageMatrixMultiply_ARGB8888 将颜色矩阵滤镜应用于 UIImage,就像 CIColorMatrix 一样

Apply color matrix filter to UIImage using vImageMatrixMultiply_ARGB8888 like does CIColorMatrix

我正在尝试构建一个 swift 函数,以使用 Accellerate 框架的 vImageMatrixMultiply_ARGB8888 将颜色矩阵滤镜应用于图像。 我以前能够使用 CIFilter CIColorMatrix.

将颜色矩阵应用于图像
     let ciimage = CIImage(image: image)!
     let colorMatrix = CIFilter(name: "CIColorMatrix")!
     colorMatrix.setDefaults()
     colorMatrix.setValue(ciimage, forKey: "inputImage")
     colorMatrix.setValue(CIVector(x: 1.9692307692307693, y: 0, z: 0, w: 0), forKey: "inputRVector")
     colorMatrix.setValue(CIVector(x: 0, y: 2.226086956521739, z: 0, w: 0), forKey: "inputGVector")
     colorMatrix.setValue(CIVector(x: 0, y: 0, z: 2.585858585858586, w: 0), forKey: "inputBVector")
     colorMatrix.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")
     return UIImage(ciImage: colorMatrix.outputImage!)

RotatioMatrix used in vImageMatrixMultiply_ARGB8888 use same values of input vectors of CIFilter.

rotationMatrix = [1.9692307692307693, 0, 0, 0
                  0, 2.226086956521739, 0, 0,
                  0, 0, 2.585858585858586, 0,
                  0, 0, 0, 1].map {
                     return Int16(Float([=12=]) * Float(divisor)) // divisor = 256
                  }

以下代码不会产生与上面 CIFilter 中相同的结果。

private func rgbAdjustmentWithMatrix(image sourceImage: UIImage, rotationMatrix: [Int16]) -> UIImage? {

    guard
        let cgImage = sourceImage.cgImage,
        let sourceImageFormat = vImage_CGImageFormat(cgImage: cgImage),
        let rgbDestinationImageFormat = vImage_CGImageFormat(
            bitsPerComponent: 8,
            bitsPerPixel: 32,
            colorSpace: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue),
            renderingIntent: .defaultIntent) else {
                print("Unable to initialize cgImage or colorSpace.")
                return nil
    }

    guard
        let sourceBuffer = try? vImage_Buffer(cgImage: cgImage),
        var rgbDestinationBuffer = try? vImage_Buffer(width: Int(sourceBuffer.width),
            height: Int(sourceBuffer.height),
            bitsPerPixel: rgbDestinationImageFormat.bitsPerPixel) else {
                print("Unable to initialize source buffer or destination buffer.")
                return nil
    }

    defer {
        sourceBuffer.free()
        rgbDestinationBuffer.free()
    }

    do {
        let toRgbConverter = try vImageConverter.make(sourceFormat: sourceImageFormat, destinationFormat: rgbDestinationImageFormat)
            try toRgbConverter.convert(source: sourceBuffer, destination: &rgbDestinationBuffer)
    } catch {
        print("Unable to initialize converter or unable to convert.")
        return nil
    }

    guard var resultBuffer = try? vImage_Buffer(width: Int(sourceBuffer.width),
            height: Int(sourceBuffer.height),
            bitsPerPixel: rgbDestinationImageFormat.bitsPerPixel) else {
                print("Unable to initialize result buffer.")
                return nil
    }

    defer {
        resultBuffer.free()
    }
    var error: vImage_Error

    let divisor: Int32 = 256

    let preBias = [Int16](repeating: -256, count: 4)
    let postBias = [Int32](repeating: 256 * divisor, count: 4)

    error = vImageMatrixMultiply_ARGB8888(&rgbDestinationBuffer,
                                  &resultBuffer,
                                  rotationMatrix,
                                  divisor,
                                  preBias,
                                  postBias,
                                  0)
    guard error == kvImageNoError else { return nil }

    if let cgImage = try? resultBuffer.createCGImage(format: rgbDestinationImageFormat) {
        return UIImage(cgImage: cgImage)
    } else {
        return nil
    } 

}

可能是颜色 space 的问题?或者 vImageMatrixMultiply_ARGB8888 输入矩阵与 CIFilter 输入矩阵不同?

更新

因此,看起来 vImageMatrixMultiply_ARGB8888 需要以下顺序的值:

let rotationMatrix = [
    1, 0,                  0,                 0,                 // A
    0, 1.9692307692307693, 0,                 0,                 // R
    0, 0,                  2.226086956521739, 0,                 // G
    0, 0,                  0,                 2.585858585858586  // B
    ].map { return Int16([=10=] * Float(divisor)) } // divisor = 0x1000

这与隐含 BGRA 的 Apple 示例代码不一致。可能该矩阵原本打算与不同的 pixelBuffer 格式一起使用 - 或者其他一些问题。

我相信您遇到的另一个问题 运行 是 CIFilter 使用默认的 CIContext。当我 运行 得到一个 linearSRGB workingColorSpace.

let context = CIContext()
print(context.workingColorSpace ?? "")

输出:

<CGColorSpace 0x7fe4abd06d30> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB Linear)

因此,您可以将 CGColorSpaceCreateDeviceRGB() 指定为 CIFilter 的 CIContext workingColorSpace,或者在 vImage 用法中将 rgbDestinationImageFormat colorSpace 设置为 .linearSRGB。在这个和更正后的矩阵顺序之间,我相信你的输出图像现在​​会匹配。

// error handling removed for brevity
let ciImage = CIImage(cgImage: cgImage)
let colorMatrix = CIFilter(name: "CIColorMatrix")!
colorMatrix.setDefaults()
colorMatrix.setValue(ciImage, forKey: "inputImage")
colorMatrix.setValue(CIVector(x: 1.9692307692307693, y: 0, z: 0, w: 0), forKey: "inputRVector")
colorMatrix.setValue(CIVector(x: 0, y: 2.226086956521739, z: 0, w: 0), forKey: "inputGVector")
colorMatrix.setValue(CIVector(x: 0, y: 0, z: 2.585858585858586, w: 0), forKey: "inputBVector")
colorMatrix.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")
let context = CIContext(options: [.workingColorSpace : CGColorSpaceCreateDeviceRGB()]) // without this, colorSpace defaults to linearSRGB
return context.createCGImage(colorMatrix.outputImage!, from: rect)! 

在这种情况下,可能不需要预先和 post 偏差,除非您有其他原因这样做。

最后,您可能会受益于更大的除数。虽然像素数据是每通道 8 位,但累加器是 32 位,矩阵值是 16 位。大多数参考代码使用 0x1000 (4096) 的除数。

func rgbAdjustmentWithMatrix(image sourceImage: NSImage, rotationMatrix: [Int16]) -> CGImage?  {

    var rect = CGRect(x: 0, y: 0, width: sourceImage.size.width, height: sourceImage.size.height)

    guard
        let cgImage = sourceImage.cgImage(forProposedRect: &rect, context: nil, hints: nil),
        let sourceImageFormat = vImage_CGImageFormat(cgImage: cgImage),
        let rgbDestinationImageFormat = vImage_CGImageFormat(
            bitsPerComponent: 8,
            bitsPerPixel: 32,
            colorSpace: CGColorSpaceCreateDeviceRGB(),
            // can match CIFilter default with this:
            // colorSpace: CGColorSpace(name: CGColorSpace.linearSRGB)!,
            bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue),
            renderingIntent: .defaultIntent)
        else {
                print("Unable to initialize cgImage or colorSpace.")
                return nil
    }

    guard
        var sourceBuffer = try? vImage_Buffer(cgImage: cgImage),
        var rgbDestinationBuffer = try? vImage_Buffer(width: Int(sourceBuffer.width),
                                                      height: Int(sourceBuffer.height),
                                                    bitsPerPixel: rgbDestinationImageFormat.bitsPerPixel)
        else {
            fatalError("Error initializing source and destination buffers.")
    }

    defer {
        sourceBuffer.free()
        rgbDestinationBuffer.free()
    }

    do {
        let toRgbConverter = try vImageConverter.make(sourceFormat: sourceImageFormat, destinationFormat: rgbDestinationImageFormat)
        try toRgbConverter.convert(source: sourceBuffer, destination: &rgbDestinationBuffer)
    } catch {
        fatalError(error.localizedDescription)
    }

    let divisor: Int32 = 0x1000 // matrix values are 16 bit and accumulator is 32 bit
                                // 4096 gives us decent overhead for the matrix operation
                                // 12-bit color

    let preBias: [Int16] = [0, 0, 0, 0]  // or simply pass nil
    let postBias: [Int32] = [0, 0, 0, 0] // or simply pass nil

    let error = vImageMatrixMultiply_ARGB8888(&rgbDestinationBuffer,
                                              &rgbDestinationBuffer,
                                              rotationMatrix,
                                              divisor,
                                              preBias,
                                              postBias,
                                              vImage_Flags(kvImageNoFlags))

    if error != kvImageNoError { print("Error: \(error)") }

    let result = try? rgbDestinationBuffer.createCGImage(format: rgbDestinationImageFormat)
    return result

}


上一个答案

查看您的代码和输出图像,我相信您将红色和蓝色系数颠倒了。如果是这样,您将像这样填充 vImage 矩阵:

rotationMatrix = [2.585858585858586, 0, 0, 0   // b
                  0, 2.226086956521739, 0, 0,  // g
                  0, 0, 1.9692307692307693, 0, // r
                  0, 0, 0, 1].map {            // a
                     return Int16(Float([=15=]) * Float(divisor)) // divisor = 256
                  }

这与 desaturating a selected portion of an image 的 Apple vImage 示例代码一致。

    let desaturationMatrix = [
        0.0722, 0.0722, 0.0722, 0,
        0.7152, 0.7152, 0.7152, 0,
        0.2126, 0.2126, 0.2126, 0,
        0,      0,      0,      1
        ].map {
            return Int16([=16=] * Float(divisor))
    }

请注意,coefficients for Rec709 到 Luma 的转换为:

Y = R * 0.2126 + G * 0.7152 + B * 0.0722