图像拼接在实际的应用场景很广,比如无人机航拍,遥感图像等等,图像拼接是进一步做图像理解基础步骤,拼接效果的好坏直接影响接下来的工作,所以一个好的图像拼接算法非常重要。

如按下图是将两张楼房图片拼接成一个图像。

1拼接步骤

要实现图像拼接,简单来说要实现以下步骤:

  1. 输入图像

  2. 图像几何校正

  3. 图像预处理

  4. 对每幅图进行特征点提取

  5. 对特征点进行匹配

  6. 进行图像配准

  7. 图像融合

  8. 对重叠边界进行特殊处理

2拼接条件

图像的拼接要具备以下几个条件:

  • 图像应具有一定的特征。

  • 图像要有重叠部分,一般重叠部分占总图像的1/4以上较合适。

  • 图像的背景亮度差异不能太大,应该低于10个灰度值,否则难以拼接成功。

  • 图像的方位差异不能太大,图像应该来源同一方位。

  • 拼合边界过渡应平滑,以消除接拼痕迹。

3特征点提取

基于SRUF 的特征点的提取与匹配

为了使拼接具有良好的精度和鲁棒性,同时又使其具有较好的实时性,本实验采用SURF 算法完成图像序列特征点的提取。

SURF 算法又称快速鲁棒特征,借鉴了SIFT 中简化近似的思想,将DoH 中的高斯二阶微分模板进行了近似简化,使得模板对图像的滤波只需要进行几个简单的加减法运算,并且这种运算与滤波模板的尺寸无关。实验证明,SURF 算法较SIFT 在运算速度上要快3 倍左右,综合性能要优于SIFT 算法。

SURF 特征点提取与描述主要包含4 个步骤:

  1. 检测尺度空间极值。

  2. 精炼特征点位置。

  3. 计算特征点的描述信息。

  4. 生成描述特征点的特征向量。

halcon特征点特取的算子如下:

select_obj (Images, ImageF, F)select_obj (Images, ImageT, T)* 提取两幅图像中的点。points_foerstner (ImageF, 1, 2, 3, 200, 0.3, 'gauss', 'false', RowJunctionsF, ColJunctionsF, CoRRJunctionsF, CoRCJunctionsF, CoCCJunctionsF, RowAreaF, ColAreaF, CoRRAreaF, CoRCAreaF, CoCCAreaF)points_foerstner (ImageT, 1, 2, 3, 200, 0.3, 'gauss', 'false', RowJunctionsT, ColJunctionsT, CoRRJunctionsT, CoRCJunctionsT, CoCCJunctionsT, RowAreaT, ColAreaT, CoRRAreaT, CoRCAreaT, CoCCAreaT)

4图像配准

图像配准是一种确定待拼接图像间的重叠区域以及重叠位置的技术,它是整个图像拼接的核心。本节采用的是基于特征点的图像配准方法,即通过匹配点对构建图像序列之间的变换矩阵,从而完成全景图像的拼接。

变换矩阵H求解是图像配准的核心,其求解的算法流程如下。

  1. 检测每幅图像中特征点。

  2. 计算特征点之间的匹配。

  3. 计算图像间变换矩阵的初始值。

  4. 迭代精炼H变换矩阵。

  5. 引导匹配。用估计的H去定义对极线附近的搜索区域,进一步确定特征点的对应。

  6. 重复迭代4)和5)直到对应点的数目稳定为止。

图像配准算子如下:

* 确定当前图像对的点匹配和变换。proj_match_points_ransac (ImageF, ImageT, RowJunctionsF, ColJunctionsF, RowJunctionsT, ColJunctionsT, 'ncc', 21, 0, 0, 480, 640, 0, 0.5, 'gold_standard', 1, 4364537, ProjMatrix, Points1, Points2)* 累加变换矩阵。ProjMatrices := [ProjMatrices,ProjMatrix]* 累积点数匹配和点数匹配。Rows1 := [Rows1,subset(RowJunctionsF,Points1)]Cols1 := [Cols1,subset(ColJunctionsF,Points1)]Rows2 := [Rows2,subset(RowJunctionsT,Points2)]Cols2 := [Cols2,subset(ColJunctionsT,Points2)]NumMatches := [NumMatches,|Points1|]* 生成表示平铺图像中提取点的十字。* 请注意,我们必须考虑平铺图像中图像的行偏移。gen_cross_contour_xld (PointsF, RowJunctionsF + (F - 1) * 500, ColJunctionsF, 6, rad(45))gen_cross_contour_xld (PointsT, RowJunctionsT + (T - 1) * 500, ColJunctionsT, 6, rad(45))* 生成匹配点对的线表示。我们从线条中创建XLD轮廓,以便放大图形窗口,更仔细地查看匹配。RowF := subset(RowJunctionsF,Points1) + (F - 1) * 500ColF := subset(ColJunctionsF,Points1)RowT := subset(RowJunctionsT,Points2) + (T - 1) * 500ColT := subset(ColJunctionsT,Points2)gen_empty_obj (Matches)for K := 0 to |RowF| - 1 by 1gen_contour_polygon_xld (Match, [RowF[K],RowT[K]], [ColF[K],ColT[K]])concat_obj (Matches, Match, Matches)endfor

5图像融合

根据仿射变换矩阵进行图像融合。

关闭窗口后重新打开

gen_projective_mosaic (Images, MosaicImage, 2, From, To, ProjMatrices, 'default', 'false', MosaicMatrices2D)get_image_size (MosaicImage, Width, Height)

6图像合并

单纯将多张图片按几行几列合并成一张大图并显示。

* 关闭窗口后重新打开dev_close_window()dev_open_window (0, 0, 600, 400, 'black', WindowHandle)* 创建一张空白图片gen_empty_obj (Images)* 遍历文件夹list_files ('C:/Users/Administrator/Desktop/test', ['files','follow_links'], ImageFiles)tuple_regexp_select (ImageFiles, ['\\.(tif|tiff|gif|bmp|jpg|jpeg|jp2|png|pcx|pgm|ppm|pbm|xwd|ima|hobj)$','ignore_case'], ImageFiles)for Index := 0 to |ImageFiles| - 1 by 1read_image (Image, ImageFiles[Index])* 缩放图片到图片统一大小zoom_image_size (Image, ImageZoom, 200, 200, 'constant')concat_obj (Images, ImageZoom, Images)endfor* 合并图片(按行填充,一行填满4张图片后再填充下一行)tile_images (Images, TiledImage, 4, 'horizontal')dev_display (TiledImage)

图像效果如下:

7拼接示例

示例如图:

代码如下:‍

dev_update_off ()dev_close_window ()dev_open_window (0, 0, 640, 480, 'white', WindowHandle)dev_set_color ('green')set_display_font (WindowHandle, 14, 'mono', 'true', 'false')* Read in the images and show them one-by-one.Please not the fold-like* degradations running across the PCB.gen_empty_obj (Images)for J := 1 to 6 by 1read_image (Image, 'mosaic/pcb_' + J$'02')concat_obj (Images, Image, Images)dev_display (Image)disp_message (WindowHandle, 'Image ' + J$'d', 'window', 12, 12, 'black', 'true')*wait_seconds (1)endfordisp_continue_message (WindowHandle, 'black', 'true')stop ()*显示用于计算投影的点匹配*在图像之间进行转换,我们将以大屏幕显示所有图像*平铺图像在图像之间留有一定的空间,以便*这些图像中的大部分很容易看到。dev_set_window_extents (-1, -1, 640 / 4, 2980 / 4)tile_images_offset (Images, TiledImage, [0, 500, 1000, 1500, 2000, 2500], [0, 0, 0, 0, 0, 0], [-1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1], 640, 2980)dev_clear_window ()dev_display (TiledImage)disp_message (WindowHandle, 'All 6 images', 'window', 12, 12, 'black', 'true')disp_message (WindowHandle, 'Click \'Run\'\nto continue', 'window', 2980 / 4 - 50, 12, 'black', 'true')stop ()*现在我们计算五对图像之间的点匹配,并以此计算图像对之间的投影变换。*请注意,下面的代码为每个图像对调用点运算符。由于图像形成一条带,*只需稍加簿记,我们就可以通过保存上一次迭代的点来提高过程的效率*(J对中的ImageT将与J+1对中的ImageF相同)。这里没有这样做,*因为在一般情况下,这样的优化会非常麻烦,因为图像可能位于无法用条带表示的一般配置中。dev_clear_window ()dev_display (TiledImage)disp_message (WindowHandle, 'Point matches', 'window', 12, 3, 'black', 'true')*我们定义了图像对,即哪个图像应该映射到哪个图像。From := [1, 2, 3, 4, 5]To := [2, 3, 4, 5, 6]Num := |From|* 我们需要一个变量来累加投影变换矩阵。ProjMatrices := []*此外,因为我们想要在下面创建一个刚性马赛克,所以我们需要累积所有点对应和匹配图像对的数量。Rows1 := []Cols1 := []Rows2 := []Cols2 := []NumMatches := []* 现在我们可以确定五个图像对之间的转换。for J := 0 to Num - 1 by 1F := From[J]T := To[J]select_obj (Images, ImageF, F)select_obj (Images, ImageT, T)* 提取两幅图像中的点。points_foerstner (ImageF, 1, 2, 3, 200, 0.3, 'gauss', 'false', RowJunctionsF, ColJunctionsF, CoRRJunctionsF, CoRCJunctionsF, CoCCJunctionsF, RowAreaF, ColAreaF, CoRRAreaF, CoRCAreaF, CoCCAreaF)points_foerstner (ImageT, 1, 2, 3, 200, 0.3, 'gauss', 'false', RowJunctionsT, ColJunctionsT, CoRRJunctionsT, CoRCJunctionsT, CoCCJunctionsT, RowAreaT, ColAreaT, CoRRAreaT, CoRCAreaT, CoCCAreaT)* 确定当前图像对的点匹配和变换。proj_match_points_ransac (ImageF, ImageT, RowJunctionsF, ColJunctionsF, RowJunctionsT, ColJunctionsT, 'ncc', 21, 0, 0, 480, 640, 0, 0.5, 'gold_standard', 1, 4364537, ProjMatrix, Points1, Points2)* 累加变换矩阵。ProjMatrices := [ProjMatrices,ProjMatrix]* 累积点数匹配和点数匹配。Rows1 := [Rows1,subset(RowJunctionsF,Points1)]Cols1 := [Cols1,subset(ColJunctionsF,Points1)]Rows2 := [Rows2,subset(RowJunctionsT,Points2)]Cols2 := [Cols2,subset(ColJunctionsT,Points2)]NumMatches := [NumMatches,|Points1|]* 生成表示平铺图像中提取点的十字。* 请注意,我们必须考虑平铺图像中图像的行偏移。gen_cross_contour_xld (PointsF, RowJunctionsF + (F - 1) * 500, ColJunctionsF, 6, rad(45))gen_cross_contour_xld (PointsT, RowJunctionsT + (T - 1) * 500, ColJunctionsT, 6, rad(45))* 生成匹配点对的线表示。我们从线条中创建XLD轮廓,以便放大图形窗口,更仔细地查看匹配。RowF := subset(RowJunctionsF,Points1) + (F - 1) * 500ColF := subset(ColJunctionsF,Points1)RowT := subset(RowJunctionsT,Points2) + (T - 1) * 500ColT := subset(ColJunctionsT,Points2)gen_empty_obj (Matches)for K := 0 to |RowF| - 1 by 1gen_contour_polygon_xld (Match, [RowF[K],RowT[K]], [ColF[K],ColT[K]])concat_obj (Matches, Match, Matches)endfor* 现在显示提取的数据。dev_set_color ('blue')dev_display (Matches)dev_set_color ('green')dev_display (PointsF)dev_display (PointsT)endfordisp_message (WindowHandle, 'Click \'Run\'\nto continue', 'window', 2980 / 4 - 50, 12, 'black', 'true')stop ()* 最后,我们可以从投影变换生成马赛克图像。gen_projective_mosaic (Images, MosaicImage, 2, From, To, ProjMatrices, 'default', 'false', MosaicMatrices2D)get_image_size (MosaicImage, Width, Height)dev_set_window_extents (-1, -1, Width / 3, Height / 3)dev_clear_window ()dev_display (MosaicImage)disp_message (WindowHandle, 'Projective mosaic', 'window', 12, 12, 'black', 'true')disp_message (WindowHandle, 'Click \'Run\'\nto continue', 'window', Height / 3 - 50, 12, 'black', 'true')stop ()* 为了更清楚地显示图像中可见的褶皱不是马赛克造成的,我们在马赛克图像中显示图像之间的接缝。*这可以通过创建包含图像边界的图像、从中生成马赛克并分割生成的马赛克图像来实现。get_image_size (Image, Width, Height)gen_image_const (ImageBlank, 'byte', Width, Height)gen_rectangle1 (Rectangle, 0, 0, Height - 1, Width - 1)paint_region (Rectangle, ImageBlank, ImageBorder, 255, 'margin')gen_empty_obj (ImagesBorder)for J := 1 to 6 by 1concat_obj (ImagesBorder, ImageBorder, ImagesBorder)endforgen_projective_mosaic (ImagesBorder, MosaicImageBorder, 2, From, To, ProjMatrices, 'default', 'false', MosaicMatrices2D)threshold (MosaicImageBorder, Seams, 128, 255)dev_clear_window ()dev_display (MosaicImage)disp_message (WindowHandle, 'Seams between the\nimages', 'window', 12, 12, 'black', 'true')dev_set_color ('yellow')dev_display (Seams)disp_message (WindowHandle, 'Click \'Run\'\nto continue', 'window', 550, 12, 'black', 'true')stop ()*如果你仔细观察上面的投影马赛克,你可能会注意到*马赛克中有一个非常轻微的投影失真。这种情况会发生*因为变换不能精确地确定*因为噪声导致的点坐标误差非常小。因为*在条形结构中,基本上是图像之间的重叠区域*成对的图像可以像一个铰链一样绕着它旋转,离开图像平面。*在这个例子中,我们知道图像之间的映射必须是刚性变换。如果我们想强制转换为刚性,*我们可以简单地使用bundle_adjust_mosaic。bundle_adjust_mosaic (6, 1, From, To, ProjMatrices, Rows1, Cols1, Rows2, Cols2, NumMatches, 'rigid', MosaicMatrices2D, Rows, Cols, Error)* 现在,我们可以从刚性变换生成马赛克图像。gen_bundle_adjusted_mosaic (Images, MosaicImageRigid, MosaicMatrices2D, 'default', 'false', TransMatrix2D)get_image_size (MosaicImageRigid, Width, Height)dev_set_window_extents (-1, -1, Width / 3, Height / 3)dev_clear_window ()dev_display (MosaicImageRigid)disp_message (WindowHandle, 'Rigid mosaic', 'window', 12, 12, 'black', 'true')