文章目錄
- 一、透視變換是什么?
- 二、透視變換的核心原理
- 1. 關鍵概念:透視變換矩陣
- 2. 核心條件:4對對應點
- 三、OpenCV實現透視變換的關鍵步驟
- 步驟1:讀取并預處理圖像
- 步驟2:尋找目標物體的4個頂點
- 步驟3:計算透視變換矩陣
- 步驟4:執行透視變換
- 四、實戰:用透視變換矯正發票圖像
- 1. 準備工作
- 2. 完整代碼解析
- 3. 效果對比
- 五、常見問題與解決方案
- 1. 找不到目標物體的 4 個頂點?
- 2. 矯正后的圖像有拉伸或變形?
- 3. 透視變換后圖像邊緣有黑邊?
- 六、透視變換的應用場景
在計算機視覺領域,我們經常會遇到拍攝角度不佳導致圖像變形的問題,比如傾斜的發票、扭曲的文檔等。這時候,圖像透視變換就成了“救星”。它能將傾斜、變形的圖像矯正為正視角,為后續的文字識別、信息提取等操作掃清障礙。今天,我們就從原理入手,結合實戰案例,帶大家全面掌握圖像透視變換。
一、透視變換是什么?
透視變換(Perspective Transformation),也叫投影變換,是一種將圖像從一個二維坐標系映射到另一個三維坐標系的非線性變換。簡單來說,它能模擬人眼視角的變化,把傾斜拍攝的“斜視圖”轉換成正面拍攝的“正視圖”。
比如我們拍攝一張放在桌面上的發票,由于拍攝角度不是正上方,得到的發票圖像可能是梯形或不規則四邊形;而通過透視變換,就能將其矯正為標準的矩形,讓發票上的文字、數字恢復正常的比例和角度。
二、透視變換的核心原理
1. 關鍵概念:透視變換矩陣
透視變換的實現依賴于透視變換矩陣(3×3矩陣) ,通過這個矩陣,可以將圖像中任意一個像素點的坐標(x,y)(x,y)(x,y)映射到新的坐標(x′,y′)(x',y')(x′,y′)。其數學表達式如下:
[x′y′w′]=[a00a01a02a10a11a12a20a21a22][xy1]\begin{bmatrix} x' \\ y' \\ w' \end{bmatrix}= \begin{bmatrix} a_{00} & a_{01} & a_{02} \\ a_{10} & a_{11} & a_{12} \\ a_{20} & a_{21} & a_{22} \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} ?x′y′w′??=?a00?a10?a20??a01?a11?a21??a02?a12?a22????xy1??
其中,w′w'w′是齊次坐標的縮放因子,最終的像素坐標需要通過x′=x′/w′x'=x'/w'x′=x′/w′、y′=y′/w′y'=y'/w'y′=y′/w′計算得到。
這個3×3矩陣包含了9個參數,但由于齊次坐標的特性,實際只需要8個獨立參數就能確定變換關系——而這8個參數,恰好可以通過4對對應點(變換前圖像的4個頂點和變換后圖像的4個頂點)來求解。
2. 核心條件:4對對應點
透視變換的前提是找到變換前圖像的4個頂點(通常是目標物體的邊界角點) 和變換后圖像的4個頂點(通常是標準的矩形頂點) 。這4對對應點必須滿足:
- 變換前的4個點不共線(比如發票的4個角,不能在同一條直線上);
- 變換后的4個點通常構成標準矩形(方便后續處理,如文字識別)。
三、OpenCV實現透視變換的關鍵步驟
在OpenCV中,實現透視變換主要依賴兩個核心函數,整個流程可分為4步:
步驟1:讀取并預處理圖像
首先讀取原始圖像,根據需求進行縮放(降低圖像尺寸可提高后續操作的速度)、灰度化、邊緣檢測等預處理,為后續尋找目標物體的4個頂點做準備。
步驟2:尋找目標物體的4個頂點
通過輪廓檢測找到目標物體(如發票)的輪廓,再通過輪廓近似,提取出目標物體的4個頂點。這一步是透視變換的關鍵——如果頂點找錯,后續的矯正效果會大打折扣。
步驟3:計算透視變換矩陣
使用cv2.getPerspectiveTransform(src, dst)
函數計算變換矩陣。其中:
src
:變換前的4個頂點坐標(需按“左上、右上、右下、左下”的順序排列);dst
:變換后的4個頂點坐標(通常是標準矩形的頂點,如[[0,0], [width,0], [width,height], [0,height]]
)。
步驟4:執行透視變換
使用cv2.warpPerspective(src, M, dsize)
函數完成透視變換。其中:
src
:原始圖像;M
:步驟3計算得到的透視變換矩陣;dsize
:變換后輸出圖像的尺寸(通常由dst
的頂點計算得到)。
四、實戰:用透視變換矯正發票圖像
接下來,我們以“發票矯正”為例,結合代碼詳細講解透視變換的實現過程。
1. 準備工作
- 環境:Python 3.x + OpenCV(版本3.4.18.65,安裝命令:
pip install opencv-python==3.4.18.65
); - 素材:一張傾斜的發票圖像(
fapiao.jpg
,圖像內容包含發票的文字、數字和邊界)。
2. 完整代碼解析
import numpy as np
import cv2# 1. 輔助函數定義
def cv_show(name, img):"""顯示圖像,按任意鍵關閉窗口"""cv2.imshow(name, img)cv2.waitKey(0)cv2.destroyWindow(name)def order_points(pts):"""將4個頂點按“左上、右上、右下、左下”的順序排列"""rect = np.zeros((4, 2), dtype="float32") # 存儲排序后的坐標# 計算每個點的x+y之和:左上點之和最小,右下點之和最大s = pts.sum(axis=1)rect[0] = pts[np.argmin(s)] # 左上rect[2] = pts[np.argmax(s)] # 右下# 計算每個點的y-x之差:右上點之差最小,左下點之差最大diff = np.diff(pts, axis=1)rect[1] = pts[np.argmin(diff)] # 右上rect[3] = pts[np.argmax(diff)] # 左下return rectdef four_point_transform(image, pts):"""執行透視變換,返回矯正后的圖像"""# 獲取排序后的4個頂點rect = order_points(pts)(tl, tr, br, bl) = rect # 左上、右上、右下、左下# 計算矯正后圖像的寬度(取左右兩邊寬度的最大值)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))# 定義矯正后圖像的4個頂點(標準矩形)dst = np.array([[0, 0], # 左上[maxWidth - 1, 0], # 右上[maxWidth - 1, maxHeight - 1], # 右下[0, maxHeight - 1] # 左下], dtype="float32")# 計算透視變換矩陣M = cv2.getPerspectiveTransform(rect, dst)# 執行透視變換warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))return warpeddef resize(image, height=None, inter=cv2.INTER_AREA):"""按高度縮放圖像,保持寬高比"""(h, w) = image.shape[:2]if height is None:return image# 計算縮放比例r = height / float(h)dim = (int(w * r), height)# 執行縮放resized = cv2.resize(image, dim, interpolation=inter)return resized# 2. 讀取并預處理圖像
# 讀取原始發票圖像
image = cv2.imread('fapiao.jpg')
cv_show('原始圖像', image)# 按高度縮放為500像素(降低尺寸,提高處理速度)
ratio = image.shape[0] / 500 # 記錄縮放比例(后續恢復原始尺寸用)
orig = image.copy() # 保存原始圖像
image = resize(orig, height=500)
cv_show('縮放后圖像', image)# 3. 尋找發票的4個頂點
# 灰度化
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 二值化(OTSU自動閾值,突出邊緣)
edged = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('二值化圖像', edged)# 輪廓檢測(尋找圖像中的所有輪廓)
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 繪制所有輪廓(紅色,線寬1)
image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)
cv_show('所有輪廓', image_contours)# 篩選出面積最大的輪廓(發票的輪廓)
screenCnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
# 輪廓近似(將不規則輪廓近似為多邊形,epsilon=0.05*周長,控制近似精度)
peri = cv2.arcLength(screenCnt, True) # 計算輪廓周長(True表示閉合輪廓)
screenCnt = cv2.approxPolyDP(screenCnt, 0.05 * peri, True)# 繪制發票的輪廓(紅色,線寬2)
image_invoice_contour = cv2.drawContours(image.copy(), [screenCnt], -1, (0, 0, 255), 2)
cv_show('發票輪廓', image_invoice_contour)# 4. 執行透視變換
# 恢復原始尺寸的頂點坐標(之前縮放了圖像,需乘以縮放比例)
pts = screenCnt.reshape(4, 2) * ratio
# 執行透視變換
warped = four_point_transform(orig, pts)# 5. 后處理(二值化,方便后續文字識別)
warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
# 二值化(白底黑字)
ref = cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 按寬度縮放為900像素,方便查看
ref = resize(ref, width=900)
cv_show('矯正后的發票', ref)# 6. 保存結果
cv2.imwrite('corrected_invoice.jpg', ref)
print("矯正完成,結果已保存為 corrected_invoice.jpg")
3. 效果對比
- 原始圖像:發票傾斜,文字和數字有透視變形;
- 矯正后圖像:發票變為標準矩形,文字、數字恢復正常比例,可直接用于 OCR 文字識別(如提取發票金額、編號等信息)。
五、常見問題與解決方案
1. 找不到目標物體的 4 個頂點?
- 原因:圖像噪聲過多、邊緣檢測不完整、輪廓篩選錯誤;
- 解決方案:
- 增加圖像預處理步驟,如使用高斯濾波(
cv2.GaussianBlur
)去除噪聲; - 調整二值化閾值(可手動設置閾值,而非依賴 OTSU 自動閾值);
- 優化輪廓近似的精度(調整
epsilon
參數,如0.02*peri
或0.08*peri
)。
- 增加圖像預處理步驟,如使用高斯濾波(
2. 矯正后的圖像有拉伸或變形?
- 原因:4 個頂點的順序錯誤、
dst
(變換后頂點)的尺寸計算不準確; - 解決方案:
- 確保
order_points
函數正確排序頂點(按 “左上、右上、右下、左下”); - 重新計算
maxWidth
和maxHeight
,確保取到正確的寬度和高度最大值。
- 確保
3. 透視變換后圖像邊緣有黑邊?
- 原因:變換矩陣計算時,部分像素超出了輸出圖像的范圍;
- 解決方案:
- 在
cv2.warpPerspective
中添加borderMode=cv2.BORDER_CONSTANT
和borderValue=(255,255,255)
(白色填充黑邊); - 示例:
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight), borderMode=cv2.BORDER_CONSTANT, borderValue=(255,255,255))
。
- 在
六、透視變換的應用場景
除了發票矯正,透視變換在計算機視覺中還有很多實用場景:
- 文檔掃描:將傾斜的紙質文檔矯正為正視圖,提升掃描質量;
- 車牌識別:矯正傾斜拍攝的車牌,提高識別準確率;
- 圖像拼接:在全景圖拼接中,通過透視變換統一多幅圖像的視角;
- AR 增強現實:將虛擬物體映射到真實場景的指定平面(如將虛擬海報貼在真實墻壁上)。