Opencv探索之路(二十):制作一个简易手动图像配准工具
2017-07-29 00:41
549 查看
近日在做基于sift特征点的图像配准时遇到匹配失败的情况,失败的原因在于两幅图像分辨率相差有点大,而且这两幅图是不同时间段的同一场景的图片,所以基于sift点的匹配已经找不到匹配点了。然后老师叫我尝试手动选择控制点来支持仿射变换。
很可惜opencv里没有这类似的库,查了下资料,看看有没有现成的手动配准软件,找到了arcgis这款软件可以做手动配准,不过这软件也都太大了吧我要的只是一个简单的功能而已!然后想了想,还是自己写个手动配准工具吧。
首先简单通俗说一下什么是图像配准。先观察一下下面两张图片。
这是两张从不同角度拍的场景,他们有大部分的重合,如果我们需要把这两张图拼接成一幅更大的图,我们需要做第一件事就是对他们进行配准,即对图二进行变换,令图二的物体转换到图一的坐标系,使得像素一一对应,这就是图像配准。
现在图像的配准方法有很多,比如基于特征点的配准,也有基于互信息的配准,都有广泛应用。现在我们使用特征点来配准,关键就在于找出两幅图像尽可能多对应的特征点,来求出变换矩阵,然后将待配准图进行变换。
现在实现一个简易的手动选择控制点的配准工具第一个版本,步骤有:
搭建交互界面,可以对两幅图自由选点,并把点坐标存储起来
求出变换矩阵
利用变换矩阵对待配准图进行仿射变换
根据以上思路,有以下代码
运行一下,弹出两幅图,一张是基准图,一张待配准图,我们仔细找出两者的匹配点,然后用鼠标左键点击该点,那么这个点的坐标信息就被记录下来了。注意匹配点的顺序必须一一对应,比如用鼠标在基准图点击了一个点,那么我们也必须在待配准图也点击对应的匹配点。
效果如下:
手动选择控制点(红点就是我们选中的点)
配准效果
再换个图试试吧
控制点选择
配准效果
这么一个简易手动配准工具1.0算是完成了。但是我们使用时遇到了新的问题,那就是需要两幅图的尺寸太大了,显示器根本没法显示完整个图像!有人会说,把图像缩小再配准不行吗?缩小再配准的话,精度就不能保证了,因为配准时像素级别的。要精确配准,就得用原图。
可惜opencv没有提供浏览大图的工具,那就只能自己再写一写了。
好在可以借助前辈们的经验
http://blog.csdn.net/chenyusiyuan/article/details/6565424
那就在原来代码的基础加点东西,来适应这种“浏览大图的效果”。但是其中需要改动的东西很多,所以1.0的代码几乎全改了。因为前辈的这种浏览大图的效果是拥塞的,只能在一幅图操作完之后才可以操作另一幅图,这个限制对于我们配准操作而言是无法接受的,所以我使用了多线程来操作这个窗口,使得我们可以随意在任何一张图片打点,随时切换。
下面是手动配准工具2.0版本的代码
main.cpp
NewWindows.cpp
NewWindows.h
看看效果吧,现在我们需要对两张2000*2000的图像进行配准,因为我们的显示器无法完全显示整张图片,所以使用了这个带浏览大图的工具来进行配准。可以看到,显示图的右侧和下侧都有滚动条,我们只需按住鼠标右键拖动即可浏览到显示不到的区域,同样地,我们是点击鼠标左键实现选点。
点的坐标一一记录
配准之后,可以看出图像发生了轻微形变,与基准图一对比,发现配准成功。
【2017.9.23更新】
有几个园友发信息给我,说不知这个程序怎么用,那我在这里总结一下使用步骤:
1.左键选择控制点,右键是用于滚动条的。选择控制点的时候注意在图一选了点后需要在图二也选好对应点,形成控制点对。
2.当控制点对全部选好后按“esc”关闭窗口。要按两次,因为要关闭两个窗口。
3.按键盘任意键开始透视变换。
4.如果你觉得这次变换OK,就按yes退出
很可惜opencv里没有这类似的库,查了下资料,看看有没有现成的手动配准软件,找到了arcgis这款软件可以做手动配准,不过这软件也都太大了吧我要的只是一个简单的功能而已!然后想了想,还是自己写个手动配准工具吧。
首先简单通俗说一下什么是图像配准。先观察一下下面两张图片。
这是两张从不同角度拍的场景,他们有大部分的重合,如果我们需要把这两张图拼接成一幅更大的图,我们需要做第一件事就是对他们进行配准,即对图二进行变换,令图二的物体转换到图一的坐标系,使得像素一一对应,这就是图像配准。
现在图像的配准方法有很多,比如基于特征点的配准,也有基于互信息的配准,都有广泛应用。现在我们使用特征点来配准,关键就在于找出两幅图像尽可能多对应的特征点,来求出变换矩阵,然后将待配准图进行变换。
现在实现一个简易的手动选择控制点的配准工具第一个版本,步骤有:
搭建交互界面,可以对两幅图自由选点,并把点坐标存储起来
求出变换矩阵
利用变换矩阵对待配准图进行仿射变换
根据以上思路,有以下代码
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <cv.h> #include <cxcore.h> #include <highgui.h> #include <iostream> using namespace std; using namespace cv; vector<Point2f> imagePoints1, imagePoints2; Mat ref_win, src_win; int pcount = 0; void on_mouse1(int event, int x, int y, int flags, void *ustc) //event鼠标事件代号,x,y鼠标坐标,flags拖拽和键盘操作的代号 { if (event == CV_EVENT_LBUTTONDOWN)//左键按下,读取初始坐标,并在图像上该点处打点 { Point p = Point(x, y); circle(ref_win, p, 1, Scalar(0, 0, 255), -1); imshow("基准图", ref_win); imagePoints1.push_back(p); //将选中的点存起来 cout << "基准图: " << p << endl; pcount++; cout << "ponit num:" << pcount << endl; } } void on_mouse2(int event, int x, int y, int flags, void *ustc) //event鼠标事件代号,x,y鼠标坐标,flags拖拽和键盘操作的代号 { if (event == CV_EVENT_LBUTTONDOWN)//左键按下,读取初始坐标,并在图像上该点处打点 { Point p = Point(x, y); circle(src_win, p, 1, Scalar(0, 0, 255), -1); imshow("待配准图", src_win); imagePoints2.push_back(p); //将选中的点存起来 cout << "待配准图: " << p << endl; } } int main() { Mat ref = imread("ref.png"); //基准图 Mat src = imread("src.png"); //待配准图 ref_win = ref.clone(); src_win = src.clone(); namedWindow("待配准图"); namedWindow("基准图"); imshow("待配准图", src_win); imshow("基准图", ref_win); setMouseCallback("待配准图", on_mouse2); setMouseCallback("基准图", on_mouse1); waitKey(); string str; printf("往下执行?\n"); cin >> str; //求变换矩阵 Mat homo = findHomography(imagePoints2, imagePoints1, CV_RANSAC); Mat imageTransform1; warpPerspective(src, imageTransform1, homo, Size(ref.cols, ref.rows)); //变换 imshow("transform", imageTransform1); imshow("基准图打点", ref_win); imshow("待配准图打点", src_win); imshow("变换图", imageTransform1); imwrite("result.jpg", imageTransform1); imwrite("src_p.jpg", src_win); imwrite("ref_p.jpg", ref_win); waitKey(); return 0; }
运行一下,弹出两幅图,一张是基准图,一张待配准图,我们仔细找出两者的匹配点,然后用鼠标左键点击该点,那么这个点的坐标信息就被记录下来了。注意匹配点的顺序必须一一对应,比如用鼠标在基准图点击了一个点,那么我们也必须在待配准图也点击对应的匹配点。
效果如下:
手动选择控制点(红点就是我们选中的点)
配准效果
再换个图试试吧
控制点选择
配准效果
这么一个简易手动配准工具1.0算是完成了。但是我们使用时遇到了新的问题,那就是需要两幅图的尺寸太大了,显示器根本没法显示完整个图像!有人会说,把图像缩小再配准不行吗?缩小再配准的话,精度就不能保证了,因为配准时像素级别的。要精确配准,就得用原图。
可惜opencv没有提供浏览大图的工具,那就只能自己再写一写了。
好在可以借助前辈们的经验
http://blog.csdn.net/chenyusiyuan/article/details/6565424
那就在原来代码的基础加点东西,来适应这种“浏览大图的效果”。但是其中需要改动的东西很多,所以1.0的代码几乎全改了。因为前辈的这种浏览大图的效果是拥塞的,只能在一幅图操作完之后才可以操作另一幅图,这个限制对于我们配准操作而言是无法接受的,所以我使用了多线程来操作这个窗口,使得我们可以随意在任何一张图片打点,随时切换。
下面是手动配准工具2.0版本的代码
main.cpp
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/opencv.hpp" #include <Windows.h> #include <iostream> #include "NewWindows.h" using namespace std; using namespace cv; void CreateWindows(char* s, char* pic); void CreateWindows2(char* s, char* pic); vector<Point2f> imagePoints1, imagePoints2; //记录匹配点 DWORD WINAPI ThreadFun1(LPVOID pM) { NewWindow ref_obj("基准", "ref.jpg"); ref_obj.CreateWindows(); imagePoints1 = ref_obj.imagePoints; return 0; } DWORD WINAPI ThreadFun2(LPVOID pM) { NewWindow src_obj("待变换", "src.jpg"); src_obj.CreateWindows(); imagePoints2 = src_obj.imagePoints; return 0; } int HandSlectPoint() { Mat tsrc1 = imread("ref.jpg"); //基准图 Mat tsrc2 = imread("src.jpg"); while (1) { #if 1 imagePoints1.clear(); imagePoints2.clear(); HANDLE handle1 = CreateThread(NULL, 0, ThreadFun1, NULL, 0, NULL); //创建线程 HANDLE handle2 = CreateThread(NULL, 0, ThreadFun2, NULL, 0, NULL); printf("往下执行?\n"); //先拥塞住,点选完再进行计算变换矩阵 string s; cin >> s; Mat homo = findHomography(imagePoints2, imagePoints1, CV_RANSAC); Mat imageTransform1; warpPerspective(tsrc2, imageTransform1, homo, Size(tsrc1.cols, tsrc1.rows)); imwrite("trans.jpg", imageTransform1); //把配准后结果存起来 CloseHandle(handle1);//销毁线程1 CloseHandle(handle2);//销毁线程1 #endif printf("是否结束?\n"); //判断是否结束,如果点选得不好,就再来一次 string str; cin >> str; if (str == "yes") break; } return 0; } int main() { HandSlectPoint(); return 0; }
NewWindows.cpp
#include "NewWindows.h" NewWindow::NewWindow(char* label, char* pic_name) { this->pic_name = pic_name; this->label = label; } void NewWindow::mouse_callback(int event, int x, int y, int flags, void* param) { p = Point(x, y); pp = Point(x + x_offset, y + y_offset); if (needScroll) { switch (event) { case CV_EVENT_RBUTTONDOWN: mx = x, my = y; dx = 0, dy = 0; // 按下左键时光标定位在水平滚动条区域内 if (x >= rect_bar_horiz.x && x <= rect_bar_horiz.x + rect_bar_horiz.width && y >= rect_bar_horiz.y && y <= rect_bar_horiz.y + rect_bar_horiz.height) { clickHorizBar = true; } // 按下左键时光标定位在垂直滚动条区域内 if (x >= rect_bar_verti.x && x <= rect_bar_verti.x + rect_bar_verti.width && y >= rect_bar_verti.y && y <= rect_bar_verti.y + rect_bar_verti.height) { clickVertiBar = true; } break; case CV_EVENT_MOUSEMOVE: if (clickHorizBar) { dx = fabs(x - mx) > 1 ? (int)(x - mx) : 0; dy = 0; } if (clickVertiBar) { dx = 0; dy = fabs(y - my) > 1 ? (int)(y - my) : 0; } mx = x, my = y; break; case CV_EVENT_RBUTTONUP: mx = x, my = y; dx = 0, dy = 0; clickHorizBar = false; clickVertiBar = false; break; case CV_EVENT_LBUTTONDOWN: //cvShowImage("jizuhn",dst_img); imagePoints.push_back(pp); cout << label <<": "<< pp << endl; //_p1count++; //cout << "zhihuan count:" << _p1count << endl; flag = 1; //dx = 0, dy = 0; break; default: dx = 0, dy = 0; break; } } } void NewWindow::myShowImageScroll(char* title, IplImage* src_img, int winWidth, int winHeight ) // 显示窗口大小默认为 1400×700 { CvRect rect_dst, // 窗口中有效的图像显示区域 rect_src; // 窗口图像对应于源图像中的区域 int imgWidth = src_img->width, imgHeight = src_img->height, barWidth = 25; // 滚动条的宽度(像素) double scale_w = (double)imgWidth / (double)winWidth, // 源图像与窗口的宽度比值 scale_h = (double)imgHeight / (double)winHeight; // 源图像与窗口的高度比值 if (scale_w<1) winWidth = imgWidth + barWidth; if (scale_h<1) winHeight = imgHeight + barWidth; int showWidth = winWidth, showHeight = winHeight; // rect_dst 的宽和高 int src_x = 0, src_y = 0; // 源图像中 rect_src 的左上角位置 int horizBar_width = 0, horizBar_height = 0, vertiBar_width = 0, vertiBar_height = 0; needScroll = scale_w>1.0 || scale_h>1.0 ? TRUE : FALSE; // 若图像大于设定的窗口大小,则显示滚动条 if (needScroll) { IplImage* dst_img = cvCreateImage(cvSize(winWidth, winHeight), src_img->depth, src_img->nChannels); cvZero(dst_img); // 源图像宽度大于窗口宽度,则显示水平滚动条 if (1) { showHeight = winHeight - barWidth; horizBar_width = (int)((double)winWidth / scale_w); horizBar_height = winHeight - showHeight; horizBar_x = min( max(0, horizBar_x + dx), winWidth - horizBar_width); rect_bar_horiz = cvRect( horizBar_x, showHeight + 1, horizBar_width, horizBar_height); // 显示水平滚动条 cvRectangleR(dst_img, rect_bar_horiz, cvScalarAll(255), -1); } // 源图像高度大于窗口高度,则显示垂直滚动条 if (scale_h > 1.0) { // printf("come!\n"); showWidth = winWidth - barWidth; vertiBar_width = winWidth - showWidth; vertiBar_height = (int)((double)winHeight / scale_h); vertiBar_y = min( max(0, vertiBar_y + dy), winHeight - vertiBar_height); //printf("vertiBar_width:%d vertiBar_height:%d\n", vertiBar_width, vertiBar_height); //printf("x:%d y:%d\n", showWidth + 1, vertiBar_y); rect_bar_verti = cvRect( showWidth + 1, vertiBar_y, vertiBar_width, vertiBar_height); // 显示垂直滚动条 //printf("w:%d h:%d\n", dst_img->width, dst_img->height); cvRectangleR(dst_img, rect_bar_verti, cvScalarAll(255), -1); } showWidth = min(showWidth, imgWidth); showHeight = min(showHeight, imgHeight); // 设置窗口显示区的 ROI rect_dst = cvRect(0, 0, showWidth, showHeight); cvSetImageROI(dst_img, rect_dst); // 设置源图像的 ROI src_x = (int)((double)horizBar_x*scale_w); src_y = (int)((double)vertiBar_y*scale_h); src_x = min(src_x, imgWidth - showWidth); src_y = min(src_y, imgHeight - showHeight); rect_src = cvRect(src_x, src_y, showWidth, showHeight); x_offset = src_x; y_offset = src_y; cvSetImageROI(src_img, rect_src); if (flag == 1) { cvCircle(src_img, p, 3, Scalar(0, 0, 255), -1); flag = 0; } // 将源图像内容复制到窗口显示区 cvCopy(src_img, dst_img); cvResetImageROI(dst_img); cvResetImageROI(src_img); // 显示图像和滚动条 cvShowImage(title, dst_img); cvReleaseImage(&dst_img); } // 源图像小于设定窗口,则直接显示图像,无滚动条 else { cvShowImage(title, src_img); } } void m_callback(int event, int x, int y, int flags, void* param) { NewWindow* p_win = (NewWindow*)param; p_win->mouse_callback(event, x, y, flags, NULL); } void NewWindow::CreateWindows() { int width = 1200, height = 700; //显示的图片大小 cvNamedWindow(label, 1); cvSetMouseCallback(label, m_callback, this); image = cvLoadImage(pic_name, CV_LOAD_IMAGE_COLOR); while (1) { myShowImageScroll(label, image, width, height); //Sleep(100); int KEY = cvWaitKey(10); if ((char)KEY == 27) break; } cvDestroyWindow(label); }
NewWindows.h
#ifndef __NEW_WINDOWS_H__ #define __NEW_WINDOWS_H__ #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc_c.h> #include <Windows.h> #include <iostream> #include <vector> #define FALSE 0 #define TRUE 1 using namespace std; using namespace cv; class NewWindow { public: vector<Point2f> imagePoints; void CreateWindows(); void mouse_callback(int event, int x, int y, int flags, void* param); NewWindow(char* label, char* pic_name); private: double mx = 0, my = 0; int dx = 0, dy = 0, horizBar_x = 0, vertiBar_y = 0; bool clickVertiBar = false, clickHorizBar = false, needScroll = false; CvRect rect_bar_horiz, rect_bar_verti; IplImage* image; Point p; Point pp; int flag = 0; int x_offset; int y_offset; char* pic_name; char* label; void myShowImageScroll(char* title, IplImage* src_img, int winWidth = 1400, int winHeight = 700); // 显示窗口大小默认为 1400×700 }; #endif
看看效果吧,现在我们需要对两张2000*2000的图像进行配准,因为我们的显示器无法完全显示整张图片,所以使用了这个带浏览大图的工具来进行配准。可以看到,显示图的右侧和下侧都有滚动条,我们只需按住鼠标右键拖动即可浏览到显示不到的区域,同样地,我们是点击鼠标左键实现选点。
点的坐标一一记录
配准之后,可以看出图像发生了轻微形变,与基准图一对比,发现配准成功。
【2017.9.23更新】
有几个园友发信息给我,说不知这个程序怎么用,那我在这里总结一下使用步骤:
1.左键选择控制点,右键是用于滚动条的。选择控制点的时候注意在图一选了点后需要在图二也选好对应点,形成控制点对。
2.当控制点对全部选好后按“esc”关闭窗口。要按两次,因为要关闭两个窗口。
3.按键盘任意键开始透视变换。
4.如果你觉得这次变换OK,就按yes退出
相关文章推荐
- OpenCV探索之路(二十二):制作一个类“全能扫描王”的简易扫描软件
- OpenCV探索之路(二十五):制作简易的图像标注小工具
- 分享一个很棒的loading图标制作工具,不会美工的有福了!
- 黑客技巧之教你制作一个简易的QQ炸弹
- 自己动手制作一个简易的shell
- LICEcap 是一个制作gif的工具
- 使用cocos制作一个简易的小闹钟
- 一个轻松制作和处理矢量图的工具和方法
- 一个非常好用的数学函数图像生成工具
- 图像标签制作工具之labelImg的安装与使用
- FreeEverything-基于everything的一个简易磁盘清理工具
- 用arduino制作一个简易抢答器
- 如何制作一个圆角图像
- izpack是一个用于解决安装程序制作的Builder工具
- 手撸一个安装包制作工具(2) --界面库
- 制作一个简易的相册用js
- 用javaScript制作一个简易的课程表
- 终于手动做好了第一个稍微有的形状的MFC程序~一个简陋的图形绘图工具收藏(更新至1.1版)
- 汉典速查: 一个简易的国学阅读工具
- 手撸一个安装包制作工具(1)