文章目錄
- 前言
- 一、什么是透視變換?
- 二、透視變換的過程
- 三、OpenCV透視變換核心函數
- 四、文檔掃描校正(代碼)
- 1、預處理
- 2、定義輪廓點的排序函數
- 3、定義透視變換函數
- 4、讀取原圖并縮放
- 5、輪廓檢測
- 6、繪制最大輪廓
- 7、對最大輪廓進行透視變換
- 8、旋轉、二值化處理
- 總結
前言
在圖像處理中,透視變換(Perspective Transformation) 是一種強大的技術,能夠校正因視角傾斜導致的圖像變形。無論是掃描文檔的自動矯正、車牌識別,還是增強現實(AR)中的虛擬物體疊加,透視變換都扮演著重要角色。本文將通過OpenCV庫,手把手教你掌握透視變換的核心原理與代碼實現。
一、什么是透視變換?
透視變換是一種將圖像從任意視角投影到新視角的幾何變換。與僅能處理平移、旋轉和縮放的仿射變換不同,透視變換可以處理三維視角變化,徹底改變圖像的投影關系,實現“視角拉正”的效果。
核心特點:
-
可將傾斜拍攝的圖像轉換為正視圖
-
需要提供4組對應點(原圖坐標 + 目標坐標)
-
通過變換矩陣實現非線性映射
二、透視變換的過程
對一張我們即將做透視變換圖像,首先要獲取到圖像中的4個坐標點,用于與目標圖像中的坐標對應,這四個點還是有順序的以坐標軸原點為參照點,距離原點最近的點為0號坐標,最遠的為2號坐標,這兩個點是最容易區分出來的;1號和3號位置可以通過坐標相減作為區分,距離X軸近的坐標的y值小于x值,所以按照x坐標減去y坐標得到的值1號坐標的值大于3號坐標的值。
區分0和2號坐標點:對四個點每個點坐標的x和y的值相加求和,我們發現,針對任意圖片輪廓,如果被四個點描繪,距離原點最近的點求和的值最小,在右下點的值求和的數值最大,可以區分出左上和右下兩個點
區分1和3號坐標點:對四個點每個點坐標的x和y的值相減(x-y),針對任意圖片輪廓,如果被四個點描繪,位于右上角做差的值為一個很大的正數,在左下點的值做差的數值為負數,可以區分出左下和右上兩個點。
水平為x軸
垂直為y軸
三、OpenCV透視變換核心函數
cv2.getPerspectiveTransform()
-
作用:根據4組對應點計算3x3透視變換矩陣。
-
輸入參數:
-
src: 原圖4個點的坐標(格式:np.float32([[x1,y1], [x2,y2], …]))
-
dst: 目標圖像對應4個點的坐標
cv2.warpPerspective()
-
作用:應用變換矩陣執行透視變換。
-
參數:
-
src: 輸入圖像
-
M: 變換矩陣
-
dsize: 輸出圖像尺寸
四、文檔掃描校正(代碼)
目的:將傾斜文檔轉為正視圖
1、預處理
import numpy as np
import cv2
def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(0)
# 調整圖像高寬,保持圖像寬高比不變
def resize(image,width=None,height=None ,inter=cv2.INTER_AREA): # 輸入參數為圖像、可選寬度、可選高度、插值方式默認為cv2.INTER_AREA,即面積插值dim = None # 存儲計算后的目標尺寸w、h(h,w) = image.shape[:2] # 返回輸入圖像高寬if width is None and height is None: # 判斷是否指定了寬和高大小,如果沒有指定則返回原圖return imageif width is None: # 判斷如果沒有指定寬度大小,則表示指定了高度大小,那么運行內部代碼r = height/float(h) # 指定高度與原圖高度的比值dim = (int(w*r),height) # 寬度乘以比值得到新的寬度,此處得到新的寬高else: # 此處表示為width不是None,即指定了寬度,與上述方法一致,計算比值r = width/float(w)dim = (width,int(h*r))resized = cv2.resize(image,dim,interpolation=inter) # 指定圖像大小為上述的dim,inter默認為cV2.INTER_AREA,即面積插值,適用于縮放圖像。return resized
2、定義輪廓點的排序函數
def order_points(pts): # 對輸入的四個點按照左上、右上、右下、左下進行排序rect = np.zeros((4,2),dtype='float32') # 創建一個4*2的數組,用來存儲排序之后的坐標位置# 按順序找到對應坐標0123分別是左上、右上、右下、左下s = pts.sum(axis=1) # 對pts矩陣的每個點的x y相加rect[0] = pts[np.argmin(s)] # np.argmin(s)表示數組s中最小值的索引,表示左上的點的坐標rect[2] = pts[np.argmax(s)] # 返回最大值索引,即右下角的點坐標diff = np.diff(pts,axis=1) # 對pts矩陣的每一行的點求差值rect[1] = pts[np.argmin(diff)] # 差值最小的點為右上角點rect[3] = pts[np.argmax(diff)] # 差值最大表示左下角點return rect # 返回排序好的四個點的坐標
3、定義透視變換函數
# 將透視扭曲的矩形變換成一個規則的矩陣
def four_point_transform(image,pts):# 獲取輸入坐標點rect = order_points(pts) # 為上述排序的四個點(tl,tr,br,bl) = rect # 分別返回給四個值,分別表示為左上、右上、右下、左下# 計算輸入的w和h值widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1]-bl[1]) ** 2)) # 計算四邊形底邊的寬度widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1]-tl[1]) ** 2)) # 計算頂邊的寬度maxWidth = max(int(widthA), int(widthB)) # 返回最大寬度heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2)) # 計算左上角到右下角的對角線長度heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2)) # 計算右上角到左下角的高的長度maxHeight = max(int(heightA),int(heightB)) # 返回最長的高度# 變換后對應坐標位置dst = np.array([[0,0], # 定義四個點,表示變換后的矩陣的角點[maxWidth-1,0],[maxWidth-1,maxHeight-1],[0,maxHeight-1]],dtype='float32')M = cv2.getPerspectiveTransform(rect,dst) # 根據原始點和變換后的點計算透視變換矩陣Mwarped = cv2.warpPerspective(image,M,(maxWidth,maxHeight)) # 對原始圖像,針推變換矩陣和輸出圖像大小進行透視變換,返回變換后的圖片# 返回變換后的結果return warped
4、讀取原圖并縮放
# # 讀取輸入
image = cv2.imread('fapiao.jpg') # 讀取原圖
cv_show('image',image) # 展示原圖# 圖片過大,進行縮小處理
ratio = image.shape[0] / 500.0 # 計算縮小比率,[0]表示圖像的高
orig = image.copy() # 對原圖復制生成副本
image = resize(orig, height=500) # 更改圖像尺寸,輸入高度自動生成寬度
cv_show('1',image) # 展示縮放后的圖片
5、輪廓檢測
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) # 灰度圖edged = cv2.threshold(gray,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] # 進行二值化,cv2.THRESH_OTSU自動尋找最優全局閾值,255表示高于最優閾值時將其更改為255
cnts = cv2.findContours(edged.copy(),cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[1] # 輪廓檢測
# cv2.RETR_LIST表示檢索所有輪廓,但是不建立層次關系
# cv2.CHAIN_APPROX_SIMPLE 表示只保存輪廓拐點的信息
# 總體返回處理的圖像、輪廓列表、層次結構,這里返回索引為1,表示返回輪廓列表image_contours = cv2.drawContours(image.copy(),cnts,-1,(0,0,255),1) # 繪制所有輪廓
# 在原始圖像的副本上繪制了輪廓
# 繪制輪廓的位置為上述獲取的拐點信息,繪制線條顏色為紅色BRG(0,0,255),線條粗細為1個像素cv_show('image_contours',image_contours) # 展示繪制好的圖片
6、繪制最大輪廓
screenCnt = sorted(cnts,key = cv2.contourArea,reverse=True)[0] # 對上述獲取的輪廓列表,排序依據是輪廓面積,reverse=True表示降序,[0]表示獲取面積最大的輪廓
peri = cv2.arcLength(screenCnt,True) # 計算最大輪廓的周長
screenCnt = cv2.approxPolyDP(screenCnt,0.02*peri,True) # 輪廓近似,近似為一個多邊形,表示新的輪廓與原來的輪廓最大距離不超過原始輪廓寬度的0.02倍,True表示輪廓為閉合的
image_contour = cv2.drawContours(image.copy(),[screenCnt],-1,(0,255,0),2) # 繪制輪廓,將上述找到的輪廓繪制到原圖的副本上
cv2.imshow('image_contour',image_contour)
cv2.waitKey(0)
7、對最大輪廓進行透視變換
warped = four_point_transform(orig,screenCnt.reshape(4,2)*ratio) # 輸入參數原圖,將最大輪廓圖形狀改變為4*2的格式,即四個點,然后乘以上述定義的比率來縮放輪廓
cv2.imwrite('invoice_new.jpg',warped) # 將經過透視變換處理的圖片存入本地
cv2.namedWindow('xx',cv2.WINDOW_NORMAL) # 設置一個窗口,名稱為xx,這個窗口大小用戶可通過拖動隨意調節大小
cv2.imshow('xx',warped) # 展示經過透視變換處理的圖片
cv2.waitKey(0)
8、旋轉、二值化處理
# 二值處理
warped = cv2.cvtColor(warped,cv2.COLOR_BGR2GRAY) # 導入新的圖片的灰度圖
ref = cv2.threshold(warped,0,255,cv2.THRESH_BINARY|cv2.THRESH_OTSU)[1] # 對灰度圖進行二值化處理kernel = np.ones((2,2),np.uint8) # 設置一個單位矩陣,大小為2*2,表示設置核kernel的大小
ref_new = cv2.morphologyEx(ref,cv2.MORPH_CLOSE,kernel) # 閉運算,先膨脹再腐蝕
ref_new = resize(ref_new.copy(),width=500) # 對閉運算處理完的圖像重置大小
cv_show('yy',ref_new)
rotated_image = cv2.rotate(ref_new,cv2.ROTATE_90_COUNTERCLOCKWISE) # 對圖像逆時針旋轉90度
cv2.imshow('result',rotated_image)
cv2.waitKey(0)
總結
通過OpenCV的透視變換,我們能夠輕松解決因拍攝角度導致的圖像形變問題。無論是手動標定點還是結合自動檢測算法,這一技術都為復雜場景下的圖像處理提供了基礎支持。