منبع اصلی نوشتار زیر در این لینک قرار دارد

ثبت تصویر image registration با استفاده از OpenCV

فرض کنید که یک تصویر هوایی از یک ناحیه شهر دارید و بعد از مدتی یک تصویر دیگر هوایی دریافت می‌کنید که بخش‌هایی از تصویر قبلی در آن موجود است. حالا می‌خواهید دو تصویر را به صورت اتوماتیک طوری روی هم قرار دهید که قسمت‌های متناظر روی هم بی‌افتند.  تصویر جدید و قدیم را باهم داشته باشید. یا فرض کنید از شهر یک سری عکس هوایی دریافت کرده اید که این عکس ها با هم اشتراک دارند و شما می‌خواهید آن‌ها را اتوماتیک به هم بچسبایند. یا فرض کنید از آسمان چندین عکس تهیه  کرده‌اید و می‌خواهید آن‌ها را کنار هم قرار دهید و یک تصویر واحد بسازید. به این کار ثبت تصویر یا image registration می‌گویند. در این پس می‌خواهیم این کار را با استفاده از پایتون و OpenCV انجام دهیم.

راه‌های مختلفی برای  این کار هست که امروز یکی از راه‌های ساده برای این کار را بررسی می‌کنیم و کدش را ارائه می‌دهیم. روش‌های بسیار متنوعی برای این کار وجود دارد و برای کابردهای مختلف روش‌های مختلفی وجود دارد. روش کلی برای انجام این کار وجود ندارد و باید برای کارهای مختلف روش های مختلفی ایجاد کرد تا بهترین نتیجه حاصل شود.

در پست بازشناسی یک شی در تصویر با استفاده از OpenCV در مورد نقاط کلیدی صحبت کردیم. یکی از بخش‌ها اصلی روش ارائه شده در این پست برای تبت تصویراستفاده از نقاط کلیدی و توصیفگر‌های ORB است.  تابع extract_orb برای تصویر یک سری نقطه کلیدی استخراج می‌کند و برای هر نقطه کلیدی یک توصیفگر ایجاد می‌کند.

def extract_orb(img):
    """
    Extract orb feature from image
    :param img: input image
    :return:
    """
    # convert to gray
    if len(img.shape) > 2:
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    else:
        img_gray = img.copy()

    # orb keypoint detector and descriptor
    orb_obj = cv2.ORB_create(nfeatures=1500)

    # extract orb keypoints and descriptors
    kp, desc = orb_obj.detectAndCompute(img_gray, None)

    # keypoints coordinate in 2D image
    kp = np.array([p.pt for p in kp]).T
    return kp, desc

 

تابع match_orb توصیفگرهای نقاط کلیدی دو تصویر مختلف را می‌گیرد و با استفاده از KNN دو نقطه کلیدی نزدیک به  هر نقطه کلیدی را پیدا می‌کند. در صورتی که فاصله توصیفگر نقطه کلیدی تا نقطه کلیدی اول به طرز قابل قبولی کمتر از فاصله نقطه کلیدی تا نقطه کلیدی دوم باشد، نشان می‌دهند که نقطه اول یک نقطه کلیدی متناظر خوب برای نقطه کلیدی است. در غیر این صورت از آن صرف نظر می‌شود و گفته می‌شود تطبیقی برای آن نقطه پیدا نشده‌است. خروجی این تابع اندیس نقاط کلیدی منطبق شده است.

def match_orb(descriptor_source, descriptor_target):
    """
    math keypoints of two images
    :param descriptor_source:
    :param descriptor_target:
    :return:
    """
    # Matcher
    bf = cv2.BFMatcher()
    matches = bf.knnMatch(descriptor_source, descriptor_target, k=2)

    # keep good matches
    good_matches = np.array([], dtype=np.int32).reshape((0, 2))
    matches_num = len(matches)
    for i in range(matches_num):
        if matches[i][0].distance <= 0.8 * matches[i][1].distance:
            temp = np.array([matches[i][0].queryIdx,
                             matches[i][0].trainIdx])
            good_matches = np.vstack((good_matches, temp))
    return good_matches

 

فرض کنید که نقاط A را داریم و نقاط B نقاط متناظر با نقاط A در یک دستگاه مختصات دیگر هستند که با استفاده از نگاشت X[A]+Y = B به دست می‌آیند. حالا فرض کنید که نقاط A و نقاط B را داده‌اند و می‌خواهیم این نگاشت خطی را پیدا کنیم. برای این کار از تابع estimate_affine_transform استفاده می‌کنیم.

def estimate_affine_transform(src_points, trg_points):
    """
    Estimate affine transform by getting a set of points and their transformed
    :param src_points:
    :param trg_points:
    :return:
    """
    num_of_points = src_points.shape[1]
    M = np.zeros((2 * num_of_points, 6))
    for i in range(num_of_points):
        temp = [[src_points[0, i], src_points[1, i], 0, 0, 1, 0],
                [0, 0, src_points[0, i], src_points[1, i], 0, 1]]
        M[2 * i: 2 * i + 2, :] = np.array(temp)
    b = trg_points.T.reshape((2 * num_of_points, 1))
    theta = np.linalg.lstsq(M, b)[0]
    X_trans = theta[:4].reshape((2, 2))
    Y_trans = theta[4:]
    return X_trans, Y_trans

 

فرض کنید که نقاط A را در یک دستگاه مختصات داریم و می‌دانیم که اگر آن‌ها را با نگاشت خطی X[A] + ‌Y به یک دستگاه مختصات دیگر ببریم، نقطه متناظر نقاط A، نقاط B می‌شود. حالا فرض کنید که A و B را داریم ولی نگاشت خطی X[ ]+Y را نداریم. به ما یک نگاشت خطی P[ ] + Q می‌دهند و می‌خواهیم بدانیم که چقدر این  نگاشت خطی دقیق است و عمل نگاشت را درست انجام می‌دهد. برای این کار نقاط A را با استفاده از PA+Q به مختصات جدید می‌بریم و مجذور مربعات فاصله نقاط را با نقاط متناظرشان که B باشد می‌سنجیم. هر چه این مقدار کمتر باشد نشان می‌دهد نگاشت خطی داده شده بهتر است. این کاری است که تابع mse_error انجام می‌دهد.

 

def mse_error(X_trans, Y_trans, src_points, trg_points):
    """

    :param X_trans: transform parameter X[]+Y
    :param Y_trans: transform parameter X[]+Y
    :param src_points: source points
    :param trg_points: traget points
    :return:
    """
    # transform source points by affine transfor X[]+Y
    src_map_on_trg = np.dot(X_trans, src_points) + Y_trans

    # calculate MSE error
    diff_square = np.power(src_map_on_trg - trg_points, 2)
    mse = np.sqrt(np.sum(diff_square, axis=0))

    # return mse error
    return mse

 

همه نقاط متناظری که با استفاده از تطبیق نقاط کلیدی با استفاده از توصیفگر‌هایشان به دست آمد، تقاط متناظر خوبی نیستند و اشتباهاتی در آن‌ها موجود است. تابع RANSAC هر بار تعداد از نقاط را انتخاب می‌کند. سپس با استفاده از آن نقاط و تابع estimate_affine_transform یک نگاشت خطی پیدا می‌کند و سپس با استفاده mse_error دقت نگاشت به دست آمده برای سایر نقاط را می‌سنجد. اینگونه بهترین نگاشت را پیدا می‌کند و تناظر‌های خوب و بد را از هم تشخیص می‌دهد.

def ransac(points_src, points_target):
    """

    :param points_src:
    :param points_target:
    :return:
    """
    # in each iteration select k point randomly
    K = 3
    # if mse error in less than threshold consider it
    mse_threshold = 1.0
    # maximum number of iterations
    number_of_iteration = 3000

    inliers_num_final = 0
    X_trans_final = None
    Y_trans_final = None
    inliers = None

    # for number of iteration repeat
    for i in range(number_of_iteration):

        # select K points randomly
        k_selected = np.random.randint(0, points_src.shape[1], (K, 1))

        # find affine transform by K selected points
        X_trans, Y_trans = estimate_affine_transform(src_points=points_src[:, k_selected],
                                                     trg_points=points_target[:, k_selected])

        # calculate mse error of affine transform
        mse = mse_error(X_trans=X_trans, Y_trans=Y_trans,
                        src_points=points_src, trg_points=points_target)

        if not (mse is None):
            inliers_tmp = np.where(mse < mse_threshold)
            inliers_num_tmp = len(inliers_tmp[0])

            # keep best affine transform until now
            if inliers_num_tmp > inliers_num_final:
                inliers_num_final = inliers_num_tmp
                inliers = inliers_tmp
                X_trans_final = X_trans
                Y_trans_final = Y_trans
        else:
            pass

    # return affine transform X[]+Y and inliers
    return X_trans_final, Y_trans_final, inliers

 

تابع affine_matrix نقاط کلیدی متناظر که توسط match_orb پیدا شده‌اند را می‌گیرد. سپس با استفاده از ransac تناظر‌های خوب از میان آن‌ها را پیدا می‌کند و تناظر‌های بد را کنار می‌گذارد. سپس با استفاده از تناظر‌های خوب پیدا شده توسط Ransac  و تابع estimate_affine_transform، ماتریس نگاشت خطی M را پیدا می‌کند.

def affine_matrix(points_in_src_img, points_in_trg_img, good_matches):
    """
    Calculate affine transform
    :param points_in_src_img: 
    :param points_in_trg_img: 
    :param good_matches: 
    :return: 
    """
    # keypoints in image 1 that are in matched points
    points_in_src_img = points_in_src_img[:, good_matches[:, 0]]

    # keypoints in image 2 that are in matched points
    points_in_trg_img = points_in_trg_img[:, good_matches[:, 1]]

    # calculate inliers with RANSAC algorithm
    _, _, inliers = ransac(points_in_src_img, points_in_trg_img)

    # keypoints in image 1 that are in inliers points
    points_in_src_img = points_in_src_img[:, inliers[0]]
    # keypoints in image 2 that are in inliers points
    points_in_trg_img = points_in_trg_img[:, inliers[0]]

    # estimate affine transform X[]+Y by inliers points in first and second image
    X_trans, Y_trans = estimate_affine_transform(points_in_src_img, points_in_trg_img)
    M = np.hstack((X_trans, Y_trans))
    return M

 

تابع two_image_registraion_without_padding، دو تصویر را می‌گیرد و با استفاده از تابع‌هایی که گفته شده کار ثبت تصویر یا image registration را انجام می‌دهد. فقط مشکلی که این تابع دارد این است که بعد از انتقال تصویر اول به دستگاه مختصات تصویر دوم، ممکن است بخشی از تصویر اول بیرون بزند. این مشکل در تابع two_images_registraion_padding حل شده است.  تابع multiple_image_registration مانند تابع  two_images_registraion_padding است با این تفاوت که بیش از دو تصویر می‌گیرد.

 

کد کلی برنامه درانتهای مطلب آورده شده‌است. در ادامه به بعضی از نمونه‌های خروجی این برنامه می‌پردازیم:

 

نقشه تهران:

تصاویر ورودی:

 

 

خروجی الگوریتم:

 

تصویر هوایی تهران:

 

خروجی برنامه:

 

 

 

 

مقبره حکیم فردوسی:

 

 

خروجی برنامه:

 

راه شیری:

 

خروجی برنامه:

 

import numpy as np
import cv2


def extract_orb(img):
    """
    Extract orb feature from image
    :param img: input image
    :return:
    """
    # convert to gray
    if len(img.shape) > 2:
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    else:
        img_gray = img.copy()

    # orb keypoint detector and descriptor
    orb_obj = cv2.ORB_create(nfeatures=1500)

    # extract orb keypoints and descriptors
    kp, desc = orb_obj.detectAndCompute(img_gray, None)

    # keypoints coordinate in 2D image
    kp = np.array([p.pt for p in kp]).T
    return kp, desc


def match_orb(descriptor_source, descriptor_target):
    """
    math keypoints of two images
    :param descriptor_source:
    :param descriptor_target:
    :return:
    """
    # Matcher
    bf = cv2.BFMatcher()
    matches = bf.knnMatch(descriptor_source, descriptor_target, k=2)

    # keep good matches
    good_matches = np.array([], dtype=np.int32).reshape((0, 2))
    matches_num = len(matches)
    for i in range(matches_num):
        if matches[i][0].distance <= 0.8 * matches[i][1].distance:
            temp = np.array([matches[i][0].queryIdx,
                             matches[i][0].trainIdx])
            good_matches = np.vstack((good_matches, temp))
    return good_matches


def estimate_affine_transform(src_points, trg_points):
    """
    Estimate affine transform by getting a set of points and their transformed
    :param src_points:
    :param trg_points:
    :return:
    """
    num_of_points = src_points.shape[1]
    M = np.zeros((2 * num_of_points, 6))
    for i in range(num_of_points):
        temp = [[src_points[0, i], src_points[1, i], 0, 0, 1, 0],
                [0, 0, src_points[0, i], src_points[1, i], 0, 1]]
        M[2 * i: 2 * i + 2, :] = np.array(temp)
    b = trg_points.T.reshape((2 * num_of_points, 1))
    theta = np.linalg.lstsq(M, b)[0]
    X_trans = theta[:4].reshape((2, 2))
    Y_trans = theta[4:]
    return X_trans, Y_trans


def mse_error(X_trans, Y_trans, src_points, trg_points):
    """

    :param X_trans: transform parameter X[]+Y
    :param Y_trans: transform parameter X[]+Y
    :param src_points: source points
    :param trg_points: traget points
    :return:
    """
    # transform source points by affine transfor X[]+Y
    src_map_on_trg = np.dot(X_trans, src_points) + Y_trans

    # calculate MSE error
    diff_square = np.power(src_map_on_trg - trg_points, 2)
    mse = np.sqrt(np.sum(diff_square, axis=0))

    # return mse error
    return mse


def ransac(points_src, points_target):
    """

    :param points_src:
    :param points_target:
    :return:
    """
    # in each iteration select k point randomly
    K = 3
    # if mse error in less than threshold consider it
    mse_threshold = 1.0
    # maximum number of iterations
    number_of_iteration = 3000

    inliers_num_final = 0
    X_trans_final = None
    Y_trans_final = None
    inliers = None

    # for number of iteration repeat
    for i in range(number_of_iteration):

        # select K points randomly
        k_selected = np.random.randint(0, points_src.shape[1], (K, 1))

        # find affine transform by K selected points
        X_trans, Y_trans = estimate_affine_transform(src_points=points_src[:, k_selected],
                                                     trg_points=points_target[:, k_selected])

        # calculate mse error of affine transform
        mse = mse_error(X_trans=X_trans, Y_trans=Y_trans,
                        src_points=points_src, trg_points=points_target)

        if not (mse is None):
            inliers_tmp = np.where(mse < mse_threshold)
            inliers_num_tmp = len(inliers_tmp[0])

            # keep best affine transform until now
            if inliers_num_tmp > inliers_num_final:
                inliers_num_final = inliers_num_tmp
                inliers = inliers_tmp
                X_trans_final = X_trans
                Y_trans_final = Y_trans
        else:
            pass

    # return affine transform X[]+Y and inliers
    return X_trans_final, Y_trans_final, inliers


def affine_matrix(points_in_src_img, points_in_trg_img, good_matches):
    """
    Calculate affine transform
    :param points_in_src_img: 
    :param points_in_trg_img: 
    :param good_matches: 
    :return: 
    """
    # keypoints in image 1 that are in matched points
    points_in_src_img = points_in_src_img[:, good_matches[:, 0]]

    # keypoints in image 2 that are in matched points
    points_in_trg_img = points_in_trg_img[:, good_matches[:, 1]]

    # calculate inliers with RANSAC algorithm
    _, _, inliers = ransac(points_in_src_img, points_in_trg_img)

    # keypoints in image 1 that are in inliers points
    points_in_src_img = points_in_src_img[:, inliers[0]]
    # keypoints in image 2 that are in inliers points
    points_in_trg_img = points_in_trg_img[:, inliers[0]]

    # estimate affine transform X[]+Y by inliers points in first and second image
    X_trans, Y_trans = estimate_affine_transform(points_in_src_img, points_in_trg_img)
    M = np.hstack((X_trans, Y_trans))
    return M


def two_image_registraion_without_padding(img_source, img_target):
    """
    Gets two image an do image registration
    :param img_source:
    :param img_target:
    :return:
    """
    # extract orb features for both images
    keypoint_source, descriptor_source = extract_orb(img_source)
    keypoint_target, descriptor_target = extract_orb(img_target)

    # math keypoints of two image
    matched_points = match_orb(descriptor_source, descriptor_target)

    # calculate affine matrix
    M = affine_matrix(keypoint_source, keypoint_target, matched_points)
    rows, cols, _ = img_target.shape

    # transform source image by H
    transformed_src_img = cv2.warpAffine(src=img_source, M=M, dsize=(cols, rows))

    # find where will be corners of source image
    new_corners = np.dot(a=M, b=np.array([[0, img_source.shape[1]-1, 0, img_source.shape[1]-1],
                                          [0, 0, img_source.shape[0]-1, img_source.shape[0]-1],
                                          [1, 1, 1, 1]]))

    # merge two images
    merged_image = cv2.addWeighted(src1=transformed_src_img, alpha=0.5, src2=img_target, beta=0.5, gamma=0)

    return merged_image, new_corners


def two_images_registraion_padding(img_source, img_target):
    """
    Does image registration with padding. no part of imaged will be lost
    :param img_source:
    :param img_target:
    :return:
    """
    # do image registration without padding
    merged_image_without_padding, new_corners = two_image_registraion_without_padding(img_source=img_source,
                                                                                      img_target=img_target)

    # find who two padd
    x_min = int(np.floor(new_corners[0, :].min()))
    x_max = int(np.ceil(new_corners[0, :].max()))
    y_min = int(np.floor(new_corners[1, :].min()))
    y_max = int(np.ceil(new_corners[1, :].max()))

    left, right, top, bottom = 0, 0, 0, 0

    if x_min < 0:
        left = -x_min

    if x_max > img_target.shape[1]:
        right = x_max-img_target.shape[1]

    if y_min < 0:
        top = -y_min

    if y_max > img_target.shape[0]:
        bottom = y_max - img_target.shape[0]

    # pad target image
    padded_img_target = cv2.copyMakeBorder(src=img_target, top=top, bottom=bottom,
                                           left=left, right=right,
                                           borderType=cv2.BORDER_CONSTANT)

    # do image registration
    merged_image_with_padding, _ = two_image_registraion_without_padding(img_source=img_source,
                                                                         img_target=padded_img_target)

    return merged_image_with_padding


def multiple_image_registration(list_imgs):
    """
    gets two or more than two images and do image registration by all of them
    :param list_imgs:
    :return:
    """
    if len(list_imgs) < 2:
        raise ValueError('Need at least 2 images!')

    for i in range(1, len(list_imgs)):
        if i == 1:
            merged_image = two_images_registraion_padding(img_source=list_imgs[0],
                                                          img_target=list_imgs[1])
        else:
            merged_image = two_images_registraion_padding(img_source=merged_image,
                                                          img_target=list_imgs[i])

    return merged_image


if __name__ == "__main__":
    np.random.seed(1)

    # ================================================
    # test image registration with two image
    # read images
    img_source = cv2.imread("tehran-1.png")
    img_target = cv2.imread("tehran-2.png")

    # do image registration
    merged_image = two_images_registraion_padding(img_source=img_source, img_target=img_target)

    # save image
    cv2.imwrite(filename='result_of_registration_two_images_tehran_map.png', img=merged_image)

    # cv2.imshow('img_source', img_source)
    # cv2.imshow('img_target', img_target)
    cv2.imshow('result_of_registration_two_images_tehran_map', merged_image)

    # ================================================
    # test image registration with two image
    # read images
    img_source = cv2.imread("ferdosi-1.png")
    img_target = cv2.imread("ferdosi-2.png")

    # do image registration
    merged_image = two_images_registraion_padding(img_source=img_source, img_target=img_target)

    # save image
    cv2.imwrite(filename='result_of_registration_two_images_hakim_ferdosi.png', img=merged_image)

    # cv2.imshow('img_source', img_source)
    # cv2.imshow('img_target', img_target)
    cv2.imshow('result_of_registration_two_images_hakim_ferdosi', merged_image)

    # ================================================
    # test image registration with several images
    img1 = cv2.imread(filename='teh1.png')
    img2 = cv2.imread(filename='teh2.png')
    img3 = cv2.imread(filename='teh3.png')

    # do image registration with several image
    merged_image = multiple_image_registration(list_imgs=[img1, img2, img3])

    cv2.imwrite(filename='result_of_registration_several_images_tehran.png', img=merged_image)
    cv2.imshow('result_of_registration_several_images_tehran', merged_image)

    # ================================================
    # test image registration with several images
    img1 = cv2.imread(filename='milkyway1.png')
    img2 = cv2.imread(filename='milkyway2.png')
    img3 = cv2.imread(filename='milkyway3.png')

    # do image registration with several image
    merged_image = multiple_image_registration(list_imgs=[img1, img2, img3])

    cv2.imwrite(filename='result_of_registration_several_images_milkyway.png', img=merged_image)
    cv2.imshow('result_of_registration_several_images_milkyway', merged_image)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

 

 

 

 

 

 

 

نوشته ثبت تصویر image registration با استفاده از OpenCV اولین بار در اوپن مایند. پدیدار شد.



برچسب ها : ,