目錄
前言
一、核心技術原理:透視變換與輪廓檢測
1. 透視變換:讓傾斜發票 “正過來”
(1)什么是透視變換?
(2)透視變換的 5 個關鍵步驟
2. 輪廓檢測:精準定位發票區域
(1)什么是輪廓檢測?
(2)輪廓檢測的 5 個執行步驟
二、項目實戰:OpenCV 發票識別全流程代碼
1. 環境準備
2. 工具函數定義
(1)圖像展示函數
(2)圖像自動縮放函數
(3)輪廓點排序函數
(4)透視變換函數
3. 發票識別全流程執行
(1)步驟 1:讀取原圖并縮放
(2)步驟 2:圖像預處理與輪廓檢測
(3)步驟 3:篩選最大輪廓(定位發票區域)
(4)步驟 4:透視變換校正發票
(5)步驟 5:二值化與形態學優化(為 OCR 準備)
前言
在辦公自動化與計算機視覺結合的場景中,發票識別是典型的落地需求 —— 實際拍攝的發票常因角度傾斜、背景雜亂導致文字提取困難,而基于 OpenCV 的透視變換與輪廓檢測技術,能快速將傾斜發票校正為正視角、高對比度的規整圖像,為后續 OCR 文字識別奠定基礎。本文將從核心技術原理出發,結合完整代碼與實戰效果,拆解發票識別的全流程,適合 OpenCV 入門者與計算機視覺愛好者學習。
一、核心技術原理:透視變換與輪廓檢測
在開始項目前,需先理解兩個關鍵技術的核心邏輯 —— 它們是發票校正與目標提取的基礎。
1. 透視變換:讓傾斜發票 “正過來”
(1)什么是透視變換?
透視變換是一種將三維空間中傾斜的平面映射到二維平面的幾何變換,它能模擬人眼的透視效果,解決 “拍攝角度傾斜導致發票邊緣不規整” 的問題。例如,從斜上方拍攝的發票,其四個角會呈現梯形或不規則四邊形,通過透視變換可將其校正為標準矩形,還原發票的真實比例。
透視變換的數學本質是通過4 組對應點(源圖像 4 個角點與目標圖像 4 個角點)計算變換矩陣,再用該矩陣對源圖像進行像素映射。核心特點是:
- 平行線可能在變換后相交(符合真實透視規律);
- 能保留圖像的細節信息,僅改變視角。
(2)透視變換的 5 個關鍵步驟
- 確定源圖像與目標圖像:源圖像是拍攝的傾斜發票,目標圖像是期望得到的 “正矩形發票”;
- 選取 4 組對應關鍵點:需在源圖像中找到發票的 4 個頂點(左上、右上、右下、左下),并定義目標圖像中對應的 4 個頂點(如
(0,0)
、(width,0)
、(width,height)
、(0,height)
); - 計算變換矩陣:使用 OpenCV 的
cv2.getPerspectiveTransform()
,通過 4 組對應點生成 3x3 的透視變換矩陣M
; - 執行透視變換:用
cv2.warpPerspective()
加載變換矩陣M
,將源圖像映射為目標圖像; - 插值處理:由于變換后像素可能映射到非整數坐標,需通過插值(如
INTER_LINEAR
)補充像素值,保證圖像清晰度。
2. 輪廓檢測:精準定位發票區域
(1)什么是輪廓檢測?
輪廓是圖像中連續的、具有相同灰度或顏色的像素邊緣,輪廓檢測本質是從圖像中提取目標物體的邊界,在發票識別中用于 “從復雜背景中精準框選出發票區域”。
與邊緣檢測(如 Canny)不同,輪廓檢測不僅能找到離散的邊緣點,還能將邊緣點連接成連續的閉合曲線,便于后續計算目標的面積、周長、頂點等特征。常用的輪廓檢測算法依賴圖像的二值化結果(黑白對比),因此預處理步驟尤為重要。
(2)輪廓檢測的 5 個執行步驟
- 圖像預處理:將彩色圖像轉為灰度圖(減少計算量),再通過高斯濾波(
cv2.GaussianBlur
)去除噪聲,避免噪聲干擾輪廓提取; - 邊緣檢測:用二值化(如
cv2.threshold
+THRESH_OTSU
自動閾值)將灰度圖轉為黑白圖像,突出發票與背景的對比; - 輪廓提取:通過
cv2.findContours()
提取圖像中所有輪廓,指定輪廓檢索模式(如RETR_LIST
提取所有輪廓)與逼近方法(如CHAIN_APPROX_SIMPLE
簡化輪廓點); - 輪廓篩選:根據輪廓的面積(
cv2.contourArea
)、周長(cv2.arcLength
)等特征篩選出 “發票輪廓”—— 通常是面積最大的閉合輪廓; - 輪廓繪制與驗證:用
cv2.drawContours()
將篩選后的輪廓繪制在原圖上,驗證是否準確框選出發票區域。
二、項目實戰:OpenCV 發票識別全流程代碼
1. 環境準備
- 編程語言:Python 3.7+
- 依賴庫:OpenCV(
pip install opencv-python
)、NumPy(pip install numpy
) - 測試數據:拍攝的傾斜發票圖像(命名為
fapiao.jpg
,建議分辨率不低于 1000x800)
2. 工具函數定義
先封裝 4 個核心工具函數,提高代碼復用性與可讀性。
(1)圖像展示函數
用于快速展示處理過程中的圖像,避免重復編寫cv2.imshow
與cv2.waitKey
:
import cv2
import numpy as npdef cv_show(name, img):"""展示圖像的通用函數:param name: 窗口名稱:param img: 輸入圖像(numpy數組)"""cv2.imshow(name, img)cv2.waitKey(0) # 等待按鍵關閉窗口cv2.destroyWindow(name) # 關閉指定窗口
(2)圖像自動縮放函數
解決 “原圖過大導致窗口無法完整顯示” 的問題,保持圖像寬高比不變:
def resize_image(image, width=None, height=None, inter=cv2.INTER_AREA):"""保持寬高比的圖像縮放函數:param image: 輸入圖像:param width: 目標寬度(None則按高度計算):param height: 目標高度(None則按寬度計算):param inter: 插值方式(默認INTER_AREA,適合縮放):return: 縮放后的圖像"""dim = None # 存儲目標尺寸(寬,高)h, w = image.shape[:2] # 獲取原圖高、寬# 若未指定寬和高,直接返回原圖if width is None and height is None:return image# 僅指定高度:按高度比例計算寬度if width is None:ratio = height / float(h)dim = (int(w * ratio), height)# 僅指定寬度:按寬度比例計算高度else:ratio = width / float(w)dim = (width, int(h * ratio))# 執行縮放resized = cv2.resize(image, dim, interpolation=inter)return resized
(3)輪廓點排序函數
透視變換需要 4 個 “有序的頂點”(左上→右上→右下→左下),該函數通過計算點的坐標和與差值實現排序:
def order_points(pts):"""對輪廓的4個頂點按“左上→右上→右下→左下”排序:param pts: 輸入的4個頂點(shape為(4,2)的numpy數組):return: 排序后的頂點數組"""rect = np.zeros((4, 2), dtype="float32") # 初始化排序后的數組# 步驟1:按“x+y”的和排序(左上和最小,右下和最大)s = pts.sum(axis=1)rect[0] = pts[np.argmin(s)] # 左上點(x+y最小)rect[2] = pts[np.argmax(s)] # 右下點(x+y最大)# 步驟2:按“y-x”的差值排序(右上差值最小,左下差值最大)diff = np.diff(pts, axis=1)rect[1] = pts[np.argmin(diff)] # 右上點(y-x最小)rect[3] = pts[np.argmax(diff)] # 左下點(y-x最大)return rect
(4)透視變換函數
輸入圖像與 4 個頂點,返回校正后的規整圖像:
def four_point_transform(image, pts):"""基于4個頂點的透視變換函數:param image: 源圖像(原始傾斜發票):param pts: 源圖像中發票的4個頂點:return: 校正后的圖像"""# 1. 排序頂點rect = order_points(pts)tl, tr, br, bl = rect # 解包為左上、右上、右下、左下# 2. 計算目標圖像的寬和高(取最大值避免圖像裁剪)# 計算底部寬度(右下-左下)和頂部寬度(右上-左上)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)) # 目標高度# 3. 定義目標圖像的4個頂點(標準矩形)dst = np.array([[0, 0],[maxWidth - 1, 0],[maxWidth - 1, maxHeight - 1],[0, maxHeight - 1]], dtype="float32")# 4. 計算透視變換矩陣并執行變換M = cv2.getPerspectiveTransform(rect, dst) # 生成3x3變換矩陣warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 執行變換return warped
3. 發票識別全流程執行
(1)步驟 1:讀取原圖并縮放
先加載原始發票圖像,按固定高度縮放(避免原圖過大),同時保存縮放比例(后續用于還原輪廓坐標):
# 讀取原始發票圖像
orig = cv2.imread("fapiao.jpg")
if orig is None:raise ValueError("未找到圖像文件,請檢查路徑是否正確!")# 縮放圖像(固定高度為500,保持寬高比)
ratio = orig.shape[0] / 500.0 # 縮放比例(原圖高 / 縮放后高)
image = resize_image(orig, height=500)# 展示原圖與縮放圖
cv_show("原始發票", orig)
cv_show("縮放后發票", image)
以下是縮放后的發票圖:
(2)步驟 2:圖像預處理與輪廓檢測
通過灰度化、二值化突出發票邊緣,再提取所有輪廓:
# 1. 灰度化(減少通道數,降低計算量)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 2. 二值化(自動閾值,突出發票與背景對比)
# THRESH_OTSU:自動計算最優閾值,適合明暗對比明顯的圖像
edged = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]# 3. 提取所有輪廓
# RETR_LIST:提取所有輪廓,不建立層次關系;CHAIN_APPROX_SIMPLE:簡化輪廓點
cnts, _ = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)# 4. 繪制所有輪廓(紅色,線寬1),驗證提取效果
image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)
cv_show("所有輪廓", image_contours)
所有輪廓如下:
(3)步驟 3:篩選最大輪廓(定位發票區域)
發票通常是圖像中面積最大的閉合區域,通過輪廓面積排序篩選出目標輪廓,并進行多邊形近似(減少輪廓點數量):
# 1. 按輪廓面積降序排序,取面積最大的輪廓(即發票輪廓)
screen_cnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]# 2. 輪廓近似(將不規則輪廓近似為多邊形)
# arcLength:計算輪廓周長(True表示閉合輪廓)
peri = cv2.arcLength(screen_cnt, True)
# approxPolyDP:輪廓近似,epsilon=0.02*peri(控制近似精度)
screen_cnt = cv2.approxPolyDP(screen_cnt, 0.02 * peri, True)# 3. 驗證輪廓是否為4個頂點(發票是四邊形,需4個頂點)
if len(screen_cnt) != 4:raise ValueError("未檢測到發票的4個頂點,請調整拍攝角度或圖像質量!")# 4. 繪制最大輪廓(綠色,線寬2)
image_max_contour = cv2.drawContours(image.copy(), [screen_cnt], -1, (0, 255, 0), 2)
cv_show("發票最大輪廓", image_max_contour)
最大輪廓如下:
(4)步驟 4:透視變換校正發票
用篩選出的 4 個頂點執行透視變換,將傾斜發票校正為正矩形:
# 1. 還原輪廓坐標(縮放后的坐標 * 縮放比例 = 原圖坐標)
screen_cnt_org = screen_cnt.reshape(4, 2) * ratio# 2. 執行透視變換(輸入原圖,避免縮放導致的細節丟失)
warped = four_point_transform(orig, screen_cnt_org)# 3. 保存并展示校正后的發票
cv2.imwrite("fapiao_corrected.jpg", warped)
cv_show("校正后發票", warped)
校正后的發票如下:
(5)步驟 5:二值化與形態學優化(為 OCR 準備)
校正后的圖像需進一步處理為 “白底黑字”,減少噪聲干擾,便于后續 OCR 文字識別:
# 1. 灰度化校正后的圖像
warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)# 2. 二值化(轉為黑白圖像,THRESH_BINARY_INV表示黑底白字→白底黑字)
ref = cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]# 3. 形態學閉運算(先膨脹再腐蝕,填充文字內部的小孔)
kernel = np.ones((2, 2), np.uint8) # 2x2結構元素
ref_processed = cv2.morphologyEx(ref, cv2.MORPH_CLOSE, kernel)# 4. 縮放并展示最終結果(寬度固定為800,便于查看)
final_result = resize_image(ref_processed, width=800)
cv_show("最終白底黑字效果", final_result)# 保存最終結果
cv2.imwrite("fapiao_final.jpg", final_result)
cv2.destroyAllWindows() # 關閉所有窗口
運行結果如下: