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("往下執行?\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退出


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM