Swift 基于GPU理论的彩色像素计数

Swift 基于GPU理论的彩色像素计数,swift,parallel-processing,gpu,computer-science,Swift,Parallel Processing,Gpu,Computer Science,我有一个128×128像素的图像 它被分解成8乘8的网格 每个网格块包含16×16个像素 要求 我想计算我的图像包含多少黑色像素 直截了当的方法: 我可以通过逐行、逐列地浏览整个图像并检查像素是否为黑色来做到这一点 GPU方式 …但我想知道,如果使用GPU,我是否可以将图像分解为块/块,并计算每个块中的所有像素,然后将结果相加 例如: 如果查看图像的左上角: 第一块‘A1’(A行,第1列)包含一个16×16像素的网格,我通过手动计算知道,有16个像素 第二个块:“A2”(A行,第2列)包含一个1

我有一个128×128像素的图像

它被分解成8乘8的网格

每个网格块包含16×16个像素

要求

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

直截了当的方法:

我可以通过逐行、逐列地浏览整个图像并检查像素是否为黑色来做到这一点

GPU方式

…但我想知道,如果使用GPU,我是否可以将图像分解为块/块,并计算每个块中的所有像素,然后将结果相加

例如:

如果查看图像的左上角:

第一块‘A1’(A行,第1列)包含一个16×16像素的网格,我通过手动计算知道,有16个像素

第二个块:“A2”(A行,第2列)包含一个16×16像素的网格,我通过手动计数知道,有62个像素

此示例的所有其他块均为空

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

推理

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


这里有很多GPU可以做的事情

我不确定您是否在这里寻找算法,但我可以向您指出一个广泛使用的GPU库,它实现了一个高效的计数过程。查看
推力
库中的
计数
功能:

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

下面计算
数据
中等于零的元素数

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>())
模板
结构零像素{
__主机设备布尔运算符()(const T&x)const{return x==0;}
};
推力::计数(data.begin(),data.end(),零像素())
这里有一个工作示例:

您应该编写一个谓词来测试像素是否为黑色(取决于像素对您来说是什么(它可能是RGB三元组,在本例中,谓词应该更详细)

我还将把像素线性化为一个线性的、可编辑的数据结构(但这取决于数据的实际情况)

如果您对直方图方法感兴趣,您可以做的是对图像像素进行排序(使用任何GPU高效算法,或者为什么不使用
sort
推力::sort(…)
)数据,以便将相等的元素分组在一起,然后按键执行缩减
推力::按键缩减

看看这个例子:


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

您的问题:我想知道GPU是否可以/可以做到这一点

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

  • 经纱尺寸:32(16x2)
  • 每个块的最大线程数:1024(8x128)(8x16)
  • 每个多处理器的最大线程数:2048…等
您可以尝试多种块/线程配置以获得最佳性能

<强>程序:一般来说,使用GPU意味着将CPU内存中的数据复制到GPU内存,然后在GPU上执行计算,最后将结果复制到CPU进行进一步计算。一个重要的考虑是所有的数据传输都是通过CPU和GPU之间的PCI-E链路完成的,THA。与两者相比,t非常慢

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

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

  • 分治递归算法,或

  • 在多个CPU内核中并行计算


  • 事实上,通用GPU(例如A8上的Apple设备中的GPU)不仅能够而且旨在解决此类并行数据处理问题

    苹果在他们的平台中引入了使用金属的数据并行处理,通过一些简单的代码,你可以使用GPU解决像你这样的问题。即使这也可以通过其他框架来完成,我也会提供一些金属+Swift案例的代码作为概念证明

    以下是在OSX Sierra上作为Swift命令行工具运行的,它是使用Xcode 9构建的(是的,我知道它是测试版)

    作为
    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!
    
    我用这个问题学习了一些关于金属和数据并行计算的知识,所以大部分代码都是作为在线文章和编辑文章中的样板文件使用的。请花时间访问下面提到的源代码以获得更多的示例。此外,对于这个特定的问题,代码几乎是硬编码的,但您不应该有太多问题适应它

    资料来源:


    这似乎是直方图的特例,GPU非常适合这种情况。请尝试搜索“GPU”+“hsitogram”
    #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);
        }
    }