CLAHE算法的OpenCV重构及详细解读

OpenCV封装的CLAHE算法阅读起来比较麻烦,作者使用OpenCV在完全遵循原代码逻辑的基础上对该算法进行了重构,完整实现了OpenCV4.6.0中的CLAHE算法(包括16位图像和8位图像),代码如下:

void CLAHE16UC1(const cv::Mat& src,cv::Mat& dst,double clip = 40.0, cv::Size tiles = cv::Size(8,8));
void CLAHE8UC1(const cv::Mat& src,cv::Mat& dst,double clip = 40.0, cv::Size tiles = cv::Size(8,8));

void CLAHE16UC1(const cv::Mat& src,cv::Mat& dst,double clip, cv::Size tiles)
{
    if(src.type() != CV_16UC1)
        return;

    int histSize = 65536;
    //图像被划分为tilesX×tilesY个分块
    int tilesX = tiles.width;
    int tilesY = tiles.height;
    cv::Size tileSize;

    cv::Mat srcForLut;
    if (src.size().width % tilesX == 0 && src.size().height % tilesY == 0)
    {
        tileSize = cv::Size(src.size().width / tilesX, src.size().height / tilesY);
        srcForLut = src.clone();
    }
    else
    {
        cv::Mat srcExt;
        //图像边缘填充,保证用于计算LUT的图像其宽度和高度可以分别被tilesX和tilesY整除
        cv::copyMakeBorder(src, srcExt,0, tilesY - (src.size().height % tilesY),0, tilesX - (src.size().width % tilesX), cv::BORDER_REFLECT_101);
        tileSize = cv::Size(srcExt.size().width / tilesX, srcExt.size().height / tilesY);
        srcForLut = srcExt.clone();
    }

    //计算直方图“削峰填谷”的阈值,计算clipLimit的过程可以理解为:局部分块图像直方图平均频数的clip倍
    int clipLimit = static_cast<int>(clip * tileSize.area() / histSize);
    clipLimit = std::max(clipLimit, 1);

    cv::Mat lut(tilesX*tilesY, histSize,src.type()); //存储每个分块的LUT,则共有tilesX*tilesY个LUT,每个LUT有histSize个元素
    lut.setTo(0);

    int* tileHist = new int[histSize];

    //对每个块分别计算LUT,存在上面的lut里
    for(int ty=0;ty<tilesY;ty++)
    {
        for(int tx=0;tx<tilesX;tx++)
        {
            memset(tileHist,0,histSize*sizeof(int));
            ushort* tileLut = lut.ptr<ushort>(ty*tilesX+tx);

            cv::Rect tileRect;
            tileRect.x = tx * tileSize.width;
            tileRect.y = ty * tileSize.height;
            tileRect.width = tileSize.width;
            tileRect.height = tileSize.height;

            const cv::Mat tile = srcForLut(tileRect).clone();
            const ushort* pTile = tile.ptr<ushort>();

            //统计局部分块的直方图
            for(int i=0;i<tile.rows;i++)
            {
                for(int j=0;j<tile.cols;j++)
                {
                    tileHist[pTile[i*tile.cols+j]]++;
                }
            }

            if (clipLimit > 0)
            {
                //直方图“削峰”,并统计削去的像素数
                int clipped = 0;
                for (int i = 0; i < histSize; ++i)
                {
                    if (tileHist[i] > clipLimit)
                    {
                        clipped += (tileHist[i] - clipLimit);
                        tileHist[i] = clipLimit;
                    }
                }

                //将削去的像素个数平均分配给直方图的每个灰阶
                int redistBatch = clipped / histSize;
                int residual = clipped - redistBatch * histSize;

                //将削去的像素个数平均分配给直方图的每个灰阶,1)首先平均分配
                for (int i = 0; i < histSize; ++i)
                    tileHist[i] += redistBatch;

                //将削去的像素个数平均分配给直方图的每个灰阶,2)仍然有剩余的,直方图上等间距分配
                if (residual != 0)
                {
                    int residualStep = MAX(histSize / residual, 1);
                    for (int i = 0; i < histSize && residual > 0; i += residualStep, residual--)
                        tileHist[i]++;
                }
            }


            const float lutScale = cv::saturate_cast<float>(histSize - 1) / (tile.cols*tile.rows);

            //计算LUT
            int sum = 0;
            for (int i = 0; i < histSize; ++i)
            {
                sum += tileHist[i];
                tileLut[i] = cv::saturate_cast<ushort>(sum * lutScale);
            }
        }
    }

    delete [] tileHist;
    tileHist = NULL;

    float invTileW = 1.0f/tileSize.width;
    float invTileH = 1.0f/tileSize.height;

    dst.create(src.size(),CV_16UC1);
    ushort* pDst = dst.ptr<ushort>();
    const ushort* pSrc = src.ptr<ushort>();

    //对每个像素点,找到其4个近邻分块的LUT所映射的像素值,并进行双线性插值
    for(int i=0;i<src.rows;i++)
    {
        for(int j=0;j<src.cols;j++)
        {
            ushort pix = pSrc[i*src.cols+j];

            float txf = j*invTileW - 0.5f;

            int tx1 = cvFloor(txf);
            int tx2 = tx1 + 1; //注意这里,说明4个近邻分块是紧挨着的

            float px1 = txf - tx1;
            float px2 = 1.0f - px1;

            tx1 = std::max(tx1, 0);
            tx2 = std::min(tx2, tilesX - 1);

            float tyf = i*invTileH - 0.5f;

            int ty1 = cvFloor(tyf);
            int ty2 = ty1 + 1; //注意这里,说明4个近邻分块是紧挨着的

            float py1 = tyf - ty1;
            float py2 = 1.0f - py1;

            ty1 = std::max(ty1, 0);
            ty2 = std::min(ty2, tilesY - 1);

            ushort* tileLuty1x1 = lut.ptr<ushort>(ty1*tilesX+tx1);
            ushort* tileLuty1x2 = lut.ptr<ushort>(ty1*tilesX+tx2);

            ushort* tileLuty2x1 = lut.ptr<ushort>(ty2*tilesX+tx1);
            ushort* tileLuty2x2 = lut.ptr<ushort>(ty2*tilesX+tx2);

            //4个邻块的映射灰度值线性插值,先x方向,再y方向
            //注意:前面提到px1+px2=1.0和py1+py2=1.0,这里的px1,px2,py1,py2都是距离。在作为双线性插值的权重时,距离近的权重应大;反之亦然。
            //以x方向插值举例:距离为px1时,其权重应取1.0-px1,即px2;距离为px2时,其权重应取1.0-px2,即px1。
            pDst[i*src.cols+j] = cv::saturate_cast<ushort>(
                        (tileLuty1x1[pix]*px2+tileLuty1x2[pix]*px1)*py2+
                        (tileLuty2x1[pix]*px2+tileLuty2x2[pix]*px1)*py1);
        }
    }
}

void CLAHE8UC1(const cv::Mat& src,cv::Mat& dst,double clip, cv::Size tiles)
{
    if(src.type() != CV_8UC1)
        return;

    int histSize = 256;
    //图像被划分为tilesX×tilesY个分块
    int tilesX = tiles.width;
    int tilesY = tiles.height;
    cv::Size tileSize;

    cv::Mat srcForLut;
    if (src.size().width % tilesX == 0 && src.size().height % tilesY == 0)
    {
        tileSize = cv::Size(src.size().width / tilesX, src.size().height / tilesY);
        srcForLut = src.clone();
    }
    else
    {
        cv::Mat srcExt;
        //图像边缘填充,保证用于计算LUT的图像其宽度和高度可以分别被tilesX和tilesY整除
        cv::copyMakeBorder(src, srcExt,0, tilesY - (src.size().height % tilesY),0, tilesX - (src.size().width % tilesX), cv::BORDER_REFLECT_101);
        tileSize = cv::Size(srcExt.size().width / tilesX, srcExt.size().height / tilesY);
        srcForLut = srcExt.clone();
    }

    //计算直方图“削峰填谷”的阈值,计算clipLimit的过程可以理解为:局部分块图像直方图平均频数的clip倍
    int clipLimit = static_cast<int>(clip * tileSize.area() / histSize);
    clipLimit = std::max(clipLimit, 1);

    cv::Mat lut(tilesX*tilesY, histSize,src.type()); //存储每个分块的LUT,则共有tilesX*tilesY个LUT,每个LUT有histSize个元素
    lut.setTo(0);

    int* tileHist = new int[histSize];

    //对每个块分别计算LUT,存在上面的lut里
    for(int ty=0;ty<tilesY;ty++)
    {
        for(int tx=0;tx<tilesX;tx++)
        {
            memset(tileHist,0,histSize*sizeof(int));
            uchar* tileLut = lut.ptr<uchar>(ty*tilesX+tx);

            cv::Rect tileRect;
            tileRect.x = tx * tileSize.width;
            tileRect.y = ty * tileSize.height;
            tileRect.width = tileSize.width;
            tileRect.height = tileSize.height;

            const cv::Mat tile = srcForLut(tileRect).clone();
            const uchar* pTile = tile.ptr<uchar>();

            //统计局部分块的直方图
            for(int i=0;i<tile.rows;i++)
            {
                for(int j=0;j<tile.cols;j++)
                {
                    tileHist[pTile[i*tile.cols+j]]++;
                }
            }

            if (clipLimit > 0)
            {
                //直方图“削峰”,并统计削去的像素数
                int clipped = 0;
                for (int i = 0; i < histSize; ++i)
                {
                    if (tileHist[i] > clipLimit)
                    {
                        clipped += (tileHist[i] - clipLimit);
                        tileHist[i] = clipLimit;
                    }
                }

                //将削去的像素个数平均分配给直方图的每个灰阶
                int redistBatch = clipped / histSize;
                int residual = clipped - redistBatch * histSize;

                //将削去的像素个数平均分配给直方图的每个灰阶,1)首先平均分配
                for (int i = 0; i < histSize; ++i)
                    tileHist[i] += redistBatch;

                //将削去的像素个数平均分配给直方图的每个灰阶,2)仍然有剩余的,直方图上等间距分配
                if (residual != 0)
                {
                    int residualStep = MAX(histSize / residual, 1);
                    for (int i = 0; i < histSize && residual > 0; i += residualStep, residual--)
                        tileHist[i]++;
                }
            }


            const float lutScale = cv::saturate_cast<float>(histSize - 1) / (tile.cols*tile.rows);

            //计算LUT
            int sum = 0;
            for (int i = 0; i < histSize; ++i)
            {
                sum += tileHist[i];
                tileLut[i] = cv::saturate_cast<uchar>(sum * lutScale);
            }
        }
    }

    delete [] tileHist;
    tileHist = NULL;

    float invTileW = 1.0f/tileSize.width;
    float invTileH = 1.0f/tileSize.height;

    dst.create(src.size(),CV_8UC1);
    uchar* pDst = dst.ptr<uchar>();
    const uchar* pSrc = src.ptr<uchar>();

    //对每个像素点,找到其4个近邻分块的LUT所映射的像素值,并进行双线性插值
    for(int i=0;i<src.rows;i++)
    {
        for(int j=0;j<src.cols;j++)
        {
            uchar pix = pSrc[i*src.cols+j];

            float txf = j*invTileW - 0.5f;

            int tx1 = cvFloor(txf);
            int tx2 = tx1 + 1; //注意这里,说明4个近邻分块是紧挨着的

            float px1 = txf - tx1;
            float px2 = 1.0f - px1;

            tx1 = std::max(tx1, 0);
            tx2 = std::min(tx2, tilesX - 1);

            float tyf = i*invTileH - 0.5f;

            int ty1 = cvFloor(tyf);
            int ty2 = ty1 + 1; //注意这里,说明4个近邻分块是紧挨着的

            float py1 = tyf - ty1;
            float py2 = 1.0f - py1;

            ty1 = std::max(ty1, 0);
            ty2 = std::min(ty2, tilesY - 1);

            uchar* tileLuty1x1 = lut.ptr<uchar>(ty1*tilesX+tx1);
            uchar* tileLuty1x2 = lut.ptr<uchar>(ty1*tilesX+tx2);

            uchar* tileLuty2x1 = lut.ptr<uchar>(ty2*tilesX+tx1);
            uchar* tileLuty2x2 = lut.ptr<uchar>(ty2*tilesX+tx2);

            //4个邻块的映射灰度值线性插值,先x方向,再y方向
            //注意:前面提到px1+px2=1.0和py1+py2=1.0,这里的px1,px2,py1,py2都是距离。在作为双线性插值的权重时,距离近的权重应大;反之亦然。
            //以x方向插值举例:距离为px1时,其权重应取1.0-px1,即px2;距离为px2时,其权重应取1.0-px2,即px1。
            pDst[i*src.cols+j] = cv::saturate_cast<uchar>(
                        (tileLuty1x1[pix]*px2+tileLuty1x2[pix]*px1)*py2+
                        (tileLuty2x1[pix]*px2+tileLuty2x2[pix]*px1)*py1);
        }
    }
}

代码中比较难理解的是双线性插值时4个近邻块的选择方式,这里根据作者的理解给出示意图(以图像被均分为6*6块为例,其中红色小块表示当前处理像素点所在位置,绿色大块表示所选择的用于双线性插值的4个近邻块的位置),如下所示:
双线性插值近邻分块选择示意图
使用上述代码里的默认参数,8位图像处理效果如下:
原图像处理后

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
如果你不想使用OpenCV库,你可以手动实现CLAHE算法。以下是一个基于NumPy库的简单实现示例: ```python import numpy as np def calculate_histogram(image): histogram = np.zeros(256, dtype=np.uint32) for i in range(image.shape[0]): for j in range(image.shape[1]): pixel_value = image[i, j] histogram[pixel_value] += 1 return histogram def apply_clahe(image, clip_limit=2.0, grid_size=(8, 8)): # 计算图像的直方图 histogram = calculate_histogram(image) # 计算累积直方图 cdf = np.cumsum(histogram) # 计算CLAHE增强后的图像 enhanced_image = np.zeros_like(image) for i in range(image.shape[0]): for j in range(image.shape[1]): pixel_value = image[i, j] # 计算分块的起始和结束坐标 block_start_i = (i // grid_size[0]) * grid_size[0] block_end_i = block_start_i + grid_size[0] block_start_j = (j // grid_size[1]) * grid_size[1] block_end_j = block_start_j + grid_size[1] # 计算分块内的直方图 block_histogram = histogram[pixel_value] block_histogram += cdf[pixel_value] - cdf[block_start_i:block_end_i, block_start_j:block_end_j].mean() # 限制对比度增强 block_histogram = np.clip(block_histogram, 0, clip_limit) # 将像素值映射到增强后的直方图范围 block_histogram = (block_histogram / clip_limit) * 255 # 更新增强后的像素值 enhanced_image[i, j] = block_histogram[pixel_value] return enhanced_image ``` 在上述代码中,`calculate_histogram`函数计算输入图像的直方图。`apply_clahe`函数接收输入图像、对比度限制(clip_limit)和分块大小(grid_size)作为参数,并应用CLAHE增强算法。 请注意,这只是一个简单的CLAHE实现示例,可能没有考虑到所有细节和优化。CLAHE算法涉及到图像分块、直方图计算、限制对比度增强等步骤,因此实现完整的CLAHE算法可能需要更复杂的代码。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值