关于Python全景图像拼接的探索和实践(其中包括对Opencv Stitcher类、PhotoShop MergeImage插件、PTGui Pro12全景图拼合软件的一些横向比较和探究)

        图像拼合技术其实很早已经出来,发展到今天已经是很成熟的技术了,我接触到最早的普及大众的运用,就是IPHONE自带相机上的全景图片拍摄,聪明的网友在运用一些技巧后,甚至能拍摄一张有多个相同人物不同姿势的拼合照片。当然了,当手机还没有像现在这么普及的时代,Photoshop、PTGui之类的PC上拼图软件有很多。今天,我们就来探究下这些软件的原理,其中也有我参考一些经典的python例子,并加入自己想法的代码,欢迎大家捧个场。

        不多说,我们先看看这些软件的实际效果:

f01339e5bd144093b398659c93a9ab8b.jpeg7c716b2e0cbd4d9d8a7a74b98b93168f.jpeg

--------------------------------------------------------  分割线--------------------------------------------------------------

e24d14c2acda47af87645784b9902afb.jpeg444d3386120141e99b23dabd7e52ff98.jpeg

--------------------------------------------------------  分割线------------------------------------------------------------- 

dd21e3f9806b4773b11e49468c80df5a.jpeg

58ae4721977c439e92337e241fd8e901.jpeg

--------------------------------------------------------  分割线--------------------------------------------------------------

        从以上的效果图可以看出,Opencv的Stitcher类和PTGUI Pro 12的效果有点相似,只是PTGUI是Opencv的Stitcher类的优化版,PTGUI很好的解决了Opencv的图像拉伸变形的问题,建筑物等一些图像边缘处理的比较平滑,而原版的opencv处理平直的边缘被拉伸成了圆弧状,如下图的红圈标示的变形:

2c5c8038ed73470eb440669eb550f62c.jpeg

         PTGUI作为一个收费不菲的国外拼图软件,在拼图的细节上做足了功夫,可以在每个细节步骤上微调参数(包括拍摄设备的参数、自定义匹配点、拼图遮罩、曝光、HDR拼接等),适合专业图像处理的人士使用,在上手难度上,不如opencv。

        Opencv的Stitcher类是我见过的封装的最好的一个图像类,使用如下的代码,就可以传入任意数量、位置、大小的图片,就可以拼接出比较理想的图片。

import cv2 as cv
import numpy as np
import os
import time

# 支持读取中文目录图片的imread函数
def cv_imread(file_path):
    return cv.imdecode(np.fromfile(file_path,dtype=np.uint8),cv.IMREAD_COLOR)

starttime = time.time()
imgs = []
#替换自己的存放图片文件夹路径即可
folder_path = '2.全景拼接图片Python版\\test10'
# 遍历文件夹中的每个文件
for filename in os.listdir(folder_path):
    if filename.endswith(('.png', '.jpg', '.jpeg')):  # 检查文件扩展名
        img_path = os.path.join(folder_path, filename)
        # 读取拼接图片
        image = cv_imread(img_path)
        imgs.append(image)


# 把图片拼接成全景图
stitcher = cv.Stitcher.create()
(status, pano) = stitcher.stitch(imgs)

# 显示所有图片
if status != cv.Stitcher_OK:
    print("不能拼接图片, error code = %d" % status)
print("拼接成功.")
# 输出图片
cv.imwrite("stitcher_finished.jpg",pano)
endtime = time.time()
print(f"拼合运算时间{endtime - starttime}秒!")
cv.imshow('pano', pano)
cv.waitKey(0)

        但是Stitcher类封装的太好了,而且只实现了python调用Stitcher的接口(opencv官方网站中的以下链接:https://github.com/opencv/opencv/blob/4.x/samples/python/stitching.py),而没有导出细节,如匹配、拼合函数,如果你不去编译opencv的c++源码,只是使用python调用的话很多东西都不能自定义、不能简化、不能优化代码。在网上找了半天,发现github有一个名为stitching的Python包,它基于OpenCV的stitching模块,并受到了stitching_detailed.py(opencv官方网站中的以下链接:https://github.com/opencv/opencv/blob/4.x/samples/python/stitching_detailed.py) Python命令行工具的启发开发,这个包有一个jupyter的调用例子(https://github.com/OpenStitching/stitching_tutorial/blob/master/docs/Stitching%20Tutorial.md),可以使用opencv官方python接口拼接图片,大家可以研究下。顺带还有一个老外自己实现的拼接例子(https://github.com/linrl3/Image-Stitching-OpenCV)一并提供给大家。

        另外通过上图看到,PhotoShop的PhotoMerge插件(在文件菜单-自动-PhotoMerge拼接选项),拼接方法不同于opencv,感觉使用了SIFT、单应性矩阵变换,等方法拼接了图片,因为是矩阵变换出来的,所以拼接后的图片并不是很对称,但是却没有opencv拼接物体边缘变形的问题,另外这个PhotoMerge插件做了自己的UI,并申请了自己的adobe的专利拼接技术,adobe的程序员John Peterson使用了jsx编写(adobe在新版中弃用了这个格式)

JSX(JavaScript XML)是一种JavaScript的语法扩展。它允许你在JavaScript代码中
书写类似XML或HTML的标记语言。JSX主要与React库一起使用,用于定义React组件的结构
和内容。通过使用JSX,开发者可以在JavaScript代码中以一种声明式的方式描述UI组件
的外观。

JSX代码在运行之前需要通过转译器(如Babel)转换为普通的JavaScript代码,因为浏览
器或JavaScript引擎本身并不直接理解JSX语法。转译过程会将JSX中的标记转换为React
函数调用,这些调用生成React元素(React elements),React库随后使用这些元素来
构建和管理DOM

说白了就是javascript的另一种xml格式,通过一些检测软件(Process Monitor),可以找到脚本的位置。

786eb4a1049b413d870005f508e0c61f.jpeg

        这个脚本可以用任何文本编辑器打开,我使用vscode打开它,可以看到它的源码,如果要独立运行,它还有一些基类jsx文件,在其他文件夹,可以用上面同样方法找到它们,但是你如果想提取出来单独使用,比如二次打包,有点难度,因为且不说它有专利版权,而且它的很多API也是封装在photoshop程序里的,基类也只是调用photoshop的API函数,但是任何东西我们只要搞懂了原理,改进优化它并不难。这个专利说明文档在google patents找到的,链接如下(https://patentimages.storage.googleapis.com/3e/4e/da/616458f3968095/US20080143820A1.pdf),我在csdn会上传机翻的中文稿等所有关于这本文的所有资源给大家下载,有兴趣的小伙伴可以去看看,你们的关注就是我的源动力。

        至于怎么用vscode调试这个脚本学习,需要安装vscode的ExtendScript Debugger插件(https://marketplace.visualstudio.com/items?itemName=Adobe.extendscript-debug),这个链接下面的英文说明已经说了很详细了,如果不想仔细阅读,其实也很简单,不需要设置vscode的debug中的launch.json,那是老方法了,新的方法是,先安装ExtendScript Debugger插件,然后把photomerge.jsx文件添加到vscode项目中,然后先打开你的photoshop主程序,然后在vscode中打开photomerge.jsx,在你需要的位置下断点后,右击里面的代码,弹出如下菜单:
45f6a9be88114a179175bfe6585f7be2.jpeg

在vscode顶部会弹出一个菜单, 选择你的photoshop即可(我之前安装过另一个版本的ps,所以有两 个,请以自己为准)

23a6c53a8f4b44fe997853ad727ef098.jpeg

这个工具条是debug工具栏,如需运行,请按第一个三角按钮,如需重新调试请按最后一个按钮断开后,使用之前的方法重新右键菜单,设置photoshop版本后调试,这样不容易死机 (这个bug在ExtendScript Debugger插件介绍里有提到) ,如死机请重启photoshop,然后重新尝试上面的方法。

2fbfdd239ead4507a723afb842b4a042.jpeg可以看到已经中断下来了,再给大家看个图,如下图,可以调试了。(另外说一句,中断后可能vscode可能没反应,不是死机,只是要到PhotoShop中操作脚本UI,来进入接下来的代码) b663a62e29184539a88f84f67eff6d76.jpeg

        因为现在有了chatgpt等一众AI辅助,我们阅读新的代码的速度能提高很多,把函数丢到AI中去,就能得到算法框架和详细的算法注释,这点在使用IDA等反编译工具逆向代码时,最能体现出来,把IDA的繁琐的伪代码给AI,chatgpt马上会给你定位到可能的关键代码,这就是时代的红利。

        废话不多说,使用AI阅读了一遍PhotoMerge插件的javascript代码后,对这个脚本的框架有了一定的认识,结合刚刚的老外的专利文档,我们可以知道插件的大概思路:

        先用PhotoShop的API读取传入的所有图片-->然后根据图片内容使用sift算法,对齐每张图片,并把矩阵变形后的图片(可能是不规则四边形、也可能是其他形状)的多个角点,使用matlab格式,保存在一个数据层中(因为这个老外程序员使用matlab建的模型)-->读取每张图片的多个角的角点,求出所有相交图片的最大重合区域,并把这个相交区域的角点记录在数据层中,然后通过PhotoShop的自带混合蒙版功能,把相交的图层,在传入的原始图片上做蒙版,接着使用了矩形融合技术(一共使用了两种技术,一种就是矩形合并/快速合并,另一种是所谓的高级图层合并),使相交角点区域的平滑过渡,最后把这些层堆叠在一起,做成psd的工程文件方便人工编辑。

1.大致的程序流程: 

59622f4d9cb6428ca9a537f7336c3bd9.jpegc7c26725c7764968a034d146ee34b845.jpeg

2.这个photomerge.advancedBlending变量控制了两种合并模式切换281a593f98c049bcb7922f17fe53c52a.jpeg

3.这个矩形融合的原理示意图:

a.先把如下3张子图通过sift算法拼合,得到相交区域(可以看作3个重叠的矩形)角点:

c6ba896adf134ea1a9ac2225e49639b8.jpeg

b.把这些相交矩形按对角线分割(得到三角形或梯形)

26cec1c9de0740eda670e25bed110e01.jpeg

c.然后沿着这些分割对角线,做图层的渐变线,达到平滑过渡的目的:

d4549c49335a49a5aacc02cbf55a6f52.jpeg

d.矩形融合算法的历史:


图片矩形融合算法(也可能被称为图像拼接或图像合成算法)的概念并不是一个单一的时间点
提出的。这个领域随着图像处理、计算机视觉和图形学的发展而逐渐演进,特别是自20世纪80年代
以来,随着计算机技术的进步,相关研究和算法的发展加速了。

在最早期,图像融合的概念主要用于卫星图像和医学图像,这些技术主要是为了在一个视图中结合
多个图像的不同信息。进入90年代和21世纪,随着个人计算机的普及和图像处理软件的发展,图像
融合技术开始被广泛用于摄影艺术、电影制作、虚拟现实等领域。

具体到矩形图像融合,它指的是将两个或多个矩形图像区域通过算法合成为一个无缝的整体图像。
这种技术在20世纪90年代末到21世纪初开始获得关注,特别是随着全景图像拼接技术的发展。
全景图像拼接是矩形图像融合应用中的一个典型例子,它要求不同图像之间在边缘处平滑过渡,
以创建看起来连贯的单一场景。

最具有里程碑意义的算法和技术,如SIFT(尺度不变特征变换)算法,是在2004年提出的,用于
图像特征的匹配和图像融合过程中的关键点检测。但需要注意的是,图像融合技术的发展是一个
持续的过程,许多算法和技术都在不断地进步和演化中。

        这个脚本代码,因为使用了大量的PhotoShop API,所以想要直接使用它的javascript代码达到类似的效果很难。不难看出,除了它最核心的高级融合算法,因为封装关系,我们不得而知,矩形融合技术使用其他语言实现并不是很难,而且我们使用python进行sift拼合,并获取四边形相交区域,进行所谓的矩形融合都是可以实现的,更何况我们还有AI外援,为了不增加文章篇幅,让大家看的太累,我把重要的这些算法的细节给大家介绍下。(PS:我只是抛砖引玉,因为可能有专利的问题,我只大概讲下思路,至于实现代码可以参见文后的链接,今天我们只讲技术,望大家借鉴时当心版权问题,当然我代码和adobe的脚本实现上并不相同,只是原理差不多而已)

代码实现:

1.读取图片主函数show:

函数说明:负责历遍读取一个文件夹目录中的所有图片(图片名称的命名请按照从左到右,依次变大的顺序),然后每次传入左、右两张图片给MergeImage调用。

def show():

    start_time = time.time()
    # 设置包含图片的文件夹路径
    folder_path = '2.全景拼接图片Python版\\test1'
    print("正在合并的图片目录: ",folder_path)
    #print(folder_path)

    #img数组和索引号
    img = []
    img_index = 0
    img1 = None
    img2 = None
    result_img = None

    # 遍历文件夹中的每个文件
    for filename in os.listdir(folder_path):
        if filename.endswith(('.png', '.jpg', '.jpeg')):  # 检查文件扩展名
            img_path = os.path.join(folder_path, filename)
            print("图片序号:" + str(img_index) + ' ,读取图片目录:"' + img_path + '"')
            img.append(cv_imread(img_path))
            if img_index == 0:
                img1 = img[img_index]
            elif img_index <= 1:
                img2 = img[img_index]
                #result_img, vis = MergeImage(img1,img2)
                result_img = MergeImage(img1,img2)
                cv_imwrite(result_img,"10_分步合并_第1轮.jpg",debug)
                print("第1次合并已完成!")
            elif img_index >= 2:
                img1 = result_img
                img2 = img[img_index]
                #result_img, vis = MergeImage(img1,img2)
                result_img = MergeImage(img1,img2)
                cv_imwrite(result_img,"10_分步合并_第" + str(img_index) + "轮.jpg",debug)
                print("第" + str(img_index) + "次合并已完成!")

            #oldimg_height,oldimg_width = img[img_index].shape[:2]
            img_index += 1
    cv.imwrite("out_finish.jpg",result_img)
    end_time = time.time()
    print(f"共耗时: {end_time - start_time} 秒!")

2.建立一个Stitcher的自定义类(这个类我参考了网上的一些通用Python例子,如以下链接:https://www.cnblogs.com/lqerio/p/11601951.html,在这里谢谢这位博主!),并添加多个函数:

A.  def stitch(self, imgs, ratio=0.60, reprojThresh=4.0):

      函数说明:合并图片的主函数,调用下面的所有方法来计算。参数imgs是MergeImage函数传入 

      的左、右两张图片,ratio参数是控制融合的,范围大概在0.4-0.75之间,默认取0.6

B.  def get_corners(self, imgs, M):

       函数说明:获得矩阵变形后的角点坐标,通过stitch函数中的这句调用      

# 使用新函数计算合并后图像的尺寸
x_min, y_min, x_max, y_max,total_width, total_height = self.get_corners(imgs, M)

     通过获得结果,我们可以用下面的代码,把矩阵变换后的左、右图(很多时候矩阵变换后的图片,都是缺失的上部和右边部分的,所以需要平移到合适的位置),平移到拼合图片相应位置方便拼合。

# 根据新计算的尺寸调整位移矩阵
translation_dist = [x_min, y_min]
H_translation = np.array([[1, 0, translation_dist[0]], [0, 1, translation_dist[1]], [0, 0, 1]])

# 因为使用的是先传左图再传右图给imgs,所以生成的M变量参考是img1,需要使用np.linalg.inv(M)逆矩阵算出img2实际坐标,然后通过计算出的H_translation.dot平移。
resize_img2 = cv.warpPerspective(img2,H_translation.dot(np.linalg.inv(M)),(total_width, total_height))

#.....省略其他代码......

# 获取img1和img2的尺寸
h2, w2 = resize_img2.shape[:2]

#把img1的尺寸和矩形变换后的img2设置成一样,方便之后的mark掩膜操作
# 检查img1是彩色图像还是灰度图像,并据此创建新图像
if len(img1.shape) == 3:
  # 彩色图像: 使用三通道
  resize_img1 = np.zeros((h2, w2, 3), dtype=img1.dtype)
else:
# 灰度图像: 使用单通道
resize_img1 = np.zeros((h2, w2), dtype=img1.dtype)
)

# 将img1放置在新图像的左上角,重新设定img1大小尺寸,并修正偏移
resize_img1[translation_dist[1]:h1+translation_dist[1], translation_dist[0]:w1+translation_dist[0]] = img1

  a.矩阵变换并平移后的右图效果:   93f35d6febd041c499bb89c46d8cebb8.jpeg

C.def find_real_corners(self, img, save_path, debug):

此函数说白了找到上面变形平移后左、右图的实际外框角点坐标,并用线画出来外框。

def find_real_corners(self, img, save_path, debug):
        # 将输入图像转换为灰度图像
        gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
        
        # 应用阈值处理:将灰度图像中的所有非黑色区域转换为白色,创建一个二值图像
        _, thresh = cv.threshold(gray, 1, 255, cv.THRESH_BINARY)

        # 在二值图像中查找外部轮廓
        contours, _ = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
        
        # 从所有找到的轮廓中选择面积最大的一个
        cnt = max(contours, key=cv.contourArea)

        # 调用 find_corners_of_contour 函数来找到这个轮廓的四个角点
        corners = self.find_corners_of_contour(cnt)

        if debug:
            for point_index in range(4):
                cv.circle(img,corners[point_index],10,(0,0,255),-1) 
                #print("point_index:",point_index)
                if point_index <3:
                    cv.line(img, corners[point_index], corners[point_index + 1], (255, 0, 0), 5)  # 蓝色线,宽度为5
                else:
                    cv.line(img, corners[point_index], corners[0], (255, 0, 0), 3)


            # 保存经过处理的图像(如果在处理过程中对 img 进行了绘制,例如标记角点)
            cv_imwrite(img,save_path,debug)

        # 打印找到的角点坐标
        #print(corners)
        log_print(debug,"find_real_corners函数获得的坐标:",corners)

        # 返回找到的角点坐标
        return corners

b49e980aa4eb42c48f8ec9fbc51363e4.jpeg

C.  def sort_corners(self, corners):

      此函数的作用是通过已知的角点,按从左上顺时针依次理顺角点顺序,方便之后设置两图相交部分的对角线使用(对角线连接的左上和右下两点最关键)     

def sort_corners(self, corners):
        log_print(debug,"排序前的角点:",corners)

        # 去除重复的点
        corners_tuple = list(map(tuple, corners))
        unique_corners_tuple = list(set(corners_tuple))
        #unique_corners = np.array(unique_corners_tuple)
        unique_corners = [list(x) for x in unique_corners_tuple]  # 转换成列表的列表,便于后续处理
        
        # 计算质心
        centroid = np.mean(unique_corners, axis=0)
        
        # 定义排序的角度函数
        def sort_angle(c):
            return np.arctan2(c[1] - centroid[1], c[0] - centroid[0])
        
        # 根据角度进行排序
        sorted_corners = sorted(unique_corners, key=sort_angle)
        log_print(debug,"排序后的角点:",sorted_corners)
        
        # 找到最左上角的点并以其为起点进行顺时针排序
        #top_left = min(sorted_corners, key=lambda c: (c[1], c[0]))
        top_left = sorted_corners[0]
        #print(top_left)
        top_left_index = sorted_corners.index(top_left)
        sorted_corners = sorted_corners[top_left_index:] + sorted_corners[:top_left_index]
        
        # 转换成 cv.polylines 可以接受的格式
        sorted_corners_np = np.array(sorted_corners, dtype=np.int32).reshape((-1, 1, 2))
        
        return sorted_corners_np

排序前后的效果图: 

ec78447cbf4a414fb69a6714df0143f4.jpeg

D.  def get_mark(self,img_shape,rect_corners):

此函数有三个作用:

def get_mark(self,img_shape,rect_corners):

        if imgSetBool:#微调模式
            diag_slope_offset=imgSetParam[count]["diag_slope_offset"]
            intercept_offset = imgSetParam[count]["intercept_offset"]
            gradient_width=imgSetParam[count]["gradient_width"]
            print("微调参数:","diag_slope_offset(斜率偏移微调):",diag_slope_offset," ,intercept_offset(截距偏移微调):",intercept_offset," ,gradient_width(渐变宽度微调):", gradient_width)
        else:#默认模式
            diag_slope_offset=0
            intercept_offset = 0
            gradient_width=100
        mark,top_left,bottom_right = self.create_diagonal_gradient_mask(img_shape, rect_corners, diag_slope_offset,intercept_offset, gradient_width)

        rect_corners_reshaped = np.array([rect_corners])

        

        mark = self.fill_uncovered_area(mark)

        # 画出对角线((338, 210) (2632, 3997))
        log_print(debug,"对角线坐标:",tuple(rect_corners[0]),tuple(rect_corners[2]))
        if debug:
            cv.polylines(mark, rect_corners_reshaped, True, (255, 255, 255), 3)
            cv.line(mark, top_left, bottom_right, (255, 255, 255), 3)

        return mark

1.设置对角线的斜率和截距微调参数,给拼合不好的一些例子,手动调节拼合位置的可能。

2.调用create_diagonal_gradient_mask(img_shape, rect_corners, diag_slope_offset,intercept_offset, gradient_width)函数计算斜率和截距,并看情况传入上面的手动参数。

3.调用fill_uncovered_area(mark)函数画出,上面求出对角线,所覆盖的mark掩膜,给之后的左、右两图合并使用。具体如下图所示: 

a.可以看到下图是非手动微调模式下的,对角线掩膜图,渐变区域沿着,白线所画的相交区域的对角线方向覆盖

fffcce8bb22e489a855aded5f5357c3b.jpeg

下图是手动微调模式的自定义渐变区域,这样的切线可以保证避开一些关键区域图像的拼合情况。 b32f479f77304bb8b9c1aa783f475a65.jpeg

手动调节的效果见最右边的绿色相交多边形 ,可以看到上部的天花板边缘右明显的一个错位(这样可以避开吊灯的拼合错位问题),这在debug模式下(代码开头的debug = False即可关闭调试输出)比较明显,关掉debug后,因为没有辅助线拼合的会更好。

c0490e722fcc45f1936402aca969132a.jpeg

F. def drawMatches(self, img1, img2, kp1, kp2, matches, mask)

这个函数的作用是画出匹配点的对照图,我更新了原帖中的这个函数的算法,原帖中的此函数是对照左图和右图相反,感觉很反人类,所以还是换过来(需要stitch函数配合修改才行),并添加了匹配点的序号以供对比,效果图如下:

a21d88e3586840839ad4e62c68bdf6ee.jpeg

def drawMatches(self, img1, img2, kp1, kp2, matches, mask):
        print('5.画出匹配点...')
        (hA, wA) = img1.shape[:2]
        (hB, wB) = img2.shape[:2]
        vis = np.zeros((max(hA, hB), wA + wB, 3), dtype='uint8')
        vis[0:hA, 0:wA] = img1
        vis[0:hB, wA:] = img2


        # 设置文本参数
        text = ""  # 要写入的文本
        org = (0, 0)  # 文本框左下角的坐标
        fontFace = cv.FONT_HERSHEY_SIMPLEX  # 字体
        fontScale = 0.3  # 字体大小
        color = (255, 0, 0)  # BGR颜色,这里为蓝色
        thickness = 1  # 线条粗细

        index_id = 0

        # 创建一个全透明的图层(遮罩),大小和原图相同
        overlay = np.zeros_like(vis, dtype='uint8')

        for (trainIdx, queryIdx), s in zip(matches, mask):
            if s == 1:
                index_id += 1
                ptA = (int(kp1[queryIdx][0]), int(kp1[queryIdx][1]))
                ptB = (int(kp2[trainIdx][0]) + wA, int(kp2[trainIdx][1]))
                cv.circle(vis,ptA,1,(0,0,255),-1) 
                cv.circle(vis,ptB,1,(255,0,0),-1) 
                cv.line(overlay, ptA, ptB, (0, 255, 0), 1)
                cv.putText(vis, str(index_id), ptA, fontFace, fontScale, color, thickness)
                cv.putText(vis, str(index_id), ptB, fontFace, fontScale, color, thickness)

        # 将遮罩按照透明度混合到原图上
        # cv2.addWeighted(源图1, 权重1, 源图2, 权重2, 伽马值)
        # 权重范围从0到1,权重1控制原图的透明度,权重2控制线条的透明度
        alpha = 0.2  # 线条的透明度
        cv.addWeighted(overlay, alpha, vis, 1 - alpha, 0, vis)

        return vis

 G.def create_diagonal_gradient_mask(sorted_corners)

这个函数是计算手工微调的函数,可以单独放在一个python文件中使用,只要先把代码开头的微调变量(imgSetBool)关闭,debug开关打开,先自动生成一遍没有微调过的调试图片,并使用window自带的画图软件打开如下的debug图片(选第10步的最后一轮比较好,因为所有的图片坐标都有)

261a43c1f4134829a1ee4b2ac4ec3017.jpeg

4edc9a6a27a843f1825f92a1701d4043.jpeg

获得两点坐标后,代入函数运行就能得到对应的斜率和截距。(不好意思,没有做成更好的UI,等有空,以后慢慢完善吧!) 

代码如下:

import numpy as np

def create_diagonal_gradient_mask(sorted_corners):

        # 左上角的点通常是第一个点,右下角的点是x和y坐标的最大值
        top_left = sorted_corners[0]  # 将 top_left 调整为 (x, y) 格式
        print("左下角坐标:",top_left)

        # 右下角的点应该是x最大或y最大的点,这取决于具体定义,这里我们尝试一个通用的逻辑
        bottom_right = sorted_corners[1]

        print("右下角坐标:",bottom_right)
        
        # 计算对角线的斜率和截距
        diag_slope = (top_left[1] - bottom_right[1]) / (bottom_right[0] - top_left[0])
        intercept = -top_left[1] - diag_slope * top_left[0]

        print("微调后的对角线公式: Y = " + str(diag_slope) + "X + " + str(intercept))

        return diag_slope,intercept

#输入新的坐标点
sorted_corners = [[2225,1033],[3089,5025]]
#输入之前计算出的老斜率和截距
# 原始对角线公式: Y = -2.272974442631865X + 2359.8075040783037
old_diag_slope = -2.272974442631865
old_intercept = 2359.8075040783037

new_diag_slope,new_intercept = create_diagonal_gradient_mask(sorted_corners)

diag_slope_offset= new_diag_slope
intercept_offset= new_intercept - old_intercept
print("diag_slope_offset=",diag_slope_offset,"intercept_offset=",intercept_offset)
# 微调后的对角线公式: Y = -1.3849557522123894X + -66.19911504424772
#输入新的坐标点
sorted_corners = [[2225,1033],[3089,5025]]
#输入之前计算出的老斜率和截距
# 原始对角线公式: Y = -2.272974442631865X + 2359.8075040783037
old_diag_slope = -2.272974442631865
old_intercept = 2359.8075040783037

如上:sorted_corners为手工微调的两个坐标点,因为计算的是偏移,所有old_diag_slope ,old_intercept两个老的斜率和截距也需在之前的调试信息中找到写入进去,写入时请注意合并的轮次,是要当前需要微调的轮次。

0d4baa9530374a40941a0c1d5da4972e.jpeg

运行后的结果:

45ab40e48263483e8461b027998ddc09.jpeg

填入imgSetParam参数,注意gradient_width是控制渐变范围的,可以设的小一点,其他默认就行,如果想默认,请把上面count3的代码覆盖count4就行:

09338334dd544c7b914b2d54c34b74d7.jpeg

最后的生成结果是"out_finish.jpg",放一张微调后的大图给大家看看效果。

8d2b3ea2a5674aeea7cebaec9210959c.jpeg

说一下我这个算法的优缺点吧:

首先看下图的算法运行时长表:

dc0f4b5a645247838dcd5732cb7ec6e9.jpeg

        因为是纯Python代码,所以运行时长在所有软件里最长,拼合的时长还受到拼接图片的复杂程度的影响,可以看到在test6有不能运行的情况,研究后发现,因为我的拼图算法是从左向右拼接,所以,如果右边的图片有一些近景出现,就会导致图片矩阵变换不过来,超限的问题出现,解决的方法在test10里,把最后的3图切割一下,去掉部分近景,问题解决 。

        可以通过上面的手动微调效果图看到,在拼合大部分没有近景的图片时,效果还是可以让人接受的,另外这个代码也实现了多张图的叠加拼接,在使用PhotoShop类似的矩形融合后,拼接的接缝并不明显(这点从木地板的接缝就可以看出),如果在使用sift函数时,调整参数控制特征点数量,或者在匹配特征时,使用FLANN匹配器,并减少checks以提高速度后,程序还有优化的可能性存在。

        这篇文章说了很多,所以篇幅太长,看到这里的都是真爱,写完这些字就是1.43w字了,所以放不上所有代码,如需下载,请移步到如下链接:https://download.csdn.net/download/donglxd/89106983,之前大家也都给我很卖力的关注着,希望大家还是一如既往的支持我。谢谢大家的观看,再见!

 

 

 

  • 33
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
全景图像拼接是将多张拍摄自同一地点、视角不同的照片拼接成一张大的全景图像。OpenCV是一个非常强大的计算机视觉库,可以用来实现全景图像拼接。 下面是实现全景图像拼接的基本步骤: 1. 加载图片。使用OpenCV的cv2.imread()函数加载图片。 2. 特征点检测。使用OpenCV的SIFT、SURF、ORB等算法检测每张图片的特征点。 3. 特征点匹配。使用OpenCV的FLANN或者Brute-Force算法对特征点进行匹配。 4. 计算单应性矩阵。使用OpenCV的findHomography函数计算单应性矩阵,将当前图片与上一张图片进行拼接。 5. 图像拼接。使用OpenCV的warpPerspective函数将当前图片进行透视变换,然后将图片拼接到上一张图片上。 6. 重复步骤2-5,直到所有图片拼接完成。 下面是一个基于OpenCV实现全景图像拼接的示例代码: ```python import cv2 import numpy as np # 加载图片 img1 = cv2.imread('img1.jpg') img2 = cv2.imread('img2.jpg') # 特征点检测 sift = cv2.xfeatures2d.SIFT_create() kp1, des1 = sift.detectAndCompute(img1, None) kp2, des2 = sift.detectAndCompute(img2, None) # 特征点匹配 bf = cv2.BFMatcher() matches = bf.knnMatch(des1, des2, k=2) good_matches = [] for m, n in matches: if m.distance < 0.75 * n.distance: good_matches.append(m) src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) # 计算单应性矩阵 M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) # 图像拼接 result = cv2.warpPerspective(img1, M, (img1.shape[1] + img2.shape[1], img1.shape[0])) result[0:img2.shape[0], 0:img2.shape[1]] = img2 cv2.imshow('result', result) cv2.waitKey(0) cv2.destroyAllWindows() ``` 这段代码实现了两张图片的拼接。你可以使用这个基本的框架,将多张图片进行拼接,从而实现全景图像拼接。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虫鸣@蝶舞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值