用 Metal 高效统计 UIImage/CIImage 中有多少个透明像素

Efficiently count how many transparent pixels are in UIImage/CIImage with Metal

我们计算 CIImage/UIImage 中存在多少透明像素的最快方法是什么?

例如:

如果我们谈论效率,我的第一个想法是使用 Metal KernelCIColorKernel 左右,但我不明白如何使用它来输出“计数”。

还有我想到的其他想法:

  1. 用某种平均颜色来计算,“越红”的像素越多?也许某种线性计算取决于图像大小(使用 CIAreaAverage CIFilter?
  2. 一个一个地计算像素并检查 RGB 个值?
  3. 使用 Metal 并行功能,类似于此 post:?
  4. 缩小图片然后数数?或者上面建议的所有其他过程都只是缩放比版本,并且它的倍数取决于计算后的缩小比例?

达到此计数的最快方法是什么?

您要执行的是归约操作,由于其大规模并行特性,它不一定适合 GPU。我建议不要自己为 GPU 编写缩减操作,而是使用 Apple 提供的一些高度优化的内置 API(例如 CIAreaAverage 或相应的 Metal Performance Shaders)。

最有效的方法在一定程度上取决于您的用例,特别是图像的来源(通过 UIImage/CGImage 或 Core Image 管道的结果加载?)以及您在哪里需要结果计数(在 CPU/Swift 侧或作为另一个 Core Image 过滤器的输入?)。
它还取决于像素是否也可以是半透明的(alpha 不是 0.01.0)。

如果图像在 GPU 上 and/or 应该在 GPU 上使用计数,我建议使用 CIAreaAverage。结果的 alpha 值应反映透明像素的百分比。请注意,这仅在现在有半透明像素时才有效。

下一个最佳解决方案可能只是迭代 CPU 上的像素数据。它可能有几百万像素,但操作本身非常快,因此几乎不需要时间。您甚至可以通过将图像分成块并使用 concurrentPerform(...) of DispatchQueue.

来使用多线程

最后一个但可能矫枉过正的解决方案是使用 Accelerate(这会让 @FlexMonkey 高兴):将图像的像素数据加载到 vDSP 缓冲区并使用 sumaverage 方法使用 CPU 的矢量单位计算百分比。

澄清

当我说归约运算“不一定非常适合 GPU”时,我的意思是说它以有效的方式实现相当复杂,而且远不如顺序算法那么简单。

检查像素是否透明可以并行完成,当然,但结果需要收集单个值,这需要多个 GPU 核心在同一内存中读取和写入值。这通常需要一些同步(从而阻碍并行执行)并且由于访问共享或全局内存而导致延迟成本 space。这就是为什么 GPU 的高效聚集算法通常遵循多步基于树的方法。我强烈建议您阅读 NVIDIA 关于该主题的出版物(例如 here and here)。这也是我建议尽可能使用内置 API 的原因,因为 Apple 的 Metal 团队知道如何为他们的硬件优化这些算法。

Apple 的 Metal Shading Language Specification(第 158 页)中还有一个缩减实现示例,它使用 simd_shuffle 内在函数来有效地向下传递树中的中间值。不过,一般原则与上面链接的 NVIDIA 出版物中描述的相同。

要回答你的问题如何做金属,你会使用 device atomic_int

本质上你创建了一个 Int MTLBuffer 并将它传递给你的内核并用 atomic_fetch_add_explicit.

递增它

创建缓冲区一次:

var bristleCounter = 0
counterBuffer = device.makeBuffer(bytes: &bristleCounter, length: MemoryLayout<Int>.size, options: [.storageModeShared])

将计数器重置为 0 并绑定计数器缓冲区:

var z = 0
counterBuffer.contents().copyMemory(from: &z, byteCount: MemoryLayout<Int>.size)
kernelEncoder.setBuffer(counterBuffer, offset: 0, index: 0)

内核:

kernel void myKernel (device atomic_int *counter [[buffer(0)]]) {}

内核中的增量计数器(并获取值):

int newCounterValue = atomic_fetch_add_explicit(counter, 1, memory_order_relaxed);

获取 CPU 侧的计数器:

kernelEncoder.endEncoding()
kernelBuffer.commit()
kernelBuffer.waitUntilCompleted()
    
//Counter from kernel now in counterBuffer
let bufPointer = counterBuffer.contents().load(as: Int.self)
print("Counter: \(bufPointer)")

如果图像包含半透明像素,可以很容易地对其进行预处理,使所有 alpha 低于特定阈值的像素完全透明,否则完全不透明。然后可以应用 CIAreaAverage,正如问题中最初建议的那样,最后可以通过将结果的 alpha 分量乘以图像大小来计算完全不透明像素的近似数量。

对于预处理,我们可以使用像这样的普通 CIColorKernel:

half4 clampAlpha(coreimage::sample_t color) {
    half4 out = half4(color);
    out.a = step(half(0.99), out.a);
    return  out;
}

(选择你喜欢的任何阈值而不是 0.99)

要从 CIAreaAverage 的输出中获取 alpha 分量,我们可以这样做:

        let context = CIContext(options: [.workingColorSpace: NSNull(), .outputColorSpace: NSNull()])
        var color: [Float] = [0, 0, 0, 0]
        context.render(output,
                       toBitmap: &color,
                       rowBytes: MemoryLayout<Float>.size * 4,
                       bounds: CGRect(origin: .zero, size: CGSize(width: 1, height: 1)),
                       format: .RGBAf,
                       colorSpace: nil)

// color[3] contains alpha component of the result

通过这种方法,一切都在 GPU 上完成,同时利用其固有的并行性。

顺便说一句,看看这个应用 https://apps.apple.com/us/app/filter-magic/id1594986951。它可以让你玩每一个 CoreImage 过滤器。