OpenCV-Python图像透视变换处理:魔变车牌案例

☞ ░ 老猿Python博文目录:https://blog.csdn.net/LaoYuanPython

一、引言

在《https://blog.csdn.net/LaoYuanPython/article/details/114003964 OpenCV-Python图像处理:透视变换概念、矩阵及实现案例详解》介绍了透视变换的概念、原理、OpenCV-Python相关的函数,并提供了一个简单案例说明了OpenCV-Python进行透视变换的实现方法。

在《https://blog.csdn.net/LaoYuanPython/article/details/113958088 狗狗变形记:任选4点的投影变换warpPerspective OpenCV-Python案例》介绍了一个在图像中自行选择4个点进行透视变换的稍微复杂案例。

本节将介绍一个更复杂的案例,通过该案例我们一方面可以进一步深入理解透视变换及应用OpenCV-Python进行应用实现的步骤,另一方面还可以看到进行透视变换处理时需要注意的点。

二、案例说明

2.1、案例背景

本案例输入图像为两辆带车牌的汽车图片,二者图像大小不一样、车牌展现的角度不一样,程序支持通过鼠标在图像上分别标记两个车牌左上角、右上角、左下角、右下角,然后进行两车车牌的交换。图片来源于网络,分别保存在f:\pic\Audi.JPGf:\pic\BMW.JPG中。图像如下:在这里插入图片描述
在这里插入图片描述
另外本节会使用到《https://blog.csdn.net/LaoYuanPython/article/details/111351901 OpenCV-Python图形图像处理:自用的一些工具函数功能及调用语法介绍》介绍的老猿自定义的常用函数constructRectFrom4Points、translation等,具体函数功能请参考原文的介绍。

2.2、实现思路

在程序中:

  1. 首先支持将两个车牌位置使用鼠标进行标记,就是用鼠标点击车牌四角;
  2. 以宝马车牌四角构造的四边形为依据,构建一个和坐标轴平行的矩形,构建原则是取四边形左上角的点为矩形的左上角点,四边形上边和下边x、y坐标的最大差距作为矩形横边的边长,以及侧边的边长
  3. 根据奥迪车车牌左上角、宝马车车牌构建的矩形边长构建一个和坐标轴平行的矩形
  4. 以源车牌选定4个点和对应新构建的矩形4个点对作为透视变换矩阵的映射点对,通过getPerspectiveTransform获取透视变换矩阵
  5. 对两个原图像进行透视变换,然后将2个矩形位置的图像相互替换,获得中间图像;
  6. 以前面获得的透视矩阵的逆矩阵对两个中间头像进行透视变换的逆变换。

上述步骤中,构建中间图像进行替换,是因为两个车牌大小不同、视角不同,对应车牌在图像矩阵中的内容不是一个矩形,不好进行替换操作,因此先统一转换成同大小、同视角的矩形再进行车牌替换,替换后需要恢复大小和视角,所以需要逆变换回去。

2.3、示例代码

import cv2
import numpy as np
from opencvPublic import replaceImgRegionBySpecImg,constructRectFrom4Points,translation

def getRect4Point(rect):
    """
    根据矩形的左上角坐标及长和高返回矩形的左上、右上、左下、右下四个点的坐标
    :param rect:为代表矩形的列表,三个元素,分别为左上角坐标、宽和高
    :return:返回矩形的四个顶点,分别是左上、右上、左下、右下四个点的坐标
    """
    luPoint, w, h = rect
    x0,y0 = luPoint
    return (luPoint,(x0+w,y0),(x0,y0+h),(x0+w,y0+h))

def OnMouseEvent( event, x, y, flags, param):
    winName,lbtDownPos,pointList,img= param

    if event == cv2.EVENT_LBUTTONUP:
        pos = (x,y)
        print("OnMouseEvent EVENT_LBUTTONUP:",pos)
        n = len(pointList)
        if pos==lbtDownPos:
            n += 1
            if n <= 4:
                pointList.append(pos)
                cv2.putText(img, '.', (x-15, y+3), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=3, color=(255, 0, 0)) #坐标调整是因为点放大了占用了不止一个像素
                cv2.circle(img,(x , y),10,(0,0,255))
                cv2.putText(img, f'{n}', (x + 20, y), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.3, color=(255, 0, 0))
                cv2.imshow(winName,img)
        lbtDownPos = None

    elif event == cv2.EVENT_LBUTTONDOWN:
        param[1] = (x,y)
        print("OnMouseEvent EVENT_LBUTTONDOWN:", lbtDownPos)

    else:lbtDownPos = None


def getselectRangePoint(img,winName,bWaitEscKey=False):
    """
       :param img: 输入图像
       :param winName: 图像窗口展示名
       :param bWaitEscKey: 是否需要设置循环等待
       :return: 一个包含4个元素的列表,4个元素分别是winName、当前最后一个选择点的坐标、选择的4个点的坐标列表、输入图像img
    """

    cv2.putText(img, 'https://blog.csdn.net/LaoYuanPython', (100, 120), fontFace=cv2.FONT_HERSHEY_SIMPLEX,fontScale=0.5, color=(255, 0, 0))
    rows,cols = img.shape[:2]

    cv2.namedWindow(winName)

    param=[winName,None,[],img]
    cv2.setMouseCallback(winName, OnMouseEvent, param)
    print("请在图中用鼠标左键分别点击ROI区域左上角、右上角、左下角、右下角4个点,选择完成后按ESC退出")

    cv2.imshow(winName, img)

    if bWaitEscKey:
        while True:  # 通过鼠标左键点击选择4个点,分别代表要映射到左上角、右上角、左下角、右下角4个点
            ch = cv2.waitKey(100)
            if ch == 27: break
    return param

def chanePlate(srcCarImgFile,destCarImgFile):
    #加载两车图像
    srcImg = cv2.imread(srcCarImgFile)
    destImg = cv2.imread(destCarImgFile)
    r1,c1 = srcImg.shape[:2]
    r2,c2 = destImg.shape[:2]

    srcImg = translation(srcImg,100,100,(c1+100,r1+100))
    destImg = translation(destImg, 100, 100, (c2 + 100, r2 + 100))
    #选择两车的车牌四角点(左上角、右上角、左下角、右下角)
    srcInf = getselectRangePoint(np.array(srcImg),"select the plate range of source car")
    destInf = getselectRangePoint(np.array(destImg),'select the plate range of target car',True)
    srcPointsSelected = srcInf[2]
    destPointsSelected = destInf[2]

    if len(srcPointsSelected)<4:
        print("源图像没有选择足够的点,请点击源图像用于选择ROI区域左上角、右上角、左下角、右下角4个点")
    elif len(destPointsSelected)<4:
        print("目标图像没有选择足够的点,请点击目标图像替换区域左上角、右上角、左下角、右下角4个点")
    else:
        destRect = constructRectFrom4Points(destPointsSelected)  #根据目标车牌的左上角、右上角、左下角、右下角4四角顶点以左上角构建矩形
        srcRect = (srcPointsSelected[0], destRect[1], destRect[2]) #根据目标车牌的矩形大小以源图像车牌左上角构建一个相同大小的矩形

        #根据车牌的四顶点将车牌区域映射到对应矩形进行透视变换得到统一车牌大小的中间图像,确保两车车牌大小和形状相同
        destRectPoints = np.float32(getRect4Point(destRect))
        srcRectPoints = np.float32(getRect4Point(srcRect))
        srcPoints = np.float32(srcPointsSelected)
        destPoints = np.float32(destPointsSelected)
        srcM = cv2.getPerspectiveTransform(srcPoints, srcRectPoints)
        destM = cv2.getPerspectiveTransform(destPoints,destRectPoints)
        dstSrc = cv2.warpPerspective(srcImg, srcM, (srcImg.shape[1]*2,srcImg.shape[0]*2))
        dstDst = cv2.warpPerspective(destImg, destM, (destImg.shape[1] * 2, destImg.shape[0] * 2))
        cv2.imshow("mid img-src", dstSrc)
        cv2.imshow("mid img-dst", dstDst)
        #将中间图像的目标车辆牌换成源车车牌
        srcX0,srcY0 = srcPointsSelected[0] #源车牌左上角点
        destX0,destY0 = destPointsSelected[0]#目标车牌左上角点
        w = destRect[1]
        h = destRect[2]
        srcPlate = np.array(dstSrc[srcY0:srcY0+h,srcX0:srcX0+w])#取源车牌对应的图像
        destPlate = np.array(dstDst[destY0:destY0+h,destX0:destX0+w]) #取目标车牌对应的图像
        replaceImgRegionBySpecImg(dstDst,destPointsSelected[0],srcPlate) #将目标车牌的图像换成源车牌图像
        replaceImgRegionBySpecImg(dstSrc, srcPointsSelected[0], destPlate)  # 将源车牌的图像换成目标车牌图像

        #将中间图像映射回原角度
        resultImgDest = cv2.warpPerspective(dstDst, destM, (destImg.shape[1], destImg.shape[0]),flags= cv2.INTER_LINEAR | cv2.WARP_INVERSE_MAP)
        resultImgSrc= cv2.warpPerspective(dstSrc, srcM, (srcImg.shape[1], srcImg.shape[0]),flags=cv2.INTER_LINEAR | cv2.WARP_INVERSE_MAP)
        cv2.destroyAllWindows()
        cv2.imshow("result img-dest", resultImgDest)
        cv2.imshow("result img-src", resultImgSrc)
        ch = cv2.waitKey(0)



chanePlate(r'f:\pic\Audi.JPG',r'f:\pic\BMW.JPG')

2.4、运行截图

选择车牌四角顶点的截图
在这里插入图片描述
在这里插入图片描述
替换后的图像
在这里插入图片描述
在这里插入图片描述

三、小结

本文介绍了实现两车交换车牌的详细过程,并提供了OpenCV-Python示例代码,输入处理的两车图像大小及车牌视角朝向不同,只能通过透视变换先统一视角和车牌大小后才能进行车牌替换,替换后需要还原原图像各自的大小和视角。根据老猿的操作经验,利用透视变换操作过程中需要注意:

  1. 变换时选择的四个点,从视角来看尽量是在两条平行线上(平行线在无穷远处相交),否则图像变换过程中可能产生畸变,即使逆变换也不一定能变换回来;

    下图是首次透视变换没有逆变回来的中间图像截图:


    在这里插入图片描述
    可以看到图像左下角已经超出范围。
  2. 变换时最好给图像四周预留一定空间,用为变换后图像大小范围有可能超出原图像大小范文,这样才能保证图像变换后没有图像丢失。本案例中使用了扩大目标图像大小、变换前先进行平移来预留空间。

更多图像处理的介绍请参考专栏《OpenCV-Python图形图像处理 https://blog.csdn.net/laoyuanpython/category_9979286.html》和《https://blog.csdn.net/laoyuanpython/category_10581071.html OpenCV-Python初学者疑难问题集》相关文章。

更多图像处理的数学基础知识请参考专栏《人工智能数学基础 https://blog.csdn.net/laoyuanpython/category_10382948.html

写博不易,敬请支持:

如果阅读本文于您有所获,敬请点赞、评论、收藏,谢谢大家的支持!

如果对文章内容存在疑问,可以在博客评论区留言,或关注:老猿Python 微信公号发消息咨询。

关于老猿的付费专栏

  1. 付费专栏《https://blog.csdn.net/laoyuanpython/category_9607725.html 使用PyQt开发图形界面Python应用》专门介绍基于Python的PyQt图形界面开发基础教程,对应文章目录为《 https://blog.csdn.net/LaoYuanPython/article/details/107580932 使用PyQt开发图形界面Python应用专栏目录》;
  2. 付费专栏《https://blog.csdn.net/laoyuanpython/category_10232926.html moviepy音视频开发专栏 )详细介绍moviepy音视频剪辑合成处理的类相关方法及使用相关方法进行相关剪辑合成场景的处理,对应文章目录为《https://blog.csdn.net/LaoYuanPython/article/details/107574583 moviepy音视频开发专栏文章目录》;
  3. 付费专栏《https://blog.csdn.net/laoyuanpython/category_10581071.html OpenCV-Python初学者疑难问题集》为《https://blog.csdn.net/laoyuanpython/category_9979286.html OpenCV-Python图形图像处理 》的伴生专栏,是笔者对OpenCV-Python图形图像处理学习中遇到的一些问题个人感悟的整合,相关资料基本上都是老猿反复研究的成果,有助于OpenCV-Python初学者比较深入地理解OpenCV,对应文章目录为《https://blog.csdn.net/LaoYuanPython/article/details/109713407 OpenCV-Python初学者疑难问题集专栏目录
  4. 付费专栏《https://blog.csdn.net/laoyuanpython/category_10762553.html Python爬虫入门 》站在一个互联网前端开发小白的角度介绍爬虫开发应知应会内容,包括爬虫入门的基础知识,以及爬取CSDN文章信息、博主信息、给文章点赞、评论等实战内容。

前两个专栏都适合有一定Python基础但无相关知识的小白读者学习,第三个专栏请大家结合《https://blog.csdn.net/laoyuanpython/category_9979286.html OpenCV-Python图形图像处理 》的学习使用。

对于缺乏Python基础的同仁,可以通过老猿的免费专栏《https://blog.csdn.net/laoyuanpython/category_9831699.html 专栏:Python基础教程目录)从零开始学习Python。

如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。

跟老猿学Python!

☞ ░ 前往老猿Python博文目录 https://blog.csdn.net/LaoYuanPython

猜你喜欢

转载自blog.csdn.net/LaoYuanPython/article/details/114205205