近日在做基于sift特征点的图像配准时遇到匹配失败的情况,失败的原因在于两幅图像分辨率相差有点大,而且这两幅图是不同时间段的同一场景的图片,所以基于sift点的匹配已经找不到匹配点了。然后老师叫我尝试手动选择控制点来支持仿射变换。

很可惜opencv里没有这类似的库,查了下资料,看看有没有现成的手动配准软件,找到了arcgis这款软件可以做手动配准,不过这软件也都太大了吧我要的只是一个简单的功能而已!然后想了想,还是自己写个手动配准工具吧。

首先简单通俗说一下什么是图像配准。先观察一下下面两张图片。

这是两张从不同角度拍的场景,他们有大部分的重合,如果我们需要把这两张图拼接成一幅更大的图,我们需要做第一件事就是对他们进行配准,即对图二进行变换,令图二的物体转换到图一的坐标系,使得像素一一对应,这就是图像配准。

现在图像的配准方法有很多,比如基于特征点的配准,也有基于互信息的配准,都有广泛应用。现在我们使用特征点来配准,关键就在于找出两幅图像尽可能多对应的特征点,来求出变换矩阵,然后将待配准图进行变换。

现在实现一个简易的手动选择控制点的配准工具第一个版本,步骤有:

  1. 搭建交互界面,可以对两幅图自由选点,并把点坐标存储起来
  2. 求出变换矩阵
  3. 利用变换矩阵对待配准图进行仿射变换

根据以上思路,有以下代码

  1. #include "opencv2/highgui/highgui.hpp"
  2. #include "opencv2/imgproc/imgproc.hpp"
  3. #include <cv.h>
  4. #include <cxcore.h>
  5. #include <highgui.h>
  6. #include <iostream>
  7. using namespace std;
  8. using namespace cv;
  9. vector<Point2f> imagePoints1, imagePoints2;
  10. Mat ref_win, src_win;
  11. int pcount = 0;
  12. void on_mouse1(int event, int x, int y, int flags, void *ustc) //event鼠标事件代号,x,y鼠标坐标,flags拖拽和键盘操作的代号
  13. {
  14. if (event == CV_EVENT_LBUTTONDOWN)//左键按下,读取初始坐标,并在图像上该点处打点
  15. {
  16. Point p = Point(x, y);
  17. circle(ref_win, p, 1, Scalar(0, 0, 255), -1);
  18. imshow("基准图", ref_win);
  19. imagePoints1.push_back(p); //将选中的点存起来
  20. cout << "基准图: " << p << endl;
  21. pcount++;
  22. cout << "ponit num:" << pcount << endl;
  23. }
  24. }
  25. void on_mouse2(int event, int x, int y, int flags, void *ustc) //event鼠标事件代号,x,y鼠标坐标,flags拖拽和键盘操作的代号
  26. {
  27. if (event == CV_EVENT_LBUTTONDOWN)//左键按下,读取初始坐标,并在图像上该点处打点
  28. {
  29. Point p = Point(x, y);
  30. circle(src_win, p, 1, Scalar(0, 0, 255), -1);
  31. imshow("待配准图", src_win);
  32. imagePoints2.push_back(p); //将选中的点存起来
  33. cout << "待配准图: " << p << endl;
  34. }
  35. }
  36. int main()
  37. {
  38. Mat ref = imread("ref.png"); //基准图
  39. Mat src = imread("src.png"); //待配准图
  40. ref_win = ref.clone();
  41. src_win = src.clone();
  42. namedWindow("待配准图");
  43. namedWindow("基准图");
  44. imshow("待配准图", src_win);
  45. imshow("基准图", ref_win);
  46. setMouseCallback("待配准图", on_mouse2);
  47. setMouseCallback("基准图", on_mouse1);
  48. waitKey();
  49. string str;
  50. printf("往下执行?\n");
  51. cin >> str;
  52. //求变换矩阵
  53. Mat homo = findHomography(imagePoints2, imagePoints1, CV_RANSAC);
  54. Mat imageTransform1;
  55. warpPerspective(src, imageTransform1, homo, Size(ref.cols, ref.rows)); //变换
  56. imshow("transform", imageTransform1);
  57. imshow("基准图打点", ref_win);
  58. imshow("待配准图打点", src_win);
  59. imshow("变换图", imageTransform1);
  60. imwrite("result.jpg", imageTransform1);
  61. imwrite("src_p.jpg", src_win);
  62. imwrite("ref_p.jpg", ref_win);
  63. waitKey();
  64. return 0;
  65. }

运行一下,弹出两幅图,一张是基准图,一张待配准图,我们仔细找出两者的匹配点,然后用鼠标左键点击该点,那么这个点的坐标信息就被记录下来了。注意匹配点的顺序必须一一对应,比如用鼠标在基准图点击了一个点,那么我们也必须在待配准图也点击对应的匹配点。

效果如下:

手动选择控制点(红点就是我们选中的点)

配准效果

再换个图试试吧

控制点选择

配准效果

这么一个简易手动配准工具1.0算是完成了。但是我们使用时遇到了新的问题,那就是需要两幅图的尺寸太大了,显示器根本没法显示完整个图像!有人会说,把图像缩小再配准不行吗?缩小再配准的话,精度就不能保证了,因为配准时像素级别的。要精确配准,就得用原图。

可惜opencv没有提供浏览大图的工具,那就只能自己再写一写了。

好在可以借助前辈们的经验

http://blog.csdn.net/chenyusiyuan/article/details/6565424

那就在原来代码的基础加点东西,来适应这种“浏览大图的效果”。但是其中需要改动的东西很多,所以1.0的代码几乎全改了。因为前辈的这种浏览大图的效果是拥塞的,只能在一幅图操作完之后才可以操作另一幅图,这个限制对于我们配准操作而言是无法接受的,所以我使用了多线程来操作这个窗口,使得我们可以随意在任何一张图片打点,随时切换。

下面是手动配准工具2.0版本的代码

main.cpp

  1. #include "opencv2/highgui/highgui.hpp"
  2. #include "opencv2/imgproc/imgproc.hpp"
  3. #include "opencv2/opencv.hpp"
  4. #include <Windows.h>
  5. #include <iostream>
  6. #include "NewWindows.h"
  7. using namespace std;
  8. using namespace cv;
  9. void CreateWindows(char* s, char* pic);
  10. void CreateWindows2(char* s, char* pic);
  11. vector<Point2f> imagePoints1, imagePoints2; //记录匹配点
  12. DWORD WINAPI ThreadFun1(LPVOID pM)
  13. {
  14. NewWindow ref_obj("基准", "ref.jpg");
  15. ref_obj.CreateWindows();
  16. imagePoints1 = ref_obj.imagePoints;
  17. return 0;
  18. }
  19. DWORD WINAPI ThreadFun2(LPVOID pM)
  20. {
  21. NewWindow src_obj("待变换", "src.jpg");
  22. src_obj.CreateWindows();
  23. imagePoints2 = src_obj.imagePoints;
  24. return 0;
  25. }
  26. int HandSlectPoint()
  27. {
  28. Mat tsrc1 = imread("ref.jpg"); //基准图
  29. Mat tsrc2 = imread("src.jpg");
  30. while (1)
  31. {
  32. #if 1
  33. imagePoints1.clear();
  34. imagePoints2.clear();
  35. HANDLE handle1 = CreateThread(NULL, 0, ThreadFun1, NULL, 0, NULL); //创建线程
  36. HANDLE handle2 = CreateThread(NULL, 0, ThreadFun2, NULL, 0, NULL);
  37. printf("往下执行?\n");
  38. //先拥塞住,点选完再进行计算变换矩阵
  39. string s;
  40. cin >> s;
  41. Mat homo = findHomography(imagePoints2, imagePoints1, CV_RANSAC);
  42. Mat imageTransform1;
  43. warpPerspective(tsrc2, imageTransform1, homo, Size(tsrc1.cols, tsrc1.rows));
  44. imwrite("trans.jpg", imageTransform1); //把配准后结果存起来
  45. CloseHandle(handle1);//销毁线程1
  46. CloseHandle(handle2);//销毁线程1
  47. #endif
  48. printf("是否结束?\n");
  49. //判断是否结束,如果点选得不好,就再来一次
  50. string str;
  51. cin >> str;
  52. if (str == "yes")
  53. break;
  54. }
  55. return 0;
  56. }
  57. int main()
  58. {
  59. HandSlectPoint();
  60. return 0;
  61. }

NewWindows.cpp

  1. #include "NewWindows.h"
  2. NewWindow::NewWindow(char* label, char* pic_name)
  3. {
  4. this->pic_name = pic_name;
  5. this->label = label;
  6. }
  7. void NewWindow::mouse_callback(int event, int x, int y, int flags, void* param)
  8. {
  9. p = Point(x, y);
  10. pp = Point(x + x_offset, y + y_offset);
  11. if (needScroll)
  12. {
  13. switch (event)
  14. {
  15. case CV_EVENT_RBUTTONDOWN:
  16. mx = x, my = y;
  17. dx = 0, dy = 0;
  18. // 按下左键时光标定位在水平滚动条区域内
  19. if (x >= rect_bar_horiz.x && x <= rect_bar_horiz.x + rect_bar_horiz.width
  20. && y >= rect_bar_horiz.y && y <= rect_bar_horiz.y + rect_bar_horiz.height)
  21. {
  22. clickHorizBar = true;
  23. }
  24. // 按下左键时光标定位在垂直滚动条区域内
  25. if (x >= rect_bar_verti.x && x <= rect_bar_verti.x + rect_bar_verti.width
  26. && y >= rect_bar_verti.y && y <= rect_bar_verti.y + rect_bar_verti.height)
  27. {
  28. clickVertiBar = true;
  29. }
  30. break;
  31. case CV_EVENT_MOUSEMOVE:
  32. if (clickHorizBar)
  33. {
  34. dx = fabs(x - mx) > 1 ? (int)(x - mx) : 0;
  35. dy = 0;
  36. }
  37. if (clickVertiBar)
  38. {
  39. dx = 0;
  40. dy = fabs(y - my) > 1 ? (int)(y - my) : 0;
  41. }
  42. mx = x, my = y;
  43. break;
  44. case CV_EVENT_RBUTTONUP:
  45. mx = x, my = y;
  46. dx = 0, dy = 0;
  47. clickHorizBar = false;
  48. clickVertiBar = false;
  49. break;
  50. case CV_EVENT_LBUTTONDOWN:
  51. //cvShowImage("jizuhn",dst_img);
  52. imagePoints.push_back(pp);
  53. cout << label <<": "<< pp << endl;
  54. //_p1count++;
  55. //cout << "zhihuan count:" << _p1count << endl;
  56. flag = 1;
  57. //dx = 0, dy = 0;
  58. break;
  59. default:
  60. dx = 0, dy = 0;
  61. break;
  62. }
  63. }
  64. }
  65. void NewWindow::myShowImageScroll(char* title, IplImage* src_img, int winWidth, int winHeight ) // 显示窗口大小默认为 1400×700
  66. {
  67. CvRect rect_dst, // 窗口中有效的图像显示区域
  68. rect_src; // 窗口图像对应于源图像中的区域
  69. int imgWidth = src_img->width,
  70. imgHeight = src_img->height,
  71. barWidth = 25; // 滚动条的宽度(像素)
  72. double scale_w = (double)imgWidth / (double)winWidth, // 源图像与窗口的宽度比值
  73. scale_h = (double)imgHeight / (double)winHeight; // 源图像与窗口的高度比值
  74. if (scale_w<1)
  75. winWidth = imgWidth + barWidth;
  76. if (scale_h<1)
  77. winHeight = imgHeight + barWidth;
  78. int showWidth = winWidth, showHeight = winHeight; // rect_dst 的宽和高
  79. int src_x = 0, src_y = 0; // 源图像中 rect_src 的左上角位置
  80. int horizBar_width = 0, horizBar_height = 0,
  81. vertiBar_width = 0, vertiBar_height = 0;
  82. needScroll = scale_w>1.0 || scale_h>1.0 ? TRUE : FALSE;
  83. // 若图像大于设定的窗口大小,则显示滚动条
  84. if (needScroll)
  85. {
  86. IplImage* dst_img = cvCreateImage(cvSize(winWidth, winHeight), src_img->depth, src_img->nChannels);
  87. cvZero(dst_img);
  88. // 源图像宽度大于窗口宽度,则显示水平滚动条
  89. if (1)
  90. {
  91. showHeight = winHeight - barWidth;
  92. horizBar_width = (int)((double)winWidth / scale_w);
  93. horizBar_height = winHeight - showHeight;
  94. horizBar_x = min(
  95. max(0, horizBar_x + dx),
  96. winWidth - horizBar_width);
  97. rect_bar_horiz = cvRect(
  98. horizBar_x,
  99. showHeight + 1,
  100. horizBar_width,
  101. horizBar_height);
  102. // 显示水平滚动条
  103. cvRectangleR(dst_img, rect_bar_horiz, cvScalarAll(255), -1);
  104. }
  105. // 源图像高度大于窗口高度,则显示垂直滚动条
  106. if (scale_h > 1.0)
  107. {
  108. // printf("come!\n");
  109. showWidth = winWidth - barWidth;
  110. vertiBar_width = winWidth - showWidth;
  111. vertiBar_height = (int)((double)winHeight / scale_h);
  112. vertiBar_y = min(
  113. max(0, vertiBar_y + dy),
  114. winHeight - vertiBar_height);
  115. //printf("vertiBar_width:%d vertiBar_height:%d\n", vertiBar_width, vertiBar_height);
  116. //printf("x:%d y:%d\n", showWidth + 1, vertiBar_y);
  117. rect_bar_verti = cvRect(
  118. showWidth + 1,
  119. vertiBar_y,
  120. vertiBar_width,
  121. vertiBar_height);
  122. // 显示垂直滚动条
  123. //printf("w:%d h:%d\n", dst_img->width, dst_img->height);
  124. cvRectangleR(dst_img, rect_bar_verti, cvScalarAll(255), -1);
  125. }
  126. showWidth = min(showWidth, imgWidth);
  127. showHeight = min(showHeight, imgHeight);
  128. // 设置窗口显示区的 ROI
  129. rect_dst = cvRect(0, 0, showWidth, showHeight);
  130. cvSetImageROI(dst_img, rect_dst);
  131. // 设置源图像的 ROI
  132. src_x = (int)((double)horizBar_x*scale_w);
  133. src_y = (int)((double)vertiBar_y*scale_h);
  134. src_x = min(src_x, imgWidth - showWidth);
  135. src_y = min(src_y, imgHeight - showHeight);
  136. rect_src = cvRect(src_x, src_y, showWidth, showHeight);
  137. x_offset = src_x;
  138. y_offset = src_y;
  139. cvSetImageROI(src_img, rect_src);
  140. if (flag == 1)
  141. {
  142. cvCircle(src_img, p, 3, Scalar(0, 0, 255), -1);
  143. flag = 0;
  144. }
  145. // 将源图像内容复制到窗口显示区
  146. cvCopy(src_img, dst_img);
  147. cvResetImageROI(dst_img);
  148. cvResetImageROI(src_img);
  149. // 显示图像和滚动条
  150. cvShowImage(title, dst_img);
  151. cvReleaseImage(&dst_img);
  152. }
  153. // 源图像小于设定窗口,则直接显示图像,无滚动条
  154. else
  155. {
  156. cvShowImage(title, src_img);
  157. }
  158. }
  159. void m_callback(int event, int x, int y, int flags, void* param)
  160. {
  161. NewWindow* p_win = (NewWindow*)param;
  162. p_win->mouse_callback(event, x, y, flags, NULL);
  163. }
  164. void NewWindow::CreateWindows()
  165. {
  166. int width = 1200, height = 700; //显示的图片大小
  167. cvNamedWindow(label, 1);
  168. cvSetMouseCallback(label, m_callback, this);
  169. image = cvLoadImage(pic_name, CV_LOAD_IMAGE_COLOR);
  170. while (1)
  171. {
  172. myShowImageScroll(label, image, width, height);
  173. //Sleep(100);
  174. int KEY = cvWaitKey(10);
  175. if ((char)KEY == 27)
  176. break;
  177. }
  178. cvDestroyWindow(label);
  179. }

NewWindows.h

  1. #ifndef __NEW_WINDOWS_H__
  2. #define __NEW_WINDOWS_H__
  3. #include <opencv2/highgui/highgui.hpp>
  4. #include <opencv2/imgproc/imgproc_c.h>
  5. #include <Windows.h>
  6. #include <iostream>
  7. #include <vector>
  8. #define FALSE 0
  9. #define TRUE 1
  10. using namespace std;
  11. using namespace cv;
  12. class NewWindow
  13. {
  14. public:
  15. vector<Point2f> imagePoints;
  16. void CreateWindows();
  17. void mouse_callback(int event, int x, int y, int flags, void* param);
  18. NewWindow(char* label, char* pic_name);
  19. private:
  20. double mx = 0, my = 0;
  21. int dx = 0, dy = 0, horizBar_x = 0, vertiBar_y = 0;
  22. bool clickVertiBar = false, clickHorizBar = false, needScroll = false;
  23. CvRect rect_bar_horiz, rect_bar_verti;
  24. IplImage* image;
  25. Point p;
  26. Point pp;
  27. int flag = 0;
  28. int x_offset;
  29. int y_offset;
  30. char* pic_name;
  31. char* label;
  32. void myShowImageScroll(char* title, IplImage* src_img,
  33. int winWidth = 1400, int winHeight = 700); // 显示窗口大小默认为 1400×700
  34. };
  35. #endif

看看效果吧,现在我们需要对两张2000*2000的图像进行配准,因为我们的显示器无法完全显示整张图片,所以使用了这个带浏览大图的工具来进行配准。可以看到,显示图的右侧和下侧都有滚动条,我们只需按住鼠标右键拖动即可浏览到显示不到的区域,同样地,我们是点击鼠标左键实现选点。

点的坐标一一记录

配准之后,可以看出图像发生了轻微形变,与基准图一对比,发现配准成功。

【2017.9.23更新】

有几个园友发信息给我,说不知这个程序怎么用,那我在这里总结一下使用步骤:

1.左键选择控制点,右键是用于滚动条的。选择控制点的时候注意在图一选了点后需要在图二也选好对应点,形成控制点对。

2.当控制点对全部选好后按“esc”关闭窗口。要按两次,因为要关闭两个窗口。

3.按键盘任意键开始透视变换。

4.如果你觉得这次变换OK,就按yes退出

Opencv探索之路(二十):制作一个简易手动图像配准工具的更多相关文章

  1. 使用 history 对象和 location 对象中的属性和方法制作一个简易的网页浏览工具

    查看本章节 查看作业目录 需求说明: 使用 history 对象和 location 对象中的属性和方法制作一个简易的网页浏览工具 实现思路: 使用history对象中的 forward() 方法和 ...

  2. 用XMLHttpRequest制作一个简易ajax

    概述 jquery退出历史舞台之后,我们怎么来发送ajax请求呢?可以用相关的库,也可以自己制作一个简易的ajax. 需要说明的是,我们使用的是XMLHttpRequest 2,它几乎兼容所有主流浏览 ...

  3. Bootstrap入门(二十八)JS插件5:工具提醒

    Bootstrap入门(二十八)JS插件5:工具提醒 工具提示在使用过程中比较常见,但是实现起来有些麻烦,而bootstrap则很好地解决了这个问题. 我们来写一个简单的实例 先引入CSS文件和JS文 ...

  4. OpenCV探索之路(二十四)图像拼接和图像融合技术

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

  5. Opencv探索之路(十九):读写xml和yml文件

    有时候我们处理完图像后需要保存一下数据到文件上,以供下一步的处理.一个比较广泛的需求场景就是:我们对一幅图像进行特征提取之后,需要把特征点信息保存到文件上,以供后面的机器学习分类操作.那么如果遇到这样 ...

  6. iOS:制作一个简易的计算器

    初步接触视图,制作了一个简易的计算器,基本上简单的计算是没有问题的,不是很完美,可能还有一些bug,再接再厉. // // ViewController.m // 计算器 // // Created ...

  7. 使用Windows Form 制作一个简易资源管理器

    自制一个简易资源管理器----TreeView控件 第一步.新建project,进行基本设置:(Set as StartUp Project:View/Toolbox/TreeView) 第二步.开始 ...

  8. Python Django 编写一个简易的后台管理工具4-添加admin模版

    导入admin后台模版 可以在网上任意搜索模版,我这里也提供一个地址github 拷贝admin后台的html文件至项目的templates文件夹 创建static文件夹,将admin后台的js,im ...

  9. opencv探索之路(十二):感兴趣区域ROI和logo添加技术

    在图像处理领域,有一个非常重要的名词ROI. 什么是ROI? 它的英文全称是Region Of Interest,对应的中文解释就是感兴趣区域. 感兴趣区域,就是我们从图像中选择一个图像区域,这个区域 ...

随机推荐

  1. [1] MVC & MVP &MVVM

    开发架构之MVC & MVP & MVVM  

  2. android的drawable资源

    1.android中可以通过xml文件配置资源,比如字符串啦,整数拉.浮点数等等,当然也可以配置图片资源和选择器,下面我们就看看几种图片资源的配置. @1矩形方框,带渐变色的配置代码 <?xml ...

  3. Java学习笔记--监视目录变化

    1.在实际开发中可能会需要监视某个目录下的文件所发生的变化.   2.在java7之前的做法 在一个独立的线程中使用File类的listFiles方法来定时检查目录中的内容,并与之前的内容进行比较   ...

  4. 10分钟就能学会的.NET Core配置

    .NET Core为我们提供了一套用于配置的API,它为程序提供了运行时从文件.命令行参数.环境变量等读取配置的方法.配置都是键值对的形式,并且支持嵌套,.NET Core还内建了从配置反序列化为PO ...

  5. js对象中动态读取属性值 动态属性值 js正则表达式全局替换

    $(document).ready(function(){ var exceptionMsg = '${exception.message }'; var exceptionstr = ''; //j ...

  6. nuget挂了吗?

    [nuget.org] Unable to load the service index for source https://api.nuget.org/v3/index.json. 发送请求时出错 ...

  7. H3CNE实验:配置交换机接口

    第1步:配置交换机端口 <H3C>system-view System View: return to User View with Ctrl+Z. [H3C]interface Giga ...

  8. Vijos 1981 跳石头 二分

    描述 一年一度的"跳石头"比赛又要开始了! 这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石.组委会已经选择好了两块岩石作为比赛起点和终点.在起点和终点之间,有 N 块岩 ...

  9. python webserver, based on SimpleHTTPServer

    #-*- coding:utf-8 -*- #author: lichmama #email: nextgodhand@163.com #filename: httpd.py import io im ...

  10. eclipse 小方法