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

3

如何快速计算CIImage/UIImage中存在多少个透明像素?

例如:

enter image description here

如果我们谈论效率,我认为最好使用Metal Kernel,可以使用CIColorKernel来输出“count”,但我不知道如何使用它。
我还有其他想法:
1. 使用某种平均颜色计算它,“越红”则填充的像素越多?也许根据图像大小进行某种线性计算(使用CIAreaAverage CIFilter)? 2. 逐个计算像素并检查RGB值? 3. 利用Metal的并行能力,类似于这篇文章:Counting coloured pixels on the GPU - Theory? 4. 缩小图像然后再计算?或者只是使用缩小版本进行所有建议的其他过程,然后根据计算后的缩放比例将其乘回去? 什么是实现此计数的最快方法?
3个回答

4
为了回答你如何使用 metal 进行操作的问题,你需要使用 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)")

2
问题在于数百个GPU核心都需要从全局地址空间读取和写入相同的值。即使使用原子内置函数,仍然会(a)阻塞任何并行执行,因为一次只有一个核心可以访问该值,并且(b)在访问全局内存时会导致很多延迟。 - Frank Rupprecht
1
你想比赛吗?在任何现代芯片上,即使处理大型图像也足够快。这个问题最初是关于如何在Metal中实现的,因此它仍然相关,即使你认为自己的方法更快(我假设这只是一个猜测)。 - Jeshua Lacock
1
金属也是最灵活的方法。目前可能不需要额外的功能,但采用这种方法,根据需要实现其他能力将非常简单。它具有无限的可定制性。 - Jeshua Lacock
1
我并不是有意冒犯你,非常抱歉!这个问题在询问最有效的解决方案,所以我认为花费20分钟列出备选方案并讨论它们的优缺点是正确的。如果需要,我可能会投入更多时间编写示例代码,但正如我在答案中所说,这最好取决于周围的用例(数据来自哪里以及结果将用于何处)。 - Frank Rupprecht
1
而且你是对的,你提供的解决方案肯定是可行的,编译器和调度程序可能有助于使其运行得相当快。然而,我仍然认为这不是一个好的解决方案,因为它违反了多个GPU编程最佳实践。 - Frank Rupprecht
1
没有提供性能比较或源代码的情况下,这只是所有理论。在实践中,根据我的经验,使用Metal来执行此类任务对于实时应用程序来说已经足够快了。我并不是个人地接受您的负评,而是这个问题要求如何在Metal中完成,并且有Metal标签,我提供了完整和完全可工作的代码,并不应该在SO精神的支持下被否定。 - Jeshua Lacock

3
你想要执行的是减少操作,这种操作不一定适合于GPU,因为它具有大规模并行的特性。我建议不要自己为GPU编写减少操作,而是使用苹果提供的一些高度优化的内置API(如CIAreaAverage或相应的Metal Performance Shaders)。
最有效的方法取决于您的用例,特别是图像来自哪里(通过UIImage/CGImage加载还是作为Core Image管道的结果?)以及您需要结果计数的位置(在CPU/Swift侧还是作为另一个Core Image过滤器的输入?)。它还取决于像素是否也可以是半透明的(alpha不为0.0或1.0)。
如果图像在GPU上和/或计数应在GPU上使用,则建议使用CIAreaAverage。结果的Alpha值应反映透明像素的百分比。请注意,这仅在没有半透明像素时才有效。
下一个最好的解决方案可能只是在CPU上迭代像素数据。它可能有几百万个像素,但操作本身非常快,因此这应该不需要花费太多时间。您甚至可以通过将图像分成块并使用DispatchQueueconcurrentPerform(...)来使用多线程。
最后一种解决方案可能过于复杂,但也可以使用加速器(这会让@FlexMonkey感到高兴):将图像的像素数据加载到vDSP缓冲区中,并使用sumaverage方法使用CPU的向量单元来计算百分比。
澄清: 当我说缩减操作“不一定适合GPU”时,我的意思是实现起来相当复杂,远不如顺序算法直接。
像检查一个像素是否透明这样的操作可以并行完成,但结果需要被“收集”到一个单一的值中,这需要多个GPU核心读写同一内存。这通常需要一些同步(从而阻碍并行执行)并且由于访问共享或全局内存空间而产生延迟成本。这就是为什么GPU的有效聚合算法通常采用多步基于树的方法。我强烈推荐阅读NVIDIA关于该主题的出版物(例如此处此处)。这也是为什么我建议在可能的情况下使用内置API,因为苹果的Metal团队知道如何最好地优化这些算法以适应他们的硬件。

苹果的Metal Shading Language Specification(第158页)中还有一个示例归约实现,它使用simd_shuffle内在函数来有效地向下传递中间值。总体原则与上述链接的NVIDIA出版物描述的相同。


1
像素计数确实是一项大规模并行操作,因此我不明白为什么您认为它不适合GPU。事实上,几乎任何基于像素的操作都适合GPU,因为它可以被分解成单个像素或内核。 - Jeshua Lacock
1
你说得对,我的措辞有点模糊。我在我的回答中添加了一个澄清。 - Frank Rupprecht
1
此外,您修改了您的答案,但是它一开始就陈述了明显错误的信息。 - Jeshua Lacock
1
我很高兴你找到了一个快速的解决方案,Jeshua。但我仍然认为我的观点是正确的,即归约操作并不是SIMD设备(如GPU)的天然良伴。当然,有办法以高效的方式实现它(可以参考我在答案中添加的苹果公司的示例),但这并不是一件容易的事情。这就是为什么我建议尽可能使用内置的高级API来完成此操作。 - Frank Rupprecht
1
如果它足够适用于实时应用程序,那么在我看来,它非常适合GPU。如果您有源代码,我可以比较性能,否则这只是理论。 - Jeshua Lacock
显示剩余4条评论

0
如果图像包含半透明像素,则可以轻松预处理以使所有 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滤镜。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接