【问题标题】:Efficiently count how many transparent pixels are in UIImage/CIImage with Metal使用 Metal 有效计算 UIImage/CIImage 中有多少透明像素
【发布时间】:2021-09-04 07:43:19
【问题描述】:

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

例如:

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

还有我想到的其他想法:

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

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

【问题讨论】:

    标签: ios swift metal core-image metal-performance-shaders


    【解决方案1】:

    要回答你的问题如何做金属,你会使用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)")
    

    【讨论】:

    • 这样做的问题是,数百个 GPU 内核中的每一个都需要从全局地址空间读取和写入 相同的值。即使使用 atomic 内在函数,您仍然 (a) 阻止任何并行执行,因为一次只有一个内核可以访问该值,并且 (b) 在访问全局内存时会导致大量延迟。
    • 你想参加比赛吗?在任何现代芯片上,即使是大图像,它也非常快。该问题最初询问如何在 Metal 中实现它,因此即使您认为您的方法更快(我假设这只是一个猜测),它也是相关的。
    • 金属也是最灵活的方法。目前可能不需要额外的功能,但是通过这种方法,根据需要实现额外的功能将非常简单。它是无限可定制的。
    • 我无意冒犯你,对不起!问题是询问最有效的解决方案,所以我认为花 20 分钟列出备选方案并讨论它们的优缺点是正确的。如果需要,我可能可以投入更多时间来编写示例代码,但正如我在回答中所说,理想情况下,它取决于周围的用例(数据来自何处以及使用结果的位置)。
    • 你是对的,你提供的解决方案肯定是有效的,编译器和调度程序可能有助于使其运行得相当快。但是,我仍然认为这不是一个好的解决方案,因为它违反了多个 GPU 编程最佳实践。
    【解决方案2】:

    您要执行的是归约操作,由于其大规模并行性质,它不一定非常适合 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 来使用多线程。

    最后一个但可能有点过分的解决方案是使用 Accelerate(这会让@FlexMonkey 高兴):将图像的像素数据加载到 vDSP 缓冲区中,然后使用 sumaverage 方法计算百分比,使用CPU 的向量单元。

    澄清

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

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

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

    【讨论】:

    • 计算像素确实是一个大规模的并行操作,所以我不明白你为什么说它不适合 GPU。事实上,几乎任何基于像素的操作都适用于 GPU,因为它可以分解为单个像素或内核。
    • 你说得对,我的措辞有点太模糊了。我在回答中添加了说明。
    • 另外,你修改了你的答案,但是,它开始陈述明显的虚假信息。
    • 我很高兴你找到了一个快速的解决方案,Jeshua。但我仍然不认为我关于归约操作与 GPU 这样的 SIMD 设备本质上不匹配的说法是错误的。肯定有办法以有效的方式实现它(请参阅我添加到我的答案中的 Apple 示例),但这并不简单。这就是为什么我建议尽可能使用内置的高级 API 来执行此操作。
    • 如果它对实时应用程序足够好,那么它是 GPU 的完美匹配,恕我直言。如果你有源代码,我可以比较性能,否则只是理论。
    【解决方案3】:

    如果图像包含半透明像素,则可以轻松地对其进行预处理,以使 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 过滤器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-12-13
      • 2019-01-26
      • 2015-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多