我的CUDA学习之旅5——OTSU二值算法(最大类间方差法、大津法)CUDA实现

引言

最近在做医疗设备相关的项目,故在项目中大量用到了各类图像分割的算法,为了在图像中分割出特定目标,用到的算法可以有很多,比如阈值分割,多通道分割,边缘分割以及一些前沿的组合分割。而对大多数图像来说,分割的一大难点是将待识别的目标与背景分离,其中一种有效简单的方法就是二值化(并不都有效),本博客试着将二值化算法中的OTSU算法进行cuda改写。

任务要求

输入一张8bit的灰度图,通过CUDA在GPU中对图片实现otsu二值化,最后将结果输出至CPU并进行显示,要求输出图与用CPU内实现后的结果一致。

实现思路

关于OTSU(大津法)二值算法的具体实现思路,具体可见此博文 最大类间方差法(大津法OTSU)
该文对最终的方差计算公式进行了一定的变形,减小了总体的计算量。
通过对OTSU算法的阅读,可以发现在遍历计算最大类间方差时,程序存在着一定的时序性,故解决方案为通过并行计算出所有需要的数据,通过数组进行保存,在时序性算法部分(这里指最大值寻找)仍然利用串行的手法,如此完成算法的改写。
实现过程:
1.统计图像灰度直方图hist数组
2.计算图像最大类间方差
3.根据计算出的最佳阈值进行二值化操作

实现环境

VS2013 + CUDA7.5 + Opencv2.4.13

实现代码

#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#include <cuda.h>
#include <device_functions.h>
#include <iostream>
#include <string.h>
#include <opencv2\opencv.hpp>
using namespace std;
using namespace cv;

/* 计算最大类间方差串行程序 只能在CPU端调用,需要将hist数组传出才可计算 计算量变大时(大图像)速度较慢 */
__host__ int otsuThresh(int* hist, int imgHeight, int imgWidth)
{
    float sum = 0;
    for (int i = 0; i < 256; i++)
    {
        sum += i * hist[i];
    }
    float w0 = 0, u0 = 0;
    float u = sum / (imgHeight * imgWidth);
    float val = 0, maxval = 0;
    float s = 0, n = 0;
    int thresh = 0;
    for (int i = 0; i < 256; i++)
    {
        s += hist[i] * i;
        n += hist[i];
        w0 = n / (imgHeight * imgWidth);
        u0 = s / n;
        val = (u - u0) * (u - u0) * w0 / (1 - w0);
        if (val > maxval)
        {
            maxval = val;
            thresh = i;
        }
    }
    return thresh;
}

//灰度直方图统计
__global__ void imhistInCuda(unsigned char* dataIn, int* hist, int imgHeight, int imgWidth)
{
    int xIndex = threadIdx.x + blockIdx.x * blockDim.x;
    int yIndex = threadIdx.y + blockIdx.y * blockDim.y;

    if (xIndex < imgWidth && yIndex < imgHeight)
    {
        atomicAdd(&hist[dataIn[yIndex * imgWidth + xIndex]], 1);
    }
}

//计算最大类间方差CUDA改编程序
__global__ void OTSUthresh(const int* hist, float* sum, float* s, float* n, float* val, int imgHeight, int imgWidth, int* OtsuThresh)
{
    if (blockIdx.x == 0)
    {
        int index = threadIdx.x;
        atomicAdd(&sum[0], hist[index] * index);
    }
    else
    {
        int index = threadIdx.x;
        if (index < blockIdx.x)
        {
            atomicAdd(&s[blockIdx.x - 1], hist[index] * index);
            atomicAdd(&n[blockIdx.x - 1], hist[index]);
        }
    }
    __syncthreads(); //所有线程同步
    if (blockIdx.x > 0)
    {
        int index = blockIdx.x - 1;
        float u = sum[0] / (imgHeight * imgWidth);
        float w0 = n[index] / (imgHeight * imgWidth);
        float u0 = s[index] / n[index];
        if (w0 == 1)
        {
            val[index] = 0;
        }
        else
        {
            val[index] = (u - u0) * (u - u0) * w0 / (1 - w0);
        }
    }
    __syncthreads(); //所有线程同步
    if (threadIdx.x == 0 && blockIdx.x == 0)
    {
        float maxval = 0;
        for (int i = 0; i < 256; i++)
        {
            if (val[i] > maxval)
            {
                maxval = val[i];
                OtsuThresh[0] = i;
                OtsuThresh[1] = val[i];
            }
        }
    }
}

//阈值化
__global__ void otsuInCuda(unsigned char* dataIn, unsigned char* dataOut, int imgHeight, int imgWidth, int* hThresh)
{
    int xIndex = threadIdx.x + blockIdx.x * blockDim.x;
    int yIndex = threadIdx.y + blockIdx.y * blockDim.y;

    if (xIndex < imgWidth && yIndex < imgHeight)
    {
        if (dataIn[yIndex * imgWidth + xIndex] > hThresh[0])
        {
            dataOut[yIndex * imgWidth + xIndex] = 255;
        }
    }
}

int main()
{
    //传入灰度图
    Mat srcImg = imread("1.jpg", 0);

    int imgHeight = srcImg.rows;
    int imgWidth = srcImg.cols;

    //opencv实现OTSU二值化
    Mat dstImg1;
    threshold(srcImg, dstImg1, 0, 255, THRESH_OTSU);

    //CUDA改编
    Mat dstImg2(imgHeight, imgWidth, CV_8UC1, Scalar(0));

    //在GPU端开辟内存
    unsigned char* d_in;
    int* d_hist;

    cudaMalloc((void**)&d_in, imgHeight * imgWidth * sizeof(unsigned char));
    cudaMalloc((void**)&d_hist, 256 * sizeof(int));

    //传入灰度图至GPU
    cudaMemcpy(d_in, srcImg.data, imgHeight * imgWidth * sizeof(unsigned char), cudaMemcpyHostToDevice);

    dim3 threadsPerBlock1(32, 32);
    dim3 blocksPerGrid1((imgWidth + 32 - 1) / 32, (imgHeight + 32 - 1) / 32);

    imhistInCuda << <blocksPerGrid1, threadsPerBlock1 >> >(d_in, d_hist, imgHeight, imgWidth);

    float* d_sum;
    float* d_s;
    float* d_n;
    float *d_val;
    int* d_t;

    cudaMalloc((void**)&d_sum, sizeof(float));
    cudaMalloc((void**)&d_s, 256 * sizeof(float));
    cudaMalloc((void**)&d_n, 256 * sizeof(float));
    cudaMalloc((void**)&d_val, 256 * sizeof(float));
    cudaMalloc((void**)&d_t, 2 * sizeof(int));

    //定义最大类间方差计算并行规格,其中257为1 + 256,
    //第1个block用来计算图像灰度的sum,后256个block用于计算256个灰度对应的s, n
    dim3 threadsPerBlock2(256, 1);
    dim3 blocksPerGrid2(257, 1);

    OTSUthresh << <blocksPerGrid2, threadsPerBlock2 >> >(d_hist, d_sum, d_s, d_n, d_val, imgHeight, imgWidth, d_t);

    unsigned char* d_out;

    cudaMalloc((void**)&d_out, imgHeight * imgWidth * sizeof(unsigned char));

    otsuInCuda << <blocksPerGrid1, threadsPerBlock1 >> >(d_in, d_out, imgHeight, imgWidth, d_t);

    //输出结果图像
    cudaMemcpy(dstImg2.data, d_out, imgHeight * imgWidth * sizeof(unsigned char), cudaMemcpyDeviceToHost);

    ////调试用输出
    //int th[2] = { 0, 0 };
    //float n[256];
    //memset(n, 0, sizeof(n));
    //cudaMemcpy(th, d_t, 2 * sizeof(int), cudaMemcpyDeviceToHost);
    //cudaMemcpy(n, d_n, 256 * sizeof(float), cudaMemcpyDeviceToHost);

    cudaFree(d_in);
    cudaFree(d_out);
    cudaFree(d_hist);
    cudaFree(d_sum);
    cudaFree(d_s);
    cudaFree(d_n);
    cudaFree(d_val);
    cudaFree(d_t);

    imwrite("result1.jpg", dstImg1);
    imwrite("result2.jpg", dstImg2);

    return 0;
}

实现结果

贴上我大酷玩演唱会图,进行验证~
原图
《我的CUDA学习之旅5——OTSU二值算法(最大类间方差法、大津法)CUDA实现》

OpenCV实现结果图
《我的CUDA学习之旅5——OTSU二值算法(最大类间方差法、大津法)CUDA实现》

CUDA实现后图像
《我的CUDA学习之旅5——OTSU二值算法(最大类间方差法、大津法)CUDA实现》

关于本文以及CUDA的一些思考

在本文的OTSU算法中,其实在改编的过程中,一直怀疑把那段计算最大类间方差的串行代码(代码中host部分)改成部分并行部分串行并没有起到提速的作用,而事实上我自己做了测速,发现确实基本没什么区别,分析了下原因在于:
1.计算量不大,GPU加深没发挥真正的作用
2.改写的过程涉及时序的部分只能采用串行,而串行的效率GPU反而低于CPU
3.算法还未优化至最佳
而从刚开始接触CUDA到现在陆陆续续也写了不少CUDA的代码了,给刚入坑的朋友提几点不成熟的建议~:
1.CUDA不是万能的,很多时候一些复杂的算法无法改写,尤其是涉及时序性的
2.加速的关键在于对于GPU内存的使用规划以及原算法的性能
3.有成熟的库函数能用的时候可不用CUDA,因为提速效果不是很明显(可能是我显卡渣的原因==),例如上面的OTSU算法,OpenCV实现20ms左右,CUDA实现10ms左右
4.CUDA的时间花费大部分都在GPU传至CPU上(一般占总时间50%以上),所以在进行编码的时候能不传数据出来就尽量不要传,尽量在GPU中完成所有算法的实现,争取一进一出~
5.CUDA中传变量只能通过数组的形式,所以就算你的变量数量为1,也要定义数组,并把数组的头指针传给核函数

点赞