在忙忙碌碌许久之后,终于有时间写 "CUDA随笔" 系列的第二集了!

这次给大家带来了一个图像处理的应用例子:计算图片的直方图. 虽然使用CUDA可以很轻松地在性能上超越CPU,如能恰当地使用CUDA优化小技巧,那运算效率便可更上一层楼。为了记录我优化这段代码的历程,这篇短文会介绍三个版本的直方图计算实现,每一个版本均比前一个版本有些许提高,望能给大家带去一些CUDA优化的思路。

所有代码均可在我的github repo上找到:

https://github.com/YifanJiangPolyU/cuda_playground/tree/master/cuda_hist​github.com

图像直方图

图像直方图是统计图片像素值分布的直方图。这一直方图在图像处理中有许多重要应用,比如直方图均衡化提升对比度(histogram equalization),边缘检测,确定二值化阈值等。维基百科给出了很好的介绍:

https://zh.wikipedia.org/wiki/%E5%BD%B1%E5%83%8F%E7%9B%B4%E6%96%B9%E5%9C%96​zh.wikipedia.org

基本思路

GPU做图像处理的一个基本套路是“分而治之”:先将图片切分称许多小块,每一个小块会被分配到CUDA的一个block,由block计算这一小块的直方图(sub histogram)。运算结束后,再将所有的sub histogram相加,得到整张图片的直方图。

直方图计算基本思路

关于实验

实验平台搭载NVidia Quadro M2200,4G显存。

V0 最直接的实现

第一个版本最简单直接地实现了直方图计算:用一维blocks在x方向覆盖整张图片,在thread中以循环遍历y方向。刀法如下图所示:

每一个block的dimension均为 512x1x1。至于为什么是512:一般block里thread的数量最好为32的整数倍(由于N卡CUDA核定义一个warp为32thread),而对于大部分应用,128~1024都能有不错的效果。至于具体用哪个,就需要实验测试了。

第一个重要函数 subhist 计算每个图片小块的 sub histogram。第二个函数 sumSubHistograms 将所有小块的 sub histogram相加,得到全局直方图。又有subhist耗时最多且较为复杂,在这里只优化 subhist 函数,对 sumSubHistograms 则不多赘述。

__global__ void subhist(uint8_t *image,         // input image (8bit gray scale)size_t w,               // input image widthsize_t h,               // input image heightsize_t memPitch,        // input memory pitch (see cudaMallocPitch)uint32_t *subHistogram, // stores sub-histogramssize_t nbins,           // number of possible pixel values (256 for 8-bit)size_t nsubhist,        // number of sub-histogramssize_t subHistogramPitch) // sub-histogram memory pitch (see cudaMallocPitch)
{uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x;uint32_t subHistId = blockIdx.x;if(idx<w){for(int i=0; i<h; i++){uint8_t pv = image[i*memPitch + idx];atomicAdd(&subHistogram[subHistId*subHistogramPitch + pv], 1);}}
}__global__ void sumSubHistograms(uint32_t *subHistogram,size_t nbins,size_t nsubhist,size_t subHistogramPitch,uint32_t *histogram)
{uint32_t sid = blockIdx.x * blockDim.x + threadIdx.x;uint32_t step = blockDim.x * gridDim.x;for(int i=sid; i<nbins; i+=step){uint32_t sum = 0;for(int j=0; j<nsubhist; j++){sum += subHistogram[j*subHistogramPitch + i];}histogram[i] = sum;}
}

运行表现如下:

V1 Shared Memory

注意到在V1中,我们对 subHistogram 这个存在于device memory中的数组进行了大量随机位置的 atomicAdd 操作。由于随机读写device memory会产生很大的延迟,我们可以考虑先将 sub-histogram 缓存在 shared memory 之中,利用shared memory的低延时来提升读写性能。当 sub-histogram 的计算结束之后,再将结果由shared memory写入device memory (此时写入为顺序写入,相邻thread写入device memory的相邻地址,效率很高)。

__global__ void subhist(uint8_t *image,size_t w,size_t h,size_t memPitch,uint32_t *subHistogram,size_t nbins,size_t nsubhist,size_t subHistogramPitch)
{// shared memory (smem) 被同一个block中的所有thread共享。// 每个block的smem大小在kernel launch 的时候确定。// kernel<<<GridDim, BlockDim, smem_size>>>(...)extern __shared__ uint32_t localHist[];uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x;uint32_t sid = threadIdx.x;for(int i=sid; i<nbins; i+=blockDim.x){// 先清空smemlocalHist[i] = 0;}__syncthreads();if(idx<w){for(int i=0; i<h; i++){// 计算图片小块的直方图(sub-histogram),缓存在smemuint8_t pv = image[i*memPitch + idx];atomicAdd(&localHist[pv], 1);}}__syncthreads();uint32_t subHistId = blockIdx.x;for(int i=sid; i<nbins; i+=blockDim.x){// 将sub-histogram写入device memorysubHistogram[subHistId*subHistogramPitch + i] = localHist[i];}
}

运行表现如下:

V2 优化Block和Grid的尺寸

合理选取Block和Grid的尺寸,可以很有效地加速CUDA kernel。除了在V0中提到的将thread/block 设为 32 的整数倍,我们还可以通过平衡每个thread的工作量和block与thread的总数来提升效能。这一提升在CUDA术语里称为 Occupancy (占用率),即每个Stream Multiprocessor (SM)上实际运行的thread数量与理论SM最高可运行thread数量的比值。高占用率意味着更高的硬件使用效率,但不一定会带来更高的throughput。具体怎么平衡,还得用实验检验真理。

SM是N卡上实际负责运算的硬件单元。根于规格不同,每款卡的SM数量会有所差异,而不同compute capabiilty的卡, SM的规格配置也有所差异,详见这个链接:

CUDA Toolkit Documentation​docs.nvidia.com

每个SM均有自己的寄存器(register)和共享内存(shared memory),且支持的最大block数量,最大warp数量和最大thread数量均是固定的。实际占用率由launch configuration和kernel所需的资源共同决定。例如,若实际launch的block数量小于SM支持的最大block数量,则SM没有被喂饱,占用率永远到不了100%。如果launch的block或thread太多了,则一个SM装不下两个又装不满,也会导致占用率不高。由此可见,调整launch configuration 对于提升占用率十分重要。

鉴于我的kernel瓶颈并不在shared memory或register,我便从launch configuration下手。首先定下block size (512或256),然后计算我的整块GPU能够放下的thread数量,最后构建一个grid,其thread总数刚好等于最大thread数。完成之后检查下每个SM的block数量和warp数量有无超标,再适量调整block size。计算过程如下:

// 读取GPU的参数
// 主要关心SM总数,和每个SM的最高thread数
int device;
cudaDeviceProp properties = {};
CUDA_CHECK(cudaGetDevice(&device));
CUDA_CHECK(cudaGetDeviceProperties(&properties, device));size_t bsx = 512; // block size (1D block)
size_t gsx = 1;
// 令grid中thread总数等于GPU最大支持thread数
size_t gsy = properties.multiProcessorCount * properties.maxThreadsPerMultiProcessor / bsx;
dim3 subHistGridDim(gsx, gsy, 1);
dim3 subHistBlockDim(bsx, 1, 1);

由于这样算出的Grid可能不足以覆盖整张图片,我们在X和Y放下移动这个Grid来计算图片的不同位置。具体刀法如下:

代码如下:

const size_t kYFactor = 8;
__global__ void subhist(uint8_t *image,size_t w,size_t h,size_t memPitch,uint32_t *subHistogram,size_t nbins,size_t nsubhist,size_t subHistogramPitch)
{extern __shared__ uint32_t localHist[];uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x;uint32_t idy = blockIdx.y * blockDim.y + threadIdx.y;uint32_t xstride = gridDim.x*blockDim.x;uint32_t ystride = gridDim.y*blockDim.y*kYFactor;uint32_t sid = threadIdx.x;for(int i=sid; i<nbins; i+=blockDim.x){localHist[i] = 0;}__syncthreads();// 在x方向移动gridfor(int j=idx; j<w; j+=xstride){// 在y方向移动gridfor(int i=idy*kYFactor; i<h; i+=ystride){// 在y方向上,每个thread处理8个像素(kYFactor=8)for(int k=0; k<min(kYFactor, h-i); k++){uint8_t pv = image[(i+k)*memPitch + j];atomicAdd(&localHist[pv], 1);}}}__syncthreads();uint32_t subHistId = idy * gridDim.x + blockIdx.x;for(int i=sid; i<nbins; i+=blockDim.x){subHistogram[subHistId*subHistogramPitch + i] = localHist[i];}
}

运行表现为:

优化总结

  1. 当需要对memory进行反复或随机读写时,考虑用shared memory减少内存延时
  2. 最佳 Launch configuration 需要反复试验才能找到

程序优化永无止境,山外有山人外有人。本文中讨论的实现,可能离最优尚有距离。这也是我不断学习,不断提高的源动力。希望能给大家带来点灵感,如有高人也望多多指点。

参考资料

CUDA Programming Guide: https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html

CUDA Runtime API: https://docs.nvidia.com/cuda/cuda-runtime-api/index.html

CUDA Achieved Occupancy: https://docs.nvidia.com/gameworks/content/developertools/desktop/analysis/report/cudaexperiments/kernellevel/achievedoccupancy.htm

cuda图像处理_CUDA随笔之图像直方图(优化历程)相关推荐

  1. 【Python CUDA版】河北工业大学计算机图像处理实验二:图像直方图及灰度变换

    一.实验目的与要求 1.掌握图像灰度直方图的概念及其计算方法,编写灰度直方图统计程序. 2.通过对图像直方图的分析,学习应用直方图法解决诸如图像二值化等具体问题. 3.熟悉直方图均衡化的计算过程及其应 ...

  2. python图像直方图与直方图均衡化

    图像直方图以及灰度与彩色图像的直方图均衡化 图像直方图: 图像的直方图用来表征该图像像素值的分布情况.用一定数目的小区间(bin)来指定表征像素值的范围,每个小区间会得到落入该小区间表示范围的像素数目 ...

  3. python画简便的图片-用python简单处理图片(5):图像直方图

    我们先来看两个函数reshape和flatten: 假设我们先生成一个一维数组: vec=np.arange(15) print vec 显示为: [ 0 1 2 3 4 5 6 7 8 9 10 1 ...

  4. 【MATLAB图像处理实用案例详解(1)】—— 基于直方图优化的图像去雾技术

    目录 一.背景意义 二.理论基础 2.1 空域图像増强 2.2 直方图均衡化 三.方法选择 3.1 全局直方图算法 3.2 局部直方图算法 3.3 Retinex算法 四.效果演示 五.完整代码 一. ...

  5. Win8 Metro(C#)数字图像处理--3.3图像直方图计算

    原文:Win8 Metro(C#)数字图像处理--3.3图像直方图计算 /// <summary>/// Get the array of histrgram./// </summa ...

  6. 图像的灰度级数越多越好_MATLAB-数字图像处理 图像直方图归一化

    图像直方图归一化 图像直方图概念: 图像直方图是反映一个图像像素分布的统计表,其实横坐标代表了图像像素的种类,可以是灰度的,也可以是彩色的.纵坐标代表了每一种颜色值在图像中的像素总数或者占所有像素个数 ...

  7. OpenCV与图像处理学习二——图像直方图与色彩空间

    OpenCV与图像处理学习二--图像直方图与色彩空间 2.4 图像直方图(Image Histogram) 2.4.1 直方图的绘制 2.4.2 三通道直方图绘制 2.5 颜色空间 2.5.1 RGB ...

  8. OpenCV图像处理学习二十,图像直方图均衡化原理与实现

    一.图像直方图的概念 图像直方图,是指对整个图像在灰度范围内的像素值(0~255)统计出现频率次数,据此生成的直方图,称为图像直方图.直方图反映了图像灰度的分布情况,是图像的统计学特征.图像的灰度直方 ...

  9. 计算机图像处理实验二 图像直方图及灰度变换

    一.实验目的与要求 1.掌握图像灰度直方图的概念及其计算方法,编写灰度直方图统计程序. 2.通过对图像直方图的分析,学习应用直方图法解决诸如图像二值化等具体问题. 3.熟悉直方图均衡化的计算过程及其应 ...

最新文章

  1. 爬虫之switch_to切换frame标签
  2. SpringBoot 之Quartz的使用
  3. 4918字,详解商品系统的存储架构设计
  4. 谷歌浏览器中打开IE
  5. angualr Material Icons
  6. C++_类和对象_对象特性_友元_友元类_在一个类中声明另一类作为自己的友元类_可以访问自己类中的private变量---C++语言工作笔记053
  7. 定点运算之原码一位乘法
  8. 获取小程序页面跳转链接
  9. 安然公司特殊目的实体(SPEs)解读
  10. 006Python-Re库入门(正则表达式)
  11. 自动白平衡技术(AWB)
  12. 微信小程序支付错误提示“商户号mch_id或sub_mch_id不存在”
  13. qq邮箱 android,QQ邮箱(com.tencent.androidqqmail) - 6.2.1 - 应用 - 酷安
  14. 美团内部讲座 | 清华大学崔鹏:因果推断技术最新的发展趋势
  15. 金蝶k3单据编码规则_金蝶K3财务操作手册
  16. JavaScript—箭头函数
  17. 不止音乐与露营——聊聊极狐汽车的微信生态营销
  18. pycharm批量注释
  19. 简单四步抓取腾讯视频MP4文件
  20. 关于idea创建maven工程没有src骨架的问题

热门文章

  1. 谈谈Java接口Result设计
  2. 面试官欺负人:new Object()到底占用几个字节?
  3. 某32岁大厂程序员吐槽:简历通过率才30%!大龄韭菜该何去何从?网友:没那么严重,同32岁,简历通过率90%!...
  4. 重大事故!线上系统频繁卡死,凶手竟然是 Full GC ?
  5. 如何优雅的处理 Java 异常,可以参考这些建议
  6. 头条hr就是刚:拒绝Offer或者放弃入职等于永远跟头条没关系!
  7. GitHub的十大JavaScript项目
  8. Worktile荣获NextWorld 2020 年度优秀品牌奖
  9. Angular CDK Overlay 弹出覆盖物
  10. 我们使用Leangoo敏捷实践分享