OpenCV源码解析:直方图均衡化的详细算法和过程

理论基础

所谓直方图,在图像中,指的就是各个像素的统计值,就是一个像素在整幅图像中出现次数。

例如下面这张16个像素的图片,其直方图就是

 直方图均衡化,是将给定图像的直方图改造成均匀分布的直方图,从而扩大像素灰度值的动态范围,达到增强图像对比度的效果。

OpenCV中的直方图均衡化

OpneCv中,可以用calcHist进行图像的均衡化,也可以使用equalizeHist可以进行全局直方图均衡化(就是直接对整个图像的直方图进行均衡化)。这里以equalizeHist为例进行详细讲解。

先看一下equalizeHist的源码

void cv::equalizeHist( InputArray _src, OutputArray _dst )
{
    CV_INSTRUMENT_REGION()

    CV_Assert( _src.type() == CV_8UC1 );

    if (_src.empty())
        return;

    CV_OCL_RUN(_src.dims() <= 2 && _dst.isUMat(),
               ocl_equalizeHist(_src, _dst))

    Mat src = _src.getMat();
    _dst.create( src.size(), src.type() );
    Mat dst = _dst.getMat();

    CV_OVX_RUN(!ovx::skipSmallImages<VX_KERNEL_EQUALIZE_HISTOGRAM>(src.cols, src.rows),
               openvx_equalize_hist(src, dst))

    Mutex histogramLockInstance;

    const int hist_sz = EqualizeHistCalcHist_Invoker::HIST_SZ;
    int hist[hist_sz] = {0,}; // 直方图表
    int lut[hist_sz]; // 查找表

    EqualizeHistCalcHist_Invoker calcBody(src, hist, &histogramLockInstance); // 建立直方图操作
    EqualizeHistLut_Invoker      lutBody(src, dst, lut); // 查找表操作
    cv::Range heightRange(0, src.rows); // 总行数的范围

    if(EqualizeHistCalcHist_Invoker::isWorthParallel(src))
        parallel_for_(heightRange, calcBody);
    else
        calcBody(heightRange);

    int i = 0;
    while (!hist[i]) ++i; // 跳过那些像素个数为0的

    int total = (int)src.total(); // 总像素的个数
    if (hist[i] == total) // 如果图片的全部像素都属于一个颜色
    {
        dst.setTo(i); // 目标直接设置成该色,返回
        return;
    }

    float scale = (hist_sz - 1.f)/(total - hist[i]); //物理意义近似于:每个像素在直方图上的横坐标上能占多宽
    int sum = 0;

	// 下面是建立查找表
    for (lut[i++] = 0; i < hist_sz; ++i)
    {
        sum += hist[i]; // 前面已经处理的像素个数的总和
        lut[i] = saturate_cast<uchar>(sum * scale); // 前面的像素在直方图横坐标上能占多宽,注意这里是uchar的整数
    }
	
	// 执行均衡化操作
    if(EqualizeHistLut_Invoker::isWorthParallel(src))
        parallel_for_(heightRange, lutBody);
    else
        lutBody(heightRange);
}

可以看出,equalizeHist 定义了EqualizeHistCalcHist_Invoker calcBody(src, hist, &histogramLockInstance)来建立直方图,同时还定义了EqualizeHistLut_Invoker  lutBody(src, dst, lut)来从查找表构建目标图像。这两个类都继承了抽象类cv::ParallelLoopBody,该类是一个专门为并行处理设计的类,其对象会在parallel_for_impl中传递给Concurrency::parallel_for函数。并行处理能更充分地发挥设备的硬件性能,所以速度非常快。

因为涉及到并行处理,我们先理解一下OpenCV并行处理的过程,同时也可以借鉴一下Intel公司在并行处理上的编码风格和艺术。

并行处理的真正实现函数是parallel_for_impl,其中定义的ParallelLoopBodyWrapperContext在始化时起到联接上下文的作用,在并行运行时本身没有实质内容。

在看源码之前,我们先理一下运行的过程和顺序,equilizeHist有两处用到了parallel_for_

EqualizeHistCalcHist_Invoker calcBody(src, hist, &histogramLockInstance);
EqualizeHistLut_Invoker      lutBody(src, dst, lut);
…
if(EqualizeHistCalcHist_Invoker::isWorthParallel(src))
        parallel_for_(heightRange, calcBody);
    else
        calcBody(heightRange);
…
    if(EqualizeHistLut_Invoker::isWorthParallel(src))
        parallel_for_(heightRange, lutBody);
    else
        lutBody(heightRange);
…

前面讲过,calcBody负责计算直方图,lutBody负责填充目标图像,这两个parallel_for_执行的过程是完全一样的。在parallel_for_impl函数中,并行运行的系统API是

Concurrency::parallel_for(stripeRange.start, stripeRange.end, pbody);

设置的处理函数是ProxyLoopBody pbody,这两个Body执行的路线(关系)就是ProxyLoopBody pbody(ctx)----> ParallelLoopBodyWrapperContext ctx(body)----> Body, 其实现都在相关类的()重载函数中。

首先pbody运行时,实例ProxyLoopBody pbody(ctx)被执行,即

    class ProxyLoopBody : public ParallelLoopBodyWrapper
    {
        void operator ()(int i) const
        {
            this->ParallelLoopBodyWrapper::operator()(cv::Range(i, i + 1));
        }
    };

其中range就是

Concurrency::parallel_for(stripeRange.start, stripeRange.end, pbody);

传入的参数范围,系统会根据这个范围逐个处理,如果启用TBB,就是每次处理一个小范围,直到全部处理完毕。

而后,这个operator()重载函数执行时,其被继承的ParallelLoopBodyWrapper类的()重载函数会被调用,

class ParallelLoopBodyWrapper : public cv::ParallelLoopBody
    {  
        void operator()(const cv::Range& sr) const
       {
            // propagate main thread state
            cv::theRNG() = ctx.rng; // 这个就是ParallelLoopBodyWrapperContext里初始化时得到的线程状态

            cv::Range r;
            cv::Range wholeRange = ctx.wholeRange;
            int nstripes = ctx.nstripes;
            r.start = (int)(wholeRange.start +
                            ((uint64)sr.start*(wholeRange.end - wholeRange.start) + nstripes/2)/nstripes);
            r.end = sr.end >= nstripes ? wholeRange.end : (int)(wholeRange.start +
                            ((uint64)sr.end*(wholeRange.end - wholeRange.start) + nstripes/2)/nstripes);

            (*ctx.body)(r); // 计算直方图 或 填充目标图像

            if (!ctx.is_rng_used && !(cv::theRNG() == ctx.rng))
                ctx.is_rng_used = true;
        }
}

其中,在构建直方图时,ctx.body就是EqualizeHistCalcHist_Invoker的实例,

            (*ctx.body)(r);

会执行下面这段直方图的计算

class EqualizeHistCalcHist_Invoker : public cv::ParallelLoopBody
{
    void operator()( const cv::Range& rowRange ) const
    {。。。}
}

在构建目标图像时,ctx.body就是EqualizeHistLut_Invoker的实例,(*ctx.body)(r)就相当于运行,

class EqualizeHistLut_Invoker : public cv::ParallelLoopBody
{
    void operator()( const cv::Range& rowRange ) const
    {
        。。。
    }
}

下面是parallel_for_impl的源码(删除了一些与本文主旨无关的),很简单,

static void parallel_for_impl(const cv::Range& range, const cv::ParallelLoopBody& body, double nstripes)
{
    if ((numThreads < 0 || numThreads > 1) && range.end - range.start > 1)
    {
        ParallelLoopBodyWrapperContext ctx(body, range, nstripes); // 这里会初始化ctx->nstripes
        ProxyLoopBody pbody(ctx); 
        cv::Range stripeRange = pbody.stripeRange();     // 就是上面ctx初始化的那个ctx->nstripes
        if( stripeRange.end - stripeRange.start == 1 )
        {
            body(range);
            return;
        }

        if(!pplScheduler || pplScheduler->Id() == Concurrency::CurrentScheduler::Id())
        {
            Concurrency::parallel_for(stripeRange.start, stripeRange.end, pbody); // 打开多线程
        }
        else
        {
            pplScheduler->Attach();
            Concurrency::parallel_for(stripeRange.start, stripeRange.end, pbody);
            Concurrency::CurrentScheduler::Detach();
        }
    }
}

OpenCV均衡化的具体算法详解

在具体算法上,用一个实在的例子来讲比较容易理解。

OpenCV是这样处理的,

先建立直方图hist[256],比如某图片的灰度图,

Hist[0~15] = 0,表示这几个颜色没有用到

hist[16] = 1, 表示颜色为16的有1个像素,

hist[17] = 2, 表示颜色为17的有10个像素,

hist[18] = 5, 表示颜色为18的有29个像素,

。。。

然后,用equalizeHist计算scale,这个scale的物理意义,大致可以理解为每个像素在直方图上的横坐标上能占多宽。

比如得到scale=0.1后建立查找表lut[256],就是这样的,

Lut[0~15] = 0

Lut[16] = (int)0.1*1 = 0

Lut[17] = (int)0.1*(1+10)=1

Lut[18] = (int)0.1*(1+10+29) = 4

。。。

再然后,在新的图像中,

原颜色为16的 改成 新色 0

原颜色为17的 改成 新色1

原颜色为18的 改成 新色4

。。。

这样,灰度值的动态范围得到了扩大,对比度得到加强,图像可以进一步处理。

源码

EqualizeHistCalcHist_Invoker

作用:进行直方图统计

class EqualizeHistCalcHist_Invoker : public cv::ParallelLoopBody
{
public:
    enum {HIST_SZ = 256};

    EqualizeHistCalcHist_Invoker(cv::Mat& src, int* histogram, cv::Mutex* histogramLock)
        : src_(src), globalHistogram_(histogram), histogramLock_(histogramLock)
    { }

    void operator()( const cv::Range& rowRange ) const 
    {
        int localHistogram[HIST_SZ] = {0, }; 

        const size_t sstep = src_.step;

        int width = src_.cols;
        int height = rowRange.end - rowRange.start;

        if (src_.isContinuous())
        {
            width *= height;
            height = 1;
        }

        for (const uchar* ptr = src_.ptr<uchar>(rowRange.start); height--; ptr += sstep)
        {
            int x = 0;
		   // 下面这个for循环,就是统计图片的直方图
		   // 每次统计4个点,
            for (; x <= width - 4; x += 4)
            {
                int t0 = ptr[x], t1 = ptr[x+1];
                localHistogram[t0]++; localHistogram[t1]++;
                t0 = ptr[x+2]; t1 = ptr[x+3];
                localHistogram[t0]++; localHistogram[t1]++;
            }

            for (; x < width; ++x)
                localHistogram[ptr[x]]++;
        }

        cv::AutoLock lock(*histogramLock_);

		// 把结果放入globalHistogram_
        for( int i = 0; i < HIST_SZ; i++ )
            globalHistogram_[i] += localHistogram[i];
    }

    static bool isWorthParallel( const cv::Mat& src )
    {
        return ( src.total() >= 640*480 );
    }

private:
    EqualizeHistCalcHist_Invoker& operator=(const EqualizeHistCalcHist_Invoker&);

    cv::Mat& src_;
    int* globalHistogram_;
    cv::Mutex* histogramLock_;
};

EqualizeHistLut_Invoker

作用:执行直方图的均衡化操作

class EqualizeHistLut_Invoker : public cv::ParallelLoopBody
{
public:
    EqualizeHistLut_Invoker( cv::Mat& src, cv::Mat& dst, int* lut )
        : src_(src),
          dst_(dst),
          lut_(lut)
    { }

    void operator()( const cv::Range& rowRange ) const
    {
        const size_t sstep = src_.step;
        const size_t dstep = dst_.step;

        int width = src_.cols;
        int height = rowRange.end - rowRange.start;
        int* lut = lut_;

        if (src_.isContinuous() && dst_.isContinuous())
        {
            width *= height;
            height = 1;
        }

        const uchar* sptr = src_.ptr<uchar>(rowRange.start);
        uchar* dptr = dst_.ptr<uchar>(rowRange.start);
	
        for (; height--; sptr += sstep, dptr += dstep)
        {
            int x = 0;
            for (; x <= width - 4; x += 4)
            {
                int v0 = sptr[x];
                int v1 = sptr[x+1];
                int x0 = lut[v0];
                int x1 = lut[v1];
                dptr[x] = (uchar)x0;
                dptr[x+1] = (uchar)x1;

                v0 = sptr[x+2];
                v1 = sptr[x+3];
                x0 = lut[v0];
                x1 = lut[v1];
                dptr[x+2] = (uchar)x0;
                dptr[x+3] = (uchar)x1;
            }

            for (; x < width; ++x)
                dptr[x] = (uchar)lut[sptr[x]];
        }
    }

    static bool isWorthParallel( const cv::Mat& src )
    {
        return ( src.total() >= 640*480 );
    }

private:
    EqualizeHistLut_Invoker& operator=(const EqualizeHistLut_Invoker&);

    cv::Mat& src_;
    cv::Mat& dst_;
    int* lut_;
};

解析完毕

 

 

posted @ 2018-08-15 12:10  SpaceVision  阅读(240)  评论(0编辑  收藏  举报