首页 > 编程语言 >图像拼接算法技术报告

图像拼接算法技术报告

时间:2023-06-14 19:56:44浏览次数:47  
标签:shape cv 算法 拼接 图像 warpImg img2 img1 col

图像拼接算法技术报告

代码介绍

图像拼接是将多个图像按照一定的顺序和几何变换方法组合在一起,形成一个更大、更完整的图像的过程。通过图像拼接,可以将多个部分图像合并为一个整体,以展示更广阔的视野或提供更全面的信息。

我们先感性地看一组实验结果(静态场景的图像拼接):

左图 右图 拼接结果
lake lake_2 image-20230614090309455

图像拼接的一般步骤:

  1. 特征匹配,对读入的两张图片进行特征点匹配。
  2. 计算右图到左图的单应矩阵,目的是将两张图片放到同一个平面坐标系下。
  3. 将右图乘以单应矩阵之后,便可以将左图拷贝到右图的左侧。
  4. 对两图重叠区域进行渐变处理。

具体是怎么提现的,我们可以直接看代码。

输入:

左图 右图
lan1 lan2
  1. 特征匹配
if __name__ == '__main__':

    pic1 = "./aa.png"
    pic2 = "./bb.png"


    img1 = cv.imread(pic1)
    img2 = cv.imread(pic2)
    # cv.imshow("Image1", img1)
    # cv.imshow("Image2", img2)

    print('img1 Dimensions : ', img1.shape)
    print('img2 Dimensions : ', img2.shape)

    # sift提取特征
    sift = cv.SIFT_create(nfeatures=0, nOctaveLayers=3, contrastThreshold=0.04, edgeThreshold=10, sigma=1.6)
    kp1, describe1 = sift.detectAndCompute(img1, None)
    kp2, describe2 = sift.detectAndCompute(img2, None)

    # FLANN 进行特征点匹配
    FLANN_INDEX_KDTREE = 0
    indexParams = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
    searchParams = dict(checks=50)
    flann = cv.FlannBasedMatcher(indexParams, searchParams)
    match = flann.knnMatch(describe1, describe2, k=2)

    good = []
    for i, (m, n) in enumerate(match):
        if m.distance < 0.6 * n.distance:
            good.append(m)

    result = cv.drawMatches(img1, kp1, img2, kp2, good, None)
    # cv.imshow("matches", result)
    

这一段是用SIFT将左右图的特征点进行提取,然后使用FLANN进行特征点匹配,最后把好的特征点匹配放在good数组中。

image-20230614103851727

  1. 计算右图img2到左图img1单应矩阵,目的是将两张图片放到同一个平面坐标系下。
src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
ano_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
M, mask = cv.findHomography(ano_pts, src_pts, cv.RANSAC, 5.0)
#把img2变到img1的坐标系下,放在一张大图上。
warpImg = cv.warpPerspective(img2, M, (img1.shape[1] + img2.shape[1], max(img2.shape[0], img1.shape[0])))     

这段代码使用右图的特征点ano_pts左图的特征点src_pts的对应关系计算出二者的单应矩阵M。然后我们对右图进行单应矩阵变换,(放在一张大图上进行变换)。

得到的结果warpImg可视化是这样:

image-20230614104001001

之后我们可以将左图img1直接覆盖到warpImg的左区域。(覆盖这个概念下文还会提到,这里也可以对两张图片进行曝光补偿,然后再进行覆盖。使得看起来更加协调)

def direct_stiching(warpImg, img1):
    direct = warpImg.copy()
    direct[0:img1.shape[0], 0:img1.shape[1]] = img1 # 左区域赋值为img
    return direct

direct结果可视化是这样:
lan1.jpgdirect

但是由于一些光线的变化等因素,两张图的色调不一致,导致左图的右边界显得很突兀。

因此我们也可以处理两张图片的重叠区域,本质上是两张图像重叠像素根据距离的一个加权融合。

def optimize_stiching(warpImg, img1):

    # 重叠部分进行过渡处理。
    rows, cols = img1.shape[:2]

    left = 0    # 开始重叠的最左端
    right = cols # 开始重叠的最右端,默认就是img1的最右端

    for col in range(0, cols): # 找到重叠的最左端
        if img1[:, col].any() and warpImg[:, col].any():
            left = col
            break

    res = np.zeros([rows, cols, 3], np.uint8)

    for row in range(0, rows):
        for col in range(left, right):
            # 如果左图没有像素点,就赋值为右图像素点
            if not img1[row, col].any():
                res[row, col] = warpImg[row, col]
            # 如果右图没有像素点就赋值为左图像素点
            elif not warpImg[row, col].any():
                res[row, col] = img1[row, col]
            # 如果都有像素点,就进行二者的权融合,alpha代表距离谁更近,谁的权重更大
            else:
                srcImgLen = float(abs(col - left))
                testImgLen = float(abs(col - right))
                alpha = srcImgLen / (srcImgLen + testImgLen)
                res[row, col] = np.clip(img1[row, col] * (1 - alpha) + warpImg[row, col] * (alpha), 0, 255)

    warpImg[0:img1.shape[0], 0:img1.shape[1]] = res

    return warpImg

可视化

lan1.jpgoptmize

动态物体

重叠区域存在动态物体时,由于重叠区域的加权融合,所以动态物体会出现“鬼影”:

image-20230614104852414

与opencv Image Stich官方函数对比

静态物体

左图 右图 My_result Opencv_result
yun1 yun2 image resultyun
lan1 lan2 image resultlan
lab1 lab2 image resultlab

结果看起来相差不大,但是我的代码还是在某些情况下出现一些鬼影(比如第三张图),初步怀疑是加权那里做的不够好。然后我的代码速度还很慢。

动态物体

当场景中存在动态物体时,前面分析过,我的算法很明显会出现鬼影,但是opencv的代码却没有。

左图 右图
dong1 dong2
My_result(有鬼影) Opencv_result
dong1optmize resultdong

探索

为了解决我代码的鬼影问题,阅读了一下opencv的Stich源码,本来是想着debug一行行地去看的,但是不知道为什么没法进入opencv内部的函数,点击 step into就直接跳到下一行了,查了一下,好像在编译opencv的时候得加上debug参数,还不一定能成功。

核心代码应该是在stich.cpp的第322行:

"Compensate exposure"(曝光补偿)是指对图像进行调整以实现曝光一致性的过程。在拼接全景图像时,不同输入图像可能具有不同的曝光水平,这可能会导致最终拼接结果中出现不连贯或明显的亮度差异。为了解决这个问题,代码中的"Compensate exposure"部分对每个图像进行曝光补偿,以使其在全景图中的曝光水平更加一致。

// Compensate exposure
exposure_comp_->apply((int)img_idx, corners[img_idx], img_warped, mask_warped);
LOGLN(" compensate exposure: " << ((getTickCount() - pt) / getTickFrequency()) << " sec");

继续深入看了一下,不求甚解。大概是计算了一些东西,然后把这个东西应用在每个输入图像上,使得他们的曝光是一致的,然后使用曝光补偿的图片进行拼接。(实在看不懂啊img)

其实说到这也就解释了为什么我的算法会有鬼影,而opencv不会。因为我的是加权平均,所以重叠区域的两张图片都会起作用;但是opencv的是曝光补偿,因此拼接后,左侧图片会直接覆盖warpped右侧图片的左区域(也有可能反过来)。

拿这个demo举个例子:

lan1 lan2

为了证明opencv是进行全局曝光补偿,然后实验图像的直接覆盖,所以如果是右图对warrped左图的覆盖,那么左图中间重叠区域的黑笔涂抹会直接消失。

image-20230614164033229

我们可以看到,实际情况是对左右图进行全局曝光,然后是右图与Warpped左图进行拼接结合下方的AOMI水印也能证明这一点。因此这个实验也解释了为什么opencv没有鬼影的出现。因为它没有使用加权平均。

总结

本篇博客先是介绍了图像拼接的一般流程:

  1. 特征匹配,对读入的两张图片进行特征点匹配。
  2. 计算右图到左图的单应矩阵,目的是将两张图片放到同一个平面坐标系下。
  3. 将右图乘以单应矩阵之后,便可以将左图拷贝到右图的左侧。
  4. 对两图重叠区域进行渐变处理。

然后给出了自己手写方法和opencv官方代码的对比。

我的方法 官方方法
静态场景效果好 静态场景效果好
速度非常慢 速度很快
动态物体会有鬼影 无鬼影

然后在探索小节通过阅读opencv的源码解释了为什么动态物体出现时我的会有鬼影,而opencv stich不会,本质上是算法的思想不同。

我的代码慢的主要原因有两个:

  • 使用Python写的,而官方方法我用的C++
  • 加权处理重叠区域代码,是一个个像素处理的,但其实这里是可以并行的。

Refer

[1]https://www.cnblogs.com/skyfsm/p/7411961.html

[2]https://www.cnblogs.com/empolder-minoz/p/14772234.html

[3]https://docs.opencv.org/4.x/d1/d46/group__stitching.html

附录

my_code.py

# 图像拼接
import cv2 as cv
import numpy as np

MIN = 10


def direct_stiching(warpImg, img1):
    direct = warpImg.copy()
    direct[0:img1.shape[0], 0:img1.shape[1]] = img1
    return direct


def optimize_stiching(warpImg, img1):

    # 重叠部分进行过渡处理。
    rows, cols = img1.shape[:2]

    left = 0    # 开始重叠的最左端
    right = cols # 开始重叠的最右端,默认就是img1的最右端

    for col in range(0, cols): # 找到重叠的最左端
        if img1[:, col].any() and warpImg[:, col].any():
            left = col
            break

    res = np.zeros([rows, cols, 3], np.uint8)

    for row in range(0, rows):
        for col in range(0, right):
            # 如果左图没有像素点,就赋值为右图像素点
            if not img1[row, col].any():
                res[row, col] = warpImg[row, col]
            # 如果右图没有像素点就赋值为左图像素点
            elif not warpImg[row, col].any():
                res[row, col] = img1[row, col]
            # 如果都有像素点,就进行二者的权融合,alpha代表距离谁更近,谁的权重更大
            else:
                srcImgLen = float(abs(col - left))
                testImgLen = float(abs(col - right))
                alpha = srcImgLen / (srcImgLen + testImgLen)
                res[row, col] = np.clip(img1[row, col] * (1 - alpha) + warpImg[row, col] * (alpha), 0, 255)

    warpImg[0:img1.shape[0], 0:img1.shape[1]] = res

    return warpImg


if __name__ == '__main__':

    pic1 = "./dong1.png"
    pic2 = "./dong2.png"


    img1 = cv.imread(pic1)
    img2 = cv.imread(pic2)
    # cv.imshow("Image1", img1)
    # cv.imshow("Image2", img2)

    print('img1 Dimensions : ', img1.shape)
    print('img2 Dimensions : ', img2.shape)

    # sift提取特征
    sift = cv.SIFT_create(nfeatures=0, nOctaveLayers=3, contrastThreshold=0.04, edgeThreshold=10, sigma=1.6)
    kp1, describe1 = sift.detectAndCompute(img1, None)
    kp2, describe2 = sift.detectAndCompute(img2, None)

    # FLANN 进行特征点匹配
    FLANN_INDEX_KDTREE = 0
    indexParams = dict(algorithm=FLANN_INDEX_KDTREE, trees=4)
    searchParams = dict(checks=32)
    flann = cv.FlannBasedMatcher(indexParams, searchParams)
    match = flann.knnMatch(describe1, describe2, k=2)

    good = []
    for i, (m, n) in enumerate(match):
        if m.distance < 0.6 * n.distance:
            good.append(m)

    result = cv.drawMatches(img1, kp1, img2, kp2, good, None)
    # cv.imshow("matches", result)

    # RANSAC:findhomography
    if len(good) > MIN:
        src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
        ano_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
        M, mask = cv.findHomography(ano_pts, src_pts, cv.RANSAC, 5.0)
        warpImg = cv.warpPerspective(img2, M, (img1.shape[1] + img2.shape[1], max(img2.shape[0], img1.shape[0])))       #把img2变到img1的坐标系下,放在一张大图上。

        direct = direct_stiching(warpImg, img1)    # 直接将左图覆盖warpImg的左区域
        optimize = optimize_stiching(warpImg, img1)  # 左图和右图的重叠区域进行加权融合


        # cv.imshow("direct_main", direct)
        # cv.imshow("optimize_main", optimize)

        cv.imwrite(pic1.split(".png")[0] + "direct.png", direct)
        cv.imwrite(pic1.split(".png")[0] + "optmize.png", warpImg)
        print("result saved!")
        cv.waitKey()
        cv.destroyAllWindows()

    else:
        print("not enough matches!")

opnecv_stich.cpp

#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/stitching.hpp"
#include <iostream>
using namespace std;
using namespace cv;
bool divide_images = false;
Stitcher::Mode mode = Stitcher::PANORAMA;
vector<Mat> imgs;
string result_name = "result.jpg";
void printUsage(char** argv);
int parseCmdArgs(int argc, char** argv);

int main(int argc, char* argv[])
{
    int retval = parseCmdArgs(argc, argv);
    if (retval) return EXIT_FAILURE;
    Mat pano;
    Ptr<Stitcher> stitcher = Stitcher::create(mode);
    Stitcher::Status status = stitcher->stitch(imgs, pano);
    if (status != Stitcher::OK)
    {
        cout << "Can't stitch images, error code = " << int(status) << endl;
        return EXIT_FAILURE;
    }
    imwrite(result_name, pano);
    cout << "stitching completed successfully\n" << result_name << " saved!";
    return EXIT_SUCCESS;
}
void printUsage(char** argv)
{
    cout <<
         "Images stitcher.\n\n" << "Usage :\n" << argv[0] <<" [Flags] img1 img2 [...imgN]\n\n"
                                                            "Flags:\n"
                                                            "  --d3\n"
                                                            "      internally creates three chunks of each image to increase stitching success\n"
                                                            "  --mode (panorama|scans)\n"
                                                            "      Determines configuration of stitcher. The default is 'panorama',\n"
                                                            "      mode suitable for creating photo panoramas. Option 'scans' is suitable\n"
                                                            "      for stitching materials under affine transformation, such as scans.\n"
                                                            "  --output <result_img>\n"
                                                            "      The default is 'result34.jpg'.\n\n"
                                                            "Example usage :\n" << argv[0] << " --d3 --mode scans img1.jpg img2.jpg\n";
}

int parseCmdArgs(int argc, char** argv)
{
    if (argc == 1)
    {
        printUsage(argv);
        return EXIT_FAILURE;
    }
    for (int i = 1; i < argc; ++i)
    {
        if (string(argv[i]) == "--help" || string(argv[i]) == "/?")
        {
            printUsage(argv);
            return EXIT_FAILURE;
        }
        else if (string(argv[i]) == "--d3")
        {
            divide_images = true;
        }
        else if (string(argv[i]) == "--output")
        {
            result_name = argv[i + 1];
            i++;
        }
        else if (string(argv[i]) == "--mode")
        {
            if (string(argv[i + 1]) == "panorama")
                mode = Stitcher::PANORAMA;
            else if (string(argv[i + 1]) == "scans")
                mode = Stitcher::SCANS;
            else
            {
                cout << "Bad --mode flag value\n";
                return EXIT_FAILURE;
            }
            i++;
        }
        else
        {
            Mat img = imread(samples::findFile(argv[i]));
            if (img.empty())
            {
                cout << "Can't read image '" << argv[i] << "'\n";
                return EXIT_FAILURE;
            }
            if (divide_images)
            {
                Rect rect(0, 0, img.cols / 2, img.rows);
                imgs.push_back(img(rect).clone());
                rect.x = img.cols / 3;
                imgs.push_back(img(rect).clone());
                rect.x = img.cols / 2;
                imgs.push_back(img(rect).clone());
            }
            else
                imgs.push_back(img);
        }
    }
    return EXIT_SUCCESS;
}

标签:shape,cv,算法,拼接,图像,warpImg,img2,img1,col
From: https://www.cnblogs.com/programmerwang/p/17480796.html

相关文章

  • go语言编写算法
    1、冒泡排序//冒泡排序a:=[]uint8{9,20,10,23,7,22,88,102}fori:=0;i<len(a);i++{fork:=i+1;k<(len(a)-i);k++{ifa[i]>a[k]{a[i],a[k]=a[k],a[i]}}......
  • 简单易学的机器学习算法——K-Means算法
    一、聚类算法的简介  聚类算法是一种典型的无监督学习算法,主要用于将相似的样本自动归到一个类别中。聚类算法与分类算法最大的区别是:聚类算法是无监督的学习算法,而分类算法属于监督的学习算法。  在聚类算法中根据样本之间的相似性,将样本划分到不同的类别中,对于不同的相似......
  • 推荐算法——基于矩阵分解的推荐算法
    一、推荐算法概述对于推荐系统(RecommendSystem,RS),从广义上的理解为:为用户(User)推荐相关的商品(Items)。常用的推荐算法主要有:基于内容的推荐(Content-BasedRecommendation)协同过滤的推荐(CollaborativeFilteringRecommendation)基于关联规则的推荐(AssociationRule-Based......
  • 推荐系统中的常用算法——基于Graph Embedding的GES和EGES
    1.概述相比较于基于CollaborativeFilter算法,基于基础GraphEmbedding模型可以根据用户的行为序列学习出item的embedding,利用item对应的Embedding可以方便计算item与item之间的相似度,并在实践中被证明是卓有成效的方法,在基于基础GraphEmbedding模型,主要包括item2vec,node2vec,deepw......
  • 文本分类fastText算法
    1.概述在深度学习遍地开花的今天,浅层的网络结构甚至是传统的机器学习算法被关注得越来越少,但是在实际的工作中,这一类算法依然得到广泛的应用,或者直接作为解决方案,或者作为该问题的baseline,fastText就是这样的一个文本分类工具。fastText是2016年由facebook开源的用于文本分类的工......
  • 机器学习算法实现解析——libFM之libFM的训练过程概述
    本节主要介绍的是libFM源码分析的第四部分——libFM的训练。FM模型的训练是FM模型的核心的部分。4.1、libFM中训练过程的实现在FM模型的训练过程中,libFM源码中共提供了四种训练的方法,分别为:StochasticGradientDescent(SGD),AdaptiveSGD(ASGD),AlternatingLeastSquares(ALS)和MarkovCh......
  • 挑战数据结构和算法面试题——二叉搜索树的后序遍历
    分析:根据二叉查找树的定义,二叉查找树或者是一棵空二叉树,或者是具有一下特性的二叉树:若它的左子树不为空,则左子树上的所有结点的值均小于根节点的值;若它的右子树不为空,则右子树上的所有结点的值均小于根节点的值;它的左右子树又分别是二叉查找树。结合二叉树的后序遍历,则初始序列的最......
  • 【数据结构和算法面试题】左旋转字符串
    问题分析:本题是常见的旋转字符串的问题,解决的方法是两步旋转的方法:方法:voiddo_reverse(char*p_start,char*p_end){ if(NULL==p_start||NULL==p_end||p_start>p_end)return; chartmp; while(p_start<p_end){ tmp=*p_start; *p_start=*p_end; *p_end......
  • 代码随想录算法训练营第七天| 344.反转字符串 、 541. 反转字符串II、 剑指Offer 05.
     344.反转字符串代码:1voidreverseString(vector<char>&s){23inti=0;4intj=s.size()-1;5while(i<j)6{7charmid=s[i];8s[i]=s[j];9s[j]=mid;1011i++;12......
  • 【数据结构与算法面试题】子数组的最大和
    题目来源“数据结构与算法面试题80道”。问题分析:在数组的每一个位置处保存当前的最大值,当前的最大值组成为:解决方案:intget_max_subarray(int*a,intlength,bool&is_array_ok){ if(NULL==a||length<=0){ is_array_ok=false; return0; } int*p_h_a=(int*......