问题描述
计算 CIImage
/UIImage
中存在多少透明像素的最快方法是什么?
例如:
如果我们谈论效率,我的第一个想法是使用 Metal Kernel
使用 CIColorKernel
左右,但我无法理解如何使用它来输出“计数”。
还有我想到的其他想法:
- 使用某种平均颜色来计算它,“越红”填充的像素越多?也许某种线性计算取决于图像大小(使用
CIAreaAverage
CIFilter
? - 一一计算像素并检查
RGB
值? - 使用 Metal 并行功能,类似于这篇文章:Counting coloured pixels on the GPU - Theory?
- 缩小图像然后计数?还是上面建议的所有其他过程都只是按比例缩小而不是版本,然后再根据计算后的缩小比例将其放大?
达到此计数的最快方法是什么?
解决方法
您要执行的是归约操作,由于其大规模并行性质,它不一定非常适合 GPU。我建议不要自己为 GPU 编写归约操作,而是使用 Apple 提供的一些高度优化的内置 API(例如 CIAreaAverage
或相应的 Metal Performance Shaders)。
最有效的方法在某种程度上取决于您的用例,特别是图像的来源(通过 UIImage
/CGImage
加载或核心图像管道的结果?)需要结果计数(在 CPU/Swift 端还是作为另一个 Core Image 过滤器的输入?)。
它还取决于像素是否也可以是半透明的(alpha 不是 0.0
或 1.0
)。
如果图像在 GPU 上和/或应该在 GPU 上使用计数,我建议使用 CIAreaAverage
。结果的 alpha 值应反映透明像素的百分比。请注意,这仅适用于现在有半透明像素的情况。
下一个最佳解决方案可能只是在 CPU 上迭代像素数据。它可能是几百万像素,但操作本身非常快,所以这几乎不需要时间。您甚至可以通过将图像分成块并使用 concurrentPerform(...)
的 DispatchQueue
来使用多线程。
最后一个但可能过大的解决方案是使用加速(这会让@FlexMonkey 高兴):将图像的像素数据加载到 vDSP 缓冲区并使用 sum
或 average
方法进行计算使用 CPU 向量单位的百分比。
澄清
当我说归约运算“不一定适合 GPU”时,我的意思是说以有效的方式实现它相当复杂,而且远不如顺序算法那么简单。
>检查一个像素是否透明可以并行进行,当然可以,但是结果需要聚合成单个值,这需要多个GPU核心读取和写入值到同一内存中。这通常需要一些同步(从而阻碍并行执行)并由于访问共享或全局内存空间而导致延迟成本。这就是为什么用于 GPU 的高效收集算法通常遵循基于多步树的方法。我强烈建议您阅读 NVIDIA 关于该主题的出版物(例如 here 和 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)")