使用 Metal 有效计算 UIImage/CIImage 中有多少透明像素

问题描述

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

例如:

enter image description here

如果我们谈论效率,我的第一个想法是使用 Metal Kernel 使用 CIColorKernel 左右,但我无法理解如何使用它来输出“计数”。

还有我想到的其他想法:

  1. 使用某种平均颜色来计算它,“越红”填充的像素越多?也许某种线性计算取决于图像大小(使用 CIAreaAverage CIFilter?
  2. 一一计算像素并检查 RGB 值?
  3. 使用 Metal 并行功能,类似于这篇文章Counting coloured pixels on the GPU - Theory?
  4. 缩小图像然后计数?还是上面建议的所有其他过程都只是按比例缩小而不是版本,然后再根据计算后的缩小比例将其放大?

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

解决方法

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

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

如果图像在 GPU 上和/或应该在 GPU 上使用计数,我建议使用 CIAreaAverage。结果的 alpha 值应反映透明像素的百分比。请注意,这仅适用于现在有半透明像素的情况。

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

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

澄清

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

>

检查一个像素是否透明可以并行进行,当然可以,但是结果需要聚合单个值,这需要多个GPU核心读取和写入值到同一内存中。这通常需要一些同步(从而阻碍并行执行)并由于访问共享或全局内存空间而导致延迟成本。这就是为什么用于 GPU 的高效收集算法通常遵循基于多步树的方法。我强烈建议您阅读 NVIDIA 关于该主题的出版物(例如 herehere)。这也是我建议尽可能使用内置 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)")