一种多聚焦融合算法的实现

背景与现状:

在现实生活中,人们使用摄像机拍摄时,希望可以获得同一场景中所有景物都清晰的图像。但是摄像机镜头受景深的限制,无法同时聚焦所有目标,因此拍摄的照片中部分区域清晰,部分区域模糊。多聚焦图像融合技术可以将多幅同一场景下聚焦区域不同的图像融合成一幅全清晰的图像,从而有效地解决这个问题,提高图像的信息利用率。准确地识别和提取源图像中的聚焦区域是多聚焦图像融合算法中的难题,如果聚焦区域提取不完整,会导致融合结果中出现伪影及边缘轮廓丢失的现象。图像融合技术包含三个不同的级别,即像素级、特征级和决策级。像素级的图像融合方法直接将源图像中的原始信息进行组合,其保留边缘、纹理等细节信息的能力要优于其他两个级别。现有的多聚焦图像融合技术都是在像素级上展开的。

当前,多聚焦图像融合要解决的核心问题是:如何将源图像中的聚焦区域识别和提取出来组成一幅全清晰的图像。解决这个问题的方法是准确找到聚焦和非聚焦区域的边界。许多学者通过构造不同的像素清晰度测量方法,从而正确区分源图像中哪些是聚焦像素,哪些是模糊像素,提高融合图像的性能。近年来,越来越多的研究人员致力于多聚焦图像融合方法的研究,并提出了许多有效的融合算法。现有的像素级多聚焦图像融合方法主要分为三类:基于空间域的方法、基于变换域的方法和基于深度学习的方法。

基于空间域的图像融合方法一般是用固定大小的窗口或者图像分割技术将源图像分成一定大小的图像块,然后通过某种清晰度测量方法分辨出源图像中的清晰区域和模糊区域,最后将这些清晰的图像块组合成融合图像。

基于变换域的图像融合方法通常是利用某种变换将源图像分解成不同频带的子图像,然后选取不同的融合规则得到融合后的子图像,最后通过逆变换生成融合图像。常用的变换域融合方法有多尺度几何变换法、稀疏表示法、替代法等。

基于深度学习的图像融合方法包括利用脉冲耦合神经网络(Pulse Coupled Neural Network,PCNN)、卷积神经网络(Convolutional NeuralNetwork,CNN)等方法提取图像特征并构建融合权重图,提高了聚焦区域和非聚焦区域分类的准确性,依靠后处理可以有效提高图像融合的质量。

算法思路:

目前比较常用的图像融合算法有线性融合、羽化融合、多频段融合,其中多频段融合通过在变换域分别对图像的不同频率成分做融合,既能保留高频轮廓,又能消除伪影。本文算法使用多频段融合做为基础。

此外,对于多张不同景深的图像,融合时需要有效地选择合适的掩码。关于掩码选择的方法,我大致描述如下:
基于空间域将每张图像分成固定大小的网格,然后计算每个网格的平均梯度,形成这个一个平均梯度矩阵数组。然后遍历每一个网格,将所有图像相同位置的平均梯度对对比,选取值最大的那张设置该网格掩码为1,其余的设为0。当所有网格遍历完成,所有图像的掩码矩阵也基本形成。然后,对所有图像及其掩码构建高斯金字塔,对图像构建拉普拉斯金字塔,并逐层融合所有图像的拉普拉斯图像,形成最后融合效果。

高斯金字塔:

常用的建立高斯金字塔的方法是自底向上一层一层地迭代计算生成。该方法通常分为两个步骤:第一,使用高斯低通滤波器对图像进行平滑(高斯模糊),第二,对经高斯平滑处理后的图像进行下采样,得到一系列尺寸的图像。减少一幅图像的尺寸,仅仅靠采样会丢失许多信息。根据采样定理,需要让所有小于最短波长的1/4采样而得到的精细结构能通过平滑滤波器来消除掉,这样才能获得一幅正确的采样图像,故采样前先进行高斯平滑。如图1所示为高斯金字塔模型的建立过程。从图中可以看出高斯金字塔的第K层图像通过高斯模糊和降采样就可得到第K+1层的高斯图像。这边的降采样为隔行隔列降采样,即可将所有的偶数行列去除,得到上层图像的像素为下层图像的1/4。通过这两个步骤反复迭代就能得到一个完整的高斯金字塔。


image.png

通过不断迭代计算就可以生成一系列的图像G0,G1…,GN,构成一个完整的高斯金字塔,G0即为原图像即为高斯金字塔的底层,GN为高斯金字塔的顶层。除了上述这种构建方法外,还可以直接将原始图像即金字塔底层与一组等效加权函数进行卷积生成高斯金字塔的各个层。如下图所示即为图像通过高斯金字塔处理后生成的一系列不同尺度的图像。


image.png

拉普拉斯金字塔:

拉普拉斯金字塔可以认为是残差金字塔,用来存储下采样后图片与原始图片的差异。我们知道,如果高斯金字塔中任意一张图Gi(比如G0为最初的高分辨率图像)先进行下采样得到图Down(Gi),再进行上采样得到图Up(Down(Gi)),得到的Up(Down(Gi))与Gi是存在差异的,因为下采样过程丢失的信息不能通过上采样来完全恢复,也就是说下采样是不可逆的。原始图片下采样后得到的小尺寸图片虽然保留了视觉效果,但是将该小尺寸图像再次上采样也不能完整的恢复出原始图像。为了能够从下采样图像Down(Gi)中还原原始图像Gi,我们需要记录再次上采样得到Up(Down(Gi))与原始图片Gi之间的差异,这就是拉普拉斯金字塔的核心思想。下图是一张拉普拉斯金字塔过程的描述:


image.png

算法实现:

opencv自带了多频段融合算法(MultiBandBlender)的实现,经初步使用,它对传入的图像格式做了较多的要求,如要求必须为三通道等等,而且性能也是个问题。下面的实现同时支持CV_8UC3和CV_8UC1两种格式,因而也可以扩展到使用BAYER格式的场景。

主要代码如下:

// 图像金字塔信息对象
class CImagePyramidInfo
{
private:
    cv::Mat m_matMeanFocus;  // 平均梯度网格矩阵
    std::vector<cv::Mat> m_vecGaussian;  // 高斯金字塔
    std::vector<cv::Mat> m_vecLaplac;  // 拉普拉斯金字塔
    std::vector<cv::Mat> m_vecMask;  // 掩码金字塔

public:
    CImagePyramidInfo(const cv::Mat& matImage, int nGradSize = 100)
    {
        __makeMeanFocus(matImage, m_matMeanFocus, nGradSize);

        cv::Mat matImageFloat;
        matImage.convertTo(matImageFloat, CV_32F, 1.0 / 255);

        __makeGaussian(matImageFloat, m_vecGaussian, 5);
        __makeLaplac(m_vecGaussian, m_vecLaplac);
    }
    virtual ~CImagePyramidInfo()
    {
    }

    // 构建掩码金字塔
    void makeMask(const cv::Mat& matMask)
    {
        __makeMask(matMask, m_vecMask, m_vecGaussian.size());
    }

    // 获取平均梯度网格矩阵
    const cv::Mat& getMeanFocus() const
    {
        return m_matMeanFocus;
    }

    // 获取高斯金字塔
    const std::vector<cv::Mat>& getGaussian() const
    {
        return m_vecGaussian;
    }

    // 获取拉普拉斯金字塔
    const std::vector<cv::Mat>& getLaplac() const
    {
        return m_vecLaplac;
    }

    // 获取掩码金字塔
    const std::vector<cv::Mat>& getMask() const
    {
        return m_vecMask;
    }

private:

    // 构建平均梯度网格矩阵
    static void __makeMeanFocus(const cv::Mat& matImage, cv::Mat& matMeanFocus, int nGradSize = 100)
    {
        // 只接受CV_8UC3和CV_8UC1格式的图像
        if (matImage.type() != CV_8UC3 && matImage.type() != CV_8UC1)
            return;

        // 计算网格数
        int nGradRows = matImage.rows / nGradSize;
        if (matImage.rows % nGradSize)
            ++nGradRows;
        int nGradCols = matImage.cols / nGradSize;
        if (matImage.cols % nGradSize)
            ++nGradCols;

        matMeanFocus = cv::Mat(nGradRows, nGradCols, CV_32FC1, cv::Scalar::all(0.0));

        // 计算每个网格的平均梯度
        for (int nRow = 0; nRow < nGradRows; ++nRow)
        {
            for (int nCol = 0; nCol < nGradCols; ++nCol)
            {
                int nOffsetX = nCol * nGradSize;
                int nOffsetY = nRow * nGradSize;
                int nWidth = nOffsetX + nGradSize > matImage.cols ? matImage.cols - nOffsetX : nGradSize;
                int nHeight = nOffsetY + nGradSize > matImage.rows ? matImage.rows - nOffsetY : nGradSize;

                cv::Mat matImageROISrc = matImage(cv::Rect(nOffsetX, nOffsetY, nWidth, nHeight));
                cv::Mat matImageROIDst;

                // 将BGR转换为灰度图像
                if (matImage.type() == CV_8UC3)
                {
                    matImageROIDst = cv::Mat(matImageROISrc.size(), CV_8UC1);
                    cv::cvtColor(matImageROISrc, matImageROIDst, cv::COLOR_BGR2GRAY);
                }
                else
                {
                    matImageROIDst = matImageROISrc.clone();
                }

                cv::GaussianBlur(matImageROIDst, matImageROIDst, cv::Size(3, 3), 0, 0);
                cv::Mat matImageROISobel;
                cv::Sobel(matImageROIDst, matImageROISobel, CV_8UC1, 1, 1, 3);

                matMeanFocus.at<float>(nRow, nCol) = cv::mean(matImageROISobel)[0];
            }
        }
    }

    // 构建高斯金字塔
    static void __makeGaussian(const cv::Mat& matImage, std::vector<cv::Mat>& vecGaussian, int nLayerCount = 5)
    {
        vecGaussian.resize(nLayerCount);

        for (int nLayerIndex = 0; nLayerIndex < nLayerCount; ++nLayerIndex)
        {
            if (nLayerIndex == 0)
            {
                vecGaussian[nLayerIndex] = matImage.clone();
            }
            else
            {
                cv::Mat matImageDown;
                cv::pyrDown(vecGaussian[nLayerIndex - 1], matImageDown);
                vecGaussian[nLayerIndex] = matImageDown;
            }
        }
    }

    // 构建拉普拉斯金字塔
    static void __makeLaplac(const std::vector<cv::Mat>& vecGaussian, std::vector<cv::Mat>& vecLaplac)
    {
        vecLaplac.resize(vecGaussian.size());

        for (int nLayerIndex = vecLaplac.size() - 1; nLayerIndex >= 0; --nLayerIndex)
        {
            if (nLayerIndex == vecLaplac.size() - 1)
            {
                vecLaplac[nLayerIndex] = vecGaussian[nLayerIndex].clone();
            }
            else
            {
                cv::Mat matImageUp;
                cv::pyrUp(vecGaussian[nLayerIndex + 1], matImageUp, vecGaussian[nLayerIndex].size());
                vecLaplac[nLayerIndex] = vecGaussian[nLayerIndex] - matImageUp;
            }
        }
    }

    // 构建掩码金字塔
    static void __makeMask(const cv::Mat& matMask, std::vector<cv::Mat>& vecMask, int nLayerCount = 5)
    {
        vecMask.resize(nLayerCount);

        for (int nLayerIndex = 0; nLayerIndex < vecMask.size(); ++nLayerIndex)
        {
            if (nLayerIndex == 0)
            {
                vecMask[nLayerIndex] = matMask.clone();
            }
            else
            {
                cv::Mat matMaskDown;
                cv::pyrDown(vecMask[nLayerIndex - 1], matMaskDown);
                vecMask[nLayerIndex] = matMaskDown;
            }
        }
    }
};

// 多聚焦多频段图像融合算法
static bool multiFocusBandBlender(const std::vector<cv::Mat>& vecImage, cv::Mat& matResult, int nGradSize = 100)
{
    if (vecImage.size() < 2)
        return false;

    // 检查每张图片的大小和格式是否一致
    for (int i = 1; i < vecImage.size(); ++i)
    {
        if (vecImage[i].size() != vecImage[i - 1].size())
            return false;

        if (vecImage[i].type() != vecImage[i - 1].type())
            return false;
    }

    // 检查每张图片的格式是否为CV_8UC3和CV_8UC1格式
    for (int i = 0; i < vecImage.size(); ++i)
    {
        if (vecImage[i].type() != CV_8UC3 && vecImage[i].type() != CV_8UC1)
            return false;
    }

    // 为每张图像建立高斯和拉普拉斯金字塔

    std::vector<CImagePyramidInfo*> vecImagePyramidInfo(vecImage.size());
    for (int i = 0; i < vecImage.size(); ++i)
    {
        vecImagePyramidInfo[i] = new CImagePyramidInfo(vecImage[i], nGradSize);
    }

    // 合成聚焦网格矩阵

    std::vector<cv::Mat> vecMeanFocus(vecImagePyramidInfo.size());
    std::vector<cv::Mat> vecBestIndex(vecImagePyramidInfo.size());
    for (int i = 0; i < vecImagePyramidInfo.size(); ++i)
    {
        vecMeanFocus[i] = vecImagePyramidInfo[i]->getMeanFocus();
        vecBestIndex[i] = cv::Mat(vecMeanFocus[i].size(), CV_8UC1, cv::Scalar::all(0));
    }

    for (int nRow = 0; nRow < vecMeanFocus[0].rows; ++nRow)
    {
        for (int nCol = 0; nCol < vecMeanFocus[0].cols; ++nCol)
        {
            // 找梯度最大图片
            float fMax = -1.0;
            int nIndex = -1;
            for (int i = 0; i < vecMeanFocus.size(); ++i)
            {
                float fMeanFocus = vecMeanFocus[i].at<float>(nRow, nCol);
                if (fMeanFocus > fMax)
                {
                    fMax = fMeanFocus;
                    nIndex = i;
                }
            }

            // 标记该图片当前网格为选中
            vecBestIndex[nIndex].at<uchar>(nRow, nCol) = 1;
        }
    }

    // 构建掩码金字塔

    std::vector<cv::Mat> vecMaskPyramid(vecBestIndex.size());

    for (int i = 0; i < vecBestIndex.size(); ++i)
    {
        vecMaskPyramid[i] = cv::Mat(vecImage[i].size(), (vecImage[i].channels() == 1 ? CV_32FC1 : CV_32FC3), cv::Scalar::all(0.0));

        for (int nRow = 0; nRow < vecBestIndex[i].rows; ++nRow)
        {
            for (int nCol = 0; nCol < vecBestIndex[i].cols; ++nCol)
            {
                if (vecBestIndex[i].at<uchar>(nRow, nCol) == 1)
                {
                    int nOffsetX = nCol * nGradSize;
                    int nOffsetY = nRow * nGradSize;
                    int nWidth = nOffsetX + nGradSize > vecImage[0].cols ? vecImage[0].cols - nOffsetX : nGradSize;
                    int nHeight = nOffsetY + nGradSize > vecImage[0].rows ? vecImage[0].rows - nOffsetY : nGradSize;

                    cv::Mat matROI = vecMaskPyramid[i](cv::Rect(nOffsetX, nOffsetY, nWidth, nHeight));
                    matROI.setTo(cv::Scalar::all(1.0));
                }
            }
        }
    }

    for (int i = 0; i < vecImagePyramidInfo.size(); ++i)
    {
        vecImagePyramidInfo[i]->makeMask(vecMaskPyramid[i]);
    }

    // 逐层融合拉普拉斯金字塔

    // 层次遍历
    for (int nLayerIndex = vecImagePyramidInfo[0]->getLaplac().size() - 1; nLayerIndex >= 0; --nLayerIndex)
    {
        cv::Mat matBlendLayer;

        // 图片遍历
        for (int i = 0; i < vecImagePyramidInfo.size(); ++i)
        {
            cv::Mat matLaplac = vecImagePyramidInfo[i]->getLaplac()[nLayerIndex];
            cv::Mat matMask = vecImagePyramidInfo[i]->getMask()[nLayerIndex];

            if (matBlendLayer.empty())
            {
                matBlendLayer = matLaplac.mul(matMask);
            }
            else
            {
                matBlendLayer += matLaplac.mul(matMask);
            }
        }

        if (matResult.empty())
        {
            matResult = matBlendLayer;
        }
        else
        {
            cv::pyrUp(matResult, matResult, matBlendLayer.size());
            matResult += matBlendLayer;
        }
    }

    cv::convertScaleAbs(matResult, matResult, 255);

    for (auto iter = vecImagePyramidInfo.begin(); iter != vecImagePyramidInfo.end(); ++iter)
    {
        delete (*iter);
        (*iter) = NULL;
    }
    
    return true;
}

调用的测试代码:

    // 多聚焦多频段图像融合算法
    {
        cv::Mat matImg0 = cv::imread("full2.jpg");
        cv::Mat matImg1 = matImg0.clone();
        cv::Mat matImg2 = matImg0.clone();
        cv::Mat matImg3 = matImg0.clone();
        //cv::cvtColor(matImg1, matImg1, cv::COLOR_BGR2GRAY);
        //cv::cvtColor(matImg2, matImg2, cv::COLOR_BGR2GRAY);
        //cv::cvtColor(matImg3, matImg3, cv::COLOR_BGR2GRAY);

        const int nGradSize = 50;

        // 随机模糊一些区域
        int nGradRows = matImg0.rows / nGradSize;
        int nGradCols = matImg0.cols / nGradSize;
        cv::RNG rng1(1);
        for (int i = 0; i < 100; ++i)
        {
            int nRow = rng1.uniform(0, nGradRows);
            int nCol = rng1.uniform(0, nGradCols);

            cv::Mat matROI = matImg1(cv::Rect(nCol * nGradSize, nRow * nGradSize, nGradSize, nGradSize));
            cv::GaussianBlur(matROI, matROI, cv::Size(5, 5), 2.0);
        }
        cv::RNG rng2(2);
        for (int i = 0; i < 100; ++i)
        {
            int nRow = rng2.uniform(0, nGradRows);
            int nCol = rng2.uniform(0, nGradCols);

            cv::Mat matROI = matImg2(cv::Rect(nCol * nGradSize, nRow * nGradSize, nGradSize, nGradSize));
            cv::GaussianBlur(matROI, matROI, cv::Size(5, 5), 2.0);
        }
        cv::RNG rng3(3);
        for (int i = 0; i < 100; ++i)
        {
            int nRow = rng3.uniform(0, nGradRows);
            int nCol = rng3.uniform(0, nGradCols);

            cv::Mat matROI = matImg3(cv::Rect(nCol * nGradSize, nRow * nGradSize, nGradSize, nGradSize));
            cv::GaussianBlur(matROI, matROI, cv::Size(5, 5), 2.0);
        }
        

        int64 llTickStart = cv::getTickCount();

        // 这里用于性能测试
        for (int n = 0; n < 1000; ++n)
        {
            std::vector<cv::Mat> vecImage;
            vecImage.push_back(matImg1);
            vecImage.push_back(matImg2);
            vecImage.push_back(matImg3);
            cv::Mat matResult;
            multiFocusBandBlender(vecImage, matResult, nGradSize);

            cv::imwrite("full2_result.jpg", matResult);
        }

        int64 llTickEnd = cv::getTickCount();
        printf("use %fs \n", (llTickEnd - llTickStart) / cv::getTickFrequency());

        return 0;
    }

运行效果:

原图:


image.png

生成的三张随机区域模糊图:
图1:


image.png

图2:


image.png

图3:


image.png

合成的效果图:


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容