计算GPU上的彩色像素 - 理论

时间:2017-06-03 16:33:52

标签: swift parallel-processing gpu computer-science

我有一个128 x 128像素的图像。

它分为8乘8格。

每个网格块包含16 x 16像素。

要求

我想计算我的图片包含多少个黑色像素。

直截了当的方式:

可以通过逐行,逐列,整个图像并检查像素是否为黑色来做到这一点。

GPU方式

...但我想知道如果使用GPU,我可以将图像分解为块/块并计算每个块中的所有像素,然后对结果求和。

例如:

如果你看一下图片的左上角:

第一个块,'A1'(第A行,第1列)包含一个16乘16像素的网格,我知道通过手动计算它们,有16个黑色像素。

第二个块:'A2',(第A行,第2列)包含16 x 16像素的网格,我知道通过手动计算它们,有62个黑色像素。

此示例的所有其他块都为空白/空。

如果我通过我的程序运行我的图像,我应该得到答案:16 + 62 = 78黑色像素。

Grid Diagram

推理

据我所知,GPU可以并行运行大量数据,有效地在分布在多个GPU线程上的大量数据上运行一个小程序。 我不担心速度/性能,我只是想知道GPU是否能够/可以做到这一点?

Whole Image

3 个答案:

答案 0 :(得分:3)

实际上,通用GPU(例如A8上的Apple设备中的那些GPU)不仅能够,而且还能够解决此类并行数据处理问题。

Apple在他们的平台上引入了使用Metal进行数据并行处理,并且使用一些简单的代码可以解决像你这样使用GPU的问题。即使这也可以使用其他框架完成,我将Metal + Swift案例的一些代码作为概念证明。

以下在OS X Sierra上作为Swift命令行工具运行,并使用Xcode 9构建(是的,我知道它的测试版)。您可以从我的github repo获取完整项目。

作为main.swift

import Foundation
import Metal
import CoreGraphics
import AppKit

guard FileManager.default.fileExists(atPath: "./testImage.png") else {
    print("./testImage.png does not exist")
    exit(1)
}

let url = URL(fileURLWithPath: "./testImage.png")
let imageData = try Data(contentsOf: url)

guard let image = NSImage(data: imageData),
    let imageRef = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
    print("Failed to load image data")
    exit(1)
}

let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * imageRef.width

var rawData = [UInt8](repeating: 0, count: Int(bytesPerRow * imageRef.height))

let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue).union(.byteOrder32Big)
let colorSpace = CGColorSpaceCreateDeviceRGB()

let context = CGContext(data: &rawData,
                        width: imageRef.width,
                        height: imageRef.height,
                        bitsPerComponent: 8,
                        bytesPerRow: bytesPerRow,
                        space: colorSpace,
                        bitmapInfo: bitmapInfo.rawValue)

let fullRect = CGRect(x: 0, y: 0, width: CGFloat(imageRef.width), height: CGFloat(imageRef.height))
context?.draw(imageRef, in: fullRect, byTiling: false)

// Get access to iPhone or iPad GPU
guard let device = MTLCreateSystemDefaultDevice() else {
    exit(1)
}

let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
    pixelFormat: .rgba8Unorm,
    width: Int(imageRef.width),
    height: Int(imageRef.height),
    mipmapped: true)

let texture = device.makeTexture(descriptor: textureDescriptor)

let region = MTLRegionMake2D(0, 0, Int(imageRef.width), Int(imageRef.height))
texture.replace(region: region, mipmapLevel: 0, withBytes: &rawData, bytesPerRow: Int(bytesPerRow))

// Queue to handle an ordered list of command buffers
let commandQueue = device.makeCommandQueue()

// Buffer for storing encoded commands that are sent to GPU
let commandBuffer = commandQueue.makeCommandBuffer()

// Access to Metal functions that are stored in Shaders.metal file, e.g. sigmoid()
guard let defaultLibrary = device.makeDefaultLibrary() else {
    print("Failed to create default metal shader library")
    exit(1)
}

// Encoder for GPU commands
let computeCommandEncoder = commandBuffer.makeComputeCommandEncoder()

// hardcoded to 16 for now (recommendation: read about threadExecutionWidth)
var threadsPerGroup = MTLSize(width:16, height:16, depth:1)
var numThreadgroups = MTLSizeMake(texture.width / threadsPerGroup.width,
                                  texture.height / threadsPerGroup.height,
                                  1);

// b. set up a compute pipeline with Sigmoid function and add it to encoder
let countBlackProgram = defaultLibrary.makeFunction(name: "countBlack")
let computePipelineState = try device.makeComputePipelineState(function: countBlackProgram!)
computeCommandEncoder.setComputePipelineState(computePipelineState)


// set the input texture for the countBlack() function, e.g. inArray
// atIndex: 0 here corresponds to texture(0) in the countBlack() function
computeCommandEncoder.setTexture(texture, index: 0)

// create the output vector for the countBlack() function, e.g. counter
// atIndex: 1 here corresponds to buffer(0) in the Sigmoid function
var counterBuffer = device.makeBuffer(length: MemoryLayout<UInt32>.size,
                                        options: .storageModeShared)
computeCommandEncoder.setBuffer(counterBuffer, offset: 0, index: 0)

computeCommandEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)

computeCommandEncoder.endEncoding()
commandBuffer.commit()
commandBuffer.waitUntilCompleted()

// a. Get GPU data
// outVectorBuffer.contents() returns UnsafeMutablePointer roughly equivalent to char* in C
var data = NSData(bytesNoCopy: counterBuffer.contents(),
                  length: MemoryLayout<UInt32>.size,
                  freeWhenDone: false)
// b. prepare Swift array large enough to receive data from GPU
var finalResultArray = [UInt32](repeating: 0, count: 1)

// c. get data from GPU into Swift array
data.getBytes(&finalResultArray, length: MemoryLayout<UInt>.size)

print("Found \(finalResultArray[0]) non-white pixels")

// d. YOU'RE ALL SET!

此外,在Shaders.metal

#include <metal_stdlib>
using namespace metal;

kernel void
countBlack(texture2d<float, access::read> inArray [[texture(0)]],
           volatile device uint *counter [[buffer(0)]],
           uint2 gid [[thread_position_in_grid]]) {

    // Atomic as we need to sync between threadgroups
    device atomic_uint *atomicBuffer = (device atomic_uint *)counter;
    float3 inColor = inArray.read(gid).rgb;
    if(inColor.r != 1.0 || inColor.g != 1.0 || inColor.b != 1.0) {
        atomic_fetch_add_explicit(atomicBuffer, 1, memory_order_relaxed);
    }
}

我使用这个问题来学习一些关于金属和数据并行计算的知识,因此大部分代码都被用作在线文章和编辑的样板。请花些时间访问下面提到的来源以获取更多示例。此外,代码对于这个特定问题几乎是硬编码的,但是你不应该在调整它时遇到很多麻烦。

来源:

http://flexmonkey.blogspot.com.ar/2016/05/histogram-equalisation-with-metal.html

http://metalbyexample.com/introduction-to-compute/

http://memkite.com/blog/2014/12/15/data-parallel-programming-with-metal-and-swift-for-iphoneipad-gpu/

答案 1 :(得分:2)

GPU可以在这里做很多事情。

我不确定您是否在寻找算法,但我可以指向一个广泛使用的GPU库,它实现了一个有效的计数过程。请查看count库中的thrust函数:https://thrust.github.io/doc/group__counting.html

它将谓词函数作为输入。它计算满足谓词的输入数据的出现次数。

以下计算data中等于零的元素数。

template <typename T>
struct zero_pixel{
  __host__ __device__ bool operator()(const T &x) const {return x == 0;}
};
thrust::count_if(data.begin(), data.end(), zero_pixel<T>())

这里有一个工作示例:https://github.com/thrust/thrust/blob/master/testing/count.cu

您应该编写一个谓词,用于测试像素是否为黑色(取决于像素对您来说是什么(它可能是RGB三元组,在这种情况下,谓词应该更复杂一点)。

我还会将像素线性化为线性和可迭代的数据结构(但这取决于您的数据实际是什么)。

如果您对直方图方法感兴趣,您可以做的是对图像的像素进行排序(使用任何GPU高效算法,或者为什么不thrust实现sort,{{1} })数据,以便将相同的元素组合在一起,然后按键 thrust::sort(...)执行减少。

看一下这个例子:https://github.com/thrust/thrust/blob/master/examples/histogram.cu

请注意,直方图方法成本更高,因为它解决了更大的问题(计算所有唯一元素的出现次数)。

答案 2 :(得分:2)

您的问题: 我想知道这是GPU可以/可以做的事情吗?

答案:是的,GPU可以处理您的计算。所有数字看起来都非常友好:

  • 经纱尺寸:32(16x2)
  • 每个块的最大线程数:1024(8x128)(8x8x16)
  • 每个多处理器的最大线程数:2048 ......等

您可以尝试多种块/线程配置以获得最佳性能。

程序:通常,使用GPU意味着您将数据从CPU内存复制到GPU内存,然后在GPU上执行计算,最后将结果复制回CPU以进一步计算。需要考虑的一个重要思考是,所有这些数据传输都是通过CPU和GPU之间的PCI-e链路完成的,与两者相比,这是非常缓慢的。

我的观点:在这种情况下,在将图像复制到GPU内存所需的时间内,即使您使用了单独的CPU计算线程,也会得到结果。这是因为您的过程不是数学/计算密集型的。您只是读取数据并将其与黑色进行比较,然后添加累加器或计数器以获得总数(这本身会引发您必须解决的竞争条件)。

我的建议:如果在分析(分析)整个程序之后,您认为这个获取黑色像素数的例程是一个真正的瓶颈,请尝试:

  1. 分而治之的递归算法,或

  2. 在多个CPU内核中并行计算。