【手动配准】python/opencv 手动选取特征点进行图像配准(图像可滚轮缩放、鼠标拖动)

背景

近日在做图像配准相关算法的研究,需要手动地配准图像,并将单应性矩阵保存为numpy数组,作为Ground truth验证配准算法特征点匹配地准确率。


该博主完成了我想实现大部分功能,但他使用的是C++,无法满足我需要保存numpy数组的需求,参考该博客自己写了一个python版本的手动配准代码。

主要功能

  • 弹窗选择待配准图像
  • 滚轮缩放图像
  • 鼠标左键拖拽图像
  • 鼠标右键点选特征点
  • 匹配效果验证

代码

from pathlib import Path

import cv2
import tkinter as tk
from tkinter import filedialog
import tkinter.messagebox as GUI

import numpy as np

# 窗口大小
g_window_wh  = [800, 600]


class struct_getPoint:
    def __init__(self, image, window):
        self.location_click = [0, 0]
        self.location_release = [0, 0]
        self.image_original = image.copy()
        self.image_show = self.image_original[0: g_window_wh[1], 0:g_window_wh[0]]
        self.location_win = [0, 0]
        self.location_win_click = [0, 0]
        self.image_zoom = self.image_original.copy()
        self.zoom = 1
        self.step = 0.1
        self.window_name = window
        self.point = []

    # OpenCV鼠标事件
    def getPoint(self):

        def mouse_callback(event, x, y, flags, param):

            def check_location(img_wh, win_wh, win_xy):
                for i in range(2):
                    if win_xy[i] < 0:
                        win_xy[i] = 0
                    elif win_xy[i] + win_wh[i] > img_wh[i] and img_wh[i] > win_wh[i]:
                        win_xy[i] = img_wh[i] - win_wh[i]
                    elif win_xy[i] + win_wh[i] > img_wh[i] and img_wh[i] < win_wh[i]:
                        win_xy[i] = 0
                # print(img_wh, win_wh, win_xy)

            # 计算缩放倍数
            # flag:鼠标滚轮上移或下移的标识, step:缩放系数,滚轮每步缩放0.1, zoom:缩放倍数
            def count_zoom(flag, step, zoom, zoom_max):
                if flag > 0:  # 滚轮上移
                    zoom += step
                    if zoom > 1 + step * 20:  # 最多只能放大到3倍
                        zoom = 1 + step * 20
                else:  # 滚轮下移
                    zoom -= step
                    if zoom < zoom_max:  # 最多只能缩小到0.1倍
                        zoom = zoom_max
                zoom = round(zoom, 2)  # 取2位有效数字
                return zoom

            if event or flags:
                w2, h2 = g_window_wh  # 窗口的宽高
                h1, w1 = param.image_zoom.shape[0:2]  # 缩放图片的宽高
                if event == cv2.EVENT_LBUTTONDOWN:  # 左键点击
                    param.location_click = [x, y]  # 左键点击时,鼠标相对于窗口的坐标
                    param.location_win_click = [param.location_win[0],
                                                param.location_win[1]]  # 窗口相对于图片的坐标,不能写成location_win = g_location_win

                elif event == cv2.EVENT_MOUSEMOVE and (flags & cv2.EVENT_FLAG_LBUTTON):  # 按住左键拖曳
                    param.location_release = [x, y]  # 左键拖曳时,鼠标相对于窗口的坐标
                    if w1 <= w2 and h1 <= h2:  # 图片的宽高小于窗口宽高,无法移动
                        param.location_win = [0, 0]
                    elif w1 >= w2 and h1 < h2:  # 图片的宽度大于窗口的宽度,可左右移动
                        param.location_win[0] = param.location_win_click[0] + param.location_click[0] - \
                                                param.location_release[0]
                    elif w1 < w2 and h1 >= h2:  # 图片的高度大于窗口的高度,可上下移动
                        param.location_win[1] = param.location_win_click[1] + param.location_click[1] - \
                                                param.location_release[1]
                    else:  # 图片的宽高大于窗口宽高,可左右上下移动
                        param.location_win[0] = param.location_win_click[0] + param.location_click[0] - \
                                                param.location_release[0]
                        param.location_win[1] = param.location_win_click[1] + param.location_click[1] - \
                                                param.location_release[1]
                    check_location([w1, h1], [w2, h2], param.location_win)  # 矫正窗口在图片中的位置

                elif event == cv2.EVENT_MOUSEWHEEL:  # 滚轮
                    z = param.zoom  # 缩放前的缩放倍数,用于计算缩放后窗口在图片中的位置
                    zoom_max = g_window_wh[0] / param.image_original.shape[1]
                    param.zoom = count_zoom(flags, param.step, param.zoom, zoom_max)  # 计算缩放倍数
                    w1, h1 = [int(param.image_original.shape[1] * param.zoom),
                              int(param.image_original.shape[0] * param.zoom)]  # 缩放图片的宽高
                    param.image_zoom = cv2.resize(param.image_original, (w1, h1), interpolation=cv2.INTER_AREA)  # 图片缩放
                    param.location_win = [int((param.location_win[0] + x) * param.zoom / z - x),
                                          int((param.location_win[1] + y) * param.zoom / z - y)]  # 缩放后,窗口在图片的位置
                    check_location([w1, h1], [w2, h2], param.location_win)  # 矫正窗口在图片中的位置

                elif event == cv2.EVENT_RBUTTONDOWN:  # 右键选点
                    point_num = len(param.point)
                    [x_ori, y_ori] = [int((param.location_win[0] + x) / param.zoom),
                                      int((param.location_win[1] + y) / param.zoom)]
                    param.point.append([x_ori, y_ori])
                    cv2.circle(param.image_original, (x_ori, y_ori), 3, (255, 0, 0), thickness=-1)  # 画圆半径为3,并填充
                    cv2.putText(param.image_original, str(point_num + 1), (x_ori, y_ori), cv2.FONT_HERSHEY_PLAIN,
                                1.0, (0, 255, 0), thickness=1)  # 加入文字,位置,字体,尺度因子,颜色,粗细
                param.image_zoom = cv2.resize(param.image_original, (w1, h1), interpolation=cv2.INTER_AREA)  # 图片缩放
                param.image_show = param.image_zoom[param.location_win[1]:param.location_win[1] + h2,
                                   param.location_win[0]:param.location_win[0] + w2]  # 实际的显示图片
                cv2.imshow(param.window_name, param.image_show)

        cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL)
        cv2.resizeWindow(self.window_name, g_window_wh[0], g_window_wh[1])
        cv2.imshow(self.window_name, self.image_show)
        cv2.setMouseCallback(self.window_name, mouse_callback, self)




def getHomography():

    while True:
        root_path1 = filedialog.askopenfilename(title='请选择图片')
        root_path2 = filedialog.askopenfilename(title='请选择图片')

        img1 = cv2.imread(root_path1)
        img2 = cv2.imread(root_path2)
        imgIn1 = img1.copy()
        imgIn2 = img2.copy()

        # 点选特征点
        C1 = struct_getPoint(img1, "window1")
        C1.getPoint()

        C2 = struct_getPoint(img2, "window2")
        C2.getPoint()

        cv2.waitKey(0)  # 等待键盘点击事件来结束阻塞

        point1 = C1.point
        point2 = C2.point

        # 判断特征点数量是否一致
        if len(point1) != len(point2):
            GUI.showinfo(title='提示', message='特征数量不一致,请重新选择')
            continue

        # 估计由输出坐标到输入坐标的单应性变换模型
        Homo, error= cv2.findHomography(np.array(point1), np.array(point2))

        # 产生输出图像
        [H, W] = img2.shape[0:2]
        imgOut = cv2.warpPerspective(img1, Homo, (W, H))
        imgRes = cv2.addWeighted(img2, 0.5, imgOut, 0.5, 0)
        cv2.namedWindow("result", flags=cv2.WINDOW_NORMAL | cv2.WINDOW_FREERATIO)
        cv2.imshow('result', imgRes)
        cv2.waitKey(0)

        cv2.destroyAllWindows()  # 销毁所有窗口


if __name__ == '__main__':
    root = tk.Tk()
    root.withdraw()

    getHomography()

欢迎大家交流讨论!