|
图像拼合技术其实很早已经出来,发展到今天已经是很成熟的技术了,我接触到最早的普及大众的运用,就是IPHONE自带相机上的全景图片拍摄,聪明的网友在运用一些技巧后,甚至能拍摄一张有多个相同人物不同姿势的拼合照片。当然了,当手机还没有像现在这么普及的时代,Photoshop、PTGui之类的PC上拼图软件有很多。今天,我们就来探究下这些软件的原理,其中也有我参考一些经典的python例子,并加入自己想法的代码,欢迎大家捧个场。 不多说,我们先看看这些软件的实际效果:-------------------------------------------------------- 分割线---------------------------------------------------------------------------------------------------------------------- 分割线------------------------------------------------------------- -------------------------------------------------------- 分割线-------------------------------------------------------------- 从以上的效果图可以看出,Opencv的Stitcher类和PTGUIPro12的效果有点相似,只是PTGUI是Opencv的Stitcher类的优化版,PTGUI很好的解决了Opencv的图像拉伸变形的问题,建筑物等一些图像边缘处理的比较平滑,而原版的opencv处理平直的边缘被拉伸成了圆弧状,如下图的红圈标示的变形:  TGUI作为一个收费不菲的国外拼图软件,在拼图的细节上做足了功夫,可以在每个细节步骤上微调参数(包括拍摄设备的参数、自定义匹配点、拼图遮罩、曝光、HDR拼接等),适合专业图像处理的人士使用,在上手难度上,不如opencv。 Opencv的Stitcher类是我见过的封装的最好的一个图像类,使用如下的代码,就可以传入任意数量、位置、大小的图片,就可以拼接出比较理想的图片。importcv2ascvimportnumpyasnpimportosimporttime#支持读取中文目录图片的imread函数defcv_imread(file_path):returncv.imdecode(np.fromfile(file_path,dtype=np.uint8),cv.IMREAD_COLOR)starttime=time.time()imgs=[]#替换自己的存放图片文件夹路径即可folder_path='2.全景拼接图片Python版\\test10'#遍历文件夹中的每个文件forfilenameinos.listdir(folder_path):iffilename.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)#显示所有图片ifstatus!=cv.Stitcher_OK:print("不能拼接图片,errorcode=%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的程序员JohnPeterson使用了jsx编写(adobe在新版中弃用了这个格式)。JSX(JavaScriptXML)是一种JavaScript的语法扩展。它允许你在JavaScript代码中书写类似XML或HTML的标记语言。JSX主要与React库一起使用,用于定义React组件的结构和内容。通过使用JSX,开发者可以在JavaScript代码中以一种声明式的方式描述UI组件的外观。JSX代码在运行之前需要通过转译器(如Babel)转换为普通的JavaScript代码,因为浏览器或JavaScript引擎本身并不直接理解JSX语法。转译过程会将JSX中的标记转换为React函数调用,这些调用生成React元素(Reactelements),React库随后使用这些元素来构建和管理DOM说白了就是javascript的另一种xml格式,通过一些检测软件(ProcessMonitor),可以找到脚本的位置。 这个脚本可以用任何文本编辑器打开,我使用vscode打开它,可以看到它的源码,如果要独立运行,它还有一些基类jsx文件,在其他文件夹,可以用上面同样方法找到它们,但是你如果想提取出来单独使用,比如二次打包,有点难度,因为且不说它有专利版权,而且它的很多API也是封装在photoshop程序里的,基类也只是调用photoshop的API函数,但是任何东西我们只要搞懂了原理,改进优化它并不难。这个专利说明文档在googlepatents找到的,链接如下(https://patentimages.storage.googleapis.com/3e/4e/da/616458f3968095/US20080143820A1.pdf),我在csdn会上传机翻的中文稿等所有关于这本文的所有资源给大家下载,有兴趣的小伙伴可以去看看,你们的关注就是我的源动力。 至于怎么用vscode调试这个脚本学习,需要安装vscode的ExtendScriptDebugger插件(https://marketplace.visualstudio.com/items?itemName=Adobe.extendscript-debug),这个链接下面的英文说明已经说了很详细了,如果不想仔细阅读,其实也很简单,不需要设置vscode的debug中的launch.json,那是老方法了,新的方法是,先安装ExtendScriptDebugger插件,然后把photomerge.jsx文件添加到vscode项目中,然后先打开你的photoshop主程序,然后在vscode中打开photomerge.jsx,在你需要的位置下断点后,右击里面的代码,弹出如下菜单:在vscode顶部会弹出一个菜单,选择你的photoshop即可(我之前安装过另一个版本的ps,所以有两个,请以自己为准)这个工具条是debug工具栏,如需运行,请按第一个三角按钮,如需重新调试请按最后一个按钮断开后,使用之前的方法重新右键菜单,设置photoshop版本后调试,这样不容易死机(这个bug在ExtendScriptDebugger插件介绍里有提到) ,如死机请重启photoshop,然后重新尝试上面的方法。可以看到已经中断下来了,再给大家看个图,如下图,可以调试了。(另外说一句,中断后可能vscode可能没反应,不是死机,只是要到PhotoShop中操作脚本UI,来进入接下来的代码) 因为现在有了chatgpt等一众AI辅助,我们阅读新的代码的速度能提高很多,把函数丢到AI中去,就能得到算法框架和详细的算法注释,这点在使用IDA等反编译工具逆向代码时,最能体现出来,把IDA的繁琐的伪代码给AI,chatgpt马上会给你定位到可能的关键代码,这就是时代的红利。 废话不多说,使用AI阅读了一遍PhotoMerge插件的javascript代码后,对这个脚本的框架有了一定的认识,结合刚刚的老外的专利文档,我们可以知道插件的大概思路: 先用PhotoShop的API读取传入的所有图片-->然后根据图片内容使用sift算法,对齐每张图片,并把矩阵变形后的图片(可能是不规则四边形、也可能是其他形状)的多个角点,使用matlab格式,保存在一个数据层中(因为这个老外程序员使用matlab建的模型)-->读取每张图片的多个角的角点,求出所有相交图片的最大重合区域,并把这个相交区域的角点记录在数据层中,然后通过PhotoShop的自带混合蒙版功能,把相交的图层,在传入的原始图片上做蒙版,接着使用了矩形融合技术(一共使用了两种技术,一种就是矩形合并/快速合并,另一种是所谓的高级图层合并),使相交角点区域的平滑过渡,最后把这些层堆叠在一起,做成psd的工程文件方便人工编辑。1.大致的程序流程: 2.这个photomerge.advancedBlending变量控制了两种合并模式切换3.这个矩形融合的原理示意图:a.先把如下3张子图通过sift算法拼合,得到相交区域(可以看作3个重叠的矩形)角点:b.把这些相交矩形按对角线分割(得到三角形或梯形)c.然后沿着这些分割对角线,做图层的渐变线,达到平滑过渡的目的:d.矩形融合算法的历史:图片矩形融合算法(也可能被称为图像拼接或图像合成算法)的概念并不是一个单一的时间点提出的。这个领域随着图像处理、计算机视觉和图形学的发展而逐渐演进,特别是自20世纪80年代以来,随着计算机技术的进步,相关研究和算法的发展加速了。在最早期,图像融合的概念主要用于卫星图像和医学图像,这些技术主要是为了在一个视图中结合多个图像的不同信息。进入90年代和21世纪,随着个人计算机的普及和图像处理软件的发展,图像融合技术开始被广泛用于摄影艺术、电影制作、虚拟现实等领域。具体到矩形图像融合,它指的是将两个或多个矩形图像区域通过算法合成为一个无缝的整体图像。这种技术在20世纪90年代末到21世纪初开始获得关注,特别是随着全景图像拼接技术的发展。全景图像拼接是矩形图像融合应用中的一个典型例子,它要求不同图像之间在边缘处平滑过渡,以创建看起来连贯的单一场景。最具有里程碑意义的算法和技术,如SIFT(尺度不变特征变换)算法,是在2004年提出的,用于图像特征的匹配和图像融合过程中的关键点检测。但需要注意的是,图像融合技术的发展是一个持续的过程,许多算法和技术都在不断地进步和演化中。 这个脚本代码,因为使用了大量的PhotoShopAPI,所以想要直接使用它的javascript代码达到类似的效果很难。不难看出,除了它最核心的高级融合算法,因为封装关系,我们不得而知,矩形融合技术使用其他语言实现并不是很难,而且我们使用python进行sift拼合,并获取四边形相交区域,进行所谓的矩形融合都是可以实现的,更何况我们还有AI外援,为了不增加文章篇幅,让大家看的太累,我把重要的这些算法的细节给大家介绍下。(PS:我只是抛砖引玉,因为可能有专利的问题,我只大概讲下思路,至于实现代码可以参见文后的链接,今天我们只讲技术,望大家借鉴时当心版权问题,当然我代码和adobe的脚本实现上并不相同,只是原理差不多而已)代码实现:1.读取图片主函数show:函数说明:负责历遍读取一个文件夹目录中的所有图片(图片名称的命名请按照从左到右,依次变大的顺序),然后每次传入左、右两张图片给MergeImage调用。defshow():start_time=time.time()#设置包含图片的文件夹路径folder_path='2.全景拼接图片Python版\\test1'print("正在合并的图片目录:",folder_path)#print(folder_path)#img数组和索引号img=[]img_index=0img1=Noneimg2=Noneresult_img=None#遍历文件夹中的每个文件forfilenameinos.listdir(folder_path):iffilename.endswith(('.png','.jpg','.jpeg')):#检查文件扩展名img_path=os.path.join(folder_path,filename)print("图片序号:"+str(img_index)+',读取图片目录:"'+img_path+'"')img.append(cv_imread(img_path))ifimg_index==0:img1=img[img_index]elifimg_index=2:img1=result_imgimg2=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+=1cv.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. defstitch(self,imgs,ratio=0.60,reprojThresh=4.0): 函数说明:合并图片的主函数,调用下面的所有方法来计算。参数imgs是MergeImage函数传入 的左、右两张图片,ratio参数是控制融合的,范围大概在0.4-0.75之间,默认取0.6B. defget_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是彩色图像还是灰度图像,并据此创建新图像iflen(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.矩阵变换并平移后的右图效果: C.deffind_real_corners(self,img,save_path,debug):此函数说白了找到上面变形平移后左、右图的实际外框角点坐标,并用线画出来外框。deffind_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)ifdebug:forpoint_indexinrange(4):cv.circle(img,corners[point_index],10,(0,0,255),-1)#print("point_index:",point_index)ifpoint_index
|
|