• Opencv探索之路(二十):制作一个简易手动图像配准工具


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

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

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

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

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

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

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

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

    #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("往下执行?
    ");
        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("往下执行?
    ");
    
            //先拥塞住,点选完再进行计算变换矩阵
            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("是否结束?
    ");
    
            //判断是否结束,如果点选得不好,就再来一次
            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!
    ");
                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_%d vertiBar_height:%d
    ", vertiBar_width, vertiBar_height);
                //printf("x:%d y:%d
    ", showWidth + 1, vertiBar_y);
                rect_bar_verti = cvRect(
                    showWidth + 1,
                    vertiBar_y,
                    vertiBar_width,
                    vertiBar_height);
                // 显示垂直滚动条  
                //printf("w:%d h:%d
    ", 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退出

  • 相关阅读:
    电子商务:不只是一个网站那么简单
    sqlserver中使用查询分析器的一点小技巧
    以成败论英雄
    SEO 工具集合
    网页中常用JAVASCRIPT技巧
    调用样式表(基础)
    【转载】fontsize:100%有什么作用
    Div+CSS常见错误
    css简单的总结
    【转载】网页尺寸规范
  • 原文地址:https://www.cnblogs.com/skyfsm/p/7253208.html
Copyright © 2020-2023  润新知