在日常辦公自動化(OA)或財務數字化場景中,拍攝的票據常因角度問題出現傾斜、變形,不僅影響視覺呈現,更會導致 OCR 文字識別準確率大幅下降。本文將從技術原理到代碼實現,手把手教你用 Python 打造票據圖像自動矯正工具,解決實際場景中的圖像預處理難題。?
一、核心依賴與整體功能?
實現票據矯正需依托 3 個關鍵庫,各模塊分工明確:?
- OpenCV(cv2):承擔圖像讀取、預處理、輪廓檢測與透視變換的核心工作,是實現矯正功能的 “主力工具”。?
- NumPy:負責圖像坐標計算、矩陣運算,為透視變換提供數值支持,解決復雜的坐標映射問題。?
- PIL(PIL.ImageChops):代碼中雖暫未直接調用,但預留用于后續圖像融合、對比度優化等擴展功能,提升工具靈活性。?
整體技術流程可簡化為:圖像讀取→預處理(縮放 / 灰度化 / 二值化)→輪廓檢測與篩選→透視變換矯正→結果輸出,最終將傾斜、變形的票據轉化為平整的標準矩形圖像。?
二、關鍵函數拆解?
代碼中 4 個核心函數構成了矯正工具的 “骨架”,每個函數對應一個關鍵技術環節,我們逐一解析其實現邏輯與作用。?
1. 圖像顯示函數:cv_show?
用于實時查看圖像處理的中間結果,方便調試過程中定位問題(如輪廓是否檢測準確、二值化效果是否達標)。?
def cv_show(name, img):?cv2.imshow(f'{name}', img) # 創建指定名稱的窗口,顯示圖像?cv2.waitKey(0) # 無限等待按鍵輸入,按任意鍵關閉窗口?
- 參數說明:name為窗口名稱(如 “原始票據”“輪廓檢測結果”),便于區分不同處理階段;img為待顯示的圖像數據。?
- 注意點:若省略cv2.waitKey(0),窗口會因程序執行過快而一閃而過,無法觀察圖像細節。?
2. 圖像縮放函數:resize?
解決原始圖像尺寸過大導致的計算效率問題,同時保證圖像寬高比不變,避免拉伸變形影響后續輪廓檢測。?
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):dim = None # 存儲縮放后的圖像尺寸(寬,高)(h, w) = image.shape[:2] # 獲取原始圖像的高度(h)和寬度(w)# 若未指定縮放尺寸,直接返回原始圖像if width is None and height is None:return image# 僅指定高度時,按高度比例計算寬度(保持寬高比)if width is None:r = height / float(h) # 縮放比例 = 目標高度 / 原始高度dim = (int(w * r), height) # 計算縮放后的寬度(需轉為整數,像素為整數單位)# 僅指定寬度時,按寬度比例計算高度else:r = width / float(w) # 縮放比例 = 目標寬度 / 原始寬度dim = (width, int(h * r)) # 計算縮放后的高度# 執行縮放操作,INTER_AREA插值法適合縮小圖像,能保留更多細節resized = cv2.resize(image, dim, interpolation=inter)return resized
- 核心優勢:通過 “比例計算 + 固定插值法”,既提升了后續輪廓檢測的速度(如將圖像高度縮放到 500 像素),又避免了圖像拉伸導致的輪廓變形。?
3. 頂點排序函數:order_points?
透視變換的前提是明確票據四個頂點的正確順序(左上→右上→右下→左下),該函數通過坐標特征實現自動排序,避免人工標注的繁瑣。?
def order_points(pts):# 初始化空數組,存儲排序后的4個頂點(形狀為(4,2),每個元素為(x,y)坐標)rect = np.zeros((4, 2), dtype="float32")# 第一步:按“x+y”的和排序——和最小的是左上角(tl),和最大的是右下角(br)s = pts.sum(axis=1) # 對每個頂點的x、y坐標求和(axis=1表示按行計算)rect[0] = pts[np.argmin(s)] # argmin(s)獲取和最小的索引,對應左上角rect[2] = pts[np.argmax(s)] # argmax(s)獲取和最大的索引,對應右下角# 第二步:按“y-x”的差排序——差最小的是右上角(tr),差最大的是左下角(bl)diff = np.diff(pts, axis=1) # 按行計算y-x(后一個元素減前一個元素)rect[1] = pts[np.argmin(diff)] # 差最小的索引對應右上角rect[3] = pts[np.argmax(diff)] # 差最大的索引對應左下角return rect
- 技術原理:利用平面直角坐標系中頂點的 “幾何特征”(如左上角 x、y 均較小,右下角 x、y 均較大),無需人工干預即可實現自動排序,為透視變換提供準確的輸入。?
4. 透視變換函數:four_point_transform?
票據矯正的 “核心引擎”,通過透視變換矩陣將傾斜的四邊形(票據)映射為標準矩形,實現 “從傾斜到平整” 的關鍵一步。?
def four_point_transform(image, pts):# 第一步:獲取排序后的四個頂點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)) # 目標高度取兩者最大值# 第四步:定義目標矩形的四個頂點(標準矩形,從(0,0)開始)dst = np.array([[0, 0], # 目標左上頂點[maxWidth - 1, 0], # 目標右上頂點(減1是因為像素坐標從0開始)[maxWidth - 1, maxHeight - 1], # 目標右下頂點[0, maxHeight - 1] # 目標左下頂點], dtype="float32")# 第五步:計算透視變換矩陣M(描述原始頂點到目標頂點的映射關系)M = cv2.getPerspectiveTransform(rect, dst)# 第六步:應用透視變換,得到矯正后的圖像warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))return warped
- 關鍵價值:透視變換突破了 “平行投影” 的限制,能處理任意角度的傾斜(如斜拍、側拍的票據),是實現 “全場景矯正” 的核心技術。?
三、主流程執行步驟?
完成核心函數定義后,通過主流程代碼將各環節串聯,實現從 “讀取圖像” 到 “保存結果” 的完整閉環,步驟如下:?
1. 讀取原始圖像并預覽?
# 讀取票據圖像(cv2.IMREAD_COLOR表示讀取彩色圖像,通道順序為BGR)
image = cv2.imread('fapiao.jpg', cv2.IMREAD_COLOR)
cv_show('原始票據', image) # 預覽原始圖像,確認圖像讀取正常
- 注意事項:若圖像路徑錯誤(如文件不存在、路徑含中文),image會返回None,后續代碼會報錯,需確保路徑正確(建議使用絕對路徑,如C:/images/fapiao.jpg)。?
2. 圖像縮放(提升計算效率)?
# 計算縮放比例:原始圖像高度 / 目標高度(此處目標高度設為500,可根據需求調整)
ratio = image.shape[0] / 500.0
orig = image.copy() # 保存原始圖像副本(后續矯正需基于原始尺寸)
image = resize(orig, height=500) # 按目標高度縮放圖像
cv_show('縮放后票據', image) # 預覽縮放后的圖像
- 為什么需要縮放?若原始圖像尺寸為 2000×3000 像素,輪廓檢測需處理大量像素,耗時較長;縮放到高度 500 像素后,計算量大幅降低,且不影響輪廓檢測的準確性。?
- 為什么保存orig?縮放后的圖像僅用于 “輪廓檢測”,最終矯正需基于原始圖像尺寸(避免縮放導致的細節丟失),因此需保存原始圖像副本。?
3. 圖像預處理(突出票據輪廓)?
print('開始預處理:灰度化→二值化...')?# 1. 灰度化:將彩色圖像轉為單通道灰度圖(減少計算量,消除色彩干擾)?gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)?# 2. 二值化:將灰度圖轉為黑白二值圖(突出票據輪廓,抑制背景噪聲)?# THRESH_BINARY:超過閾值設為255(白色),低于設為0(黑色);THRESH_OTSU:自動計算最佳閾值?edge = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]?
- 預處理的意義:彩色圖像含 3 個通道(BGR),噪聲較多;灰度化后變為單通道,二值化進一步 “強化輪廓、弱化背景”,為后續輪廓檢測掃清障礙。?
4. 輪廓檢測與可視化?
# 檢測圖像中所有輪廓(RETR_LIST:獲取所有輪廓;CHAIN_APPROX_SIMPLE:簡化輪廓,減少點數)
# [-2]確保兼容不同OpenCV版本(部分版本返回值為(圖像, 輪廓, 層級),部分為(輪廓, 層級))
cnts = cv2.findContours(edge.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 繪制所有輪廓(在縮放圖像副本上繪制,顏色為紅色(0,0,255),線條寬度1)
image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)
cv_show('所有輪廓', image_contours) # 預覽輪廓檢測結果
- 輪廓的定義:圖像中連續的、灰度值相同的像素組成的曲線,此處主要指票據的邊界輪廓(如票據的四條邊)。?
- 可視化的作用:通過繪制輪廓,可直觀確認是否檢測到票據邊界,若未檢測到,需調整預處理參數(如增加模糊步驟)。?
5. 篩選票據輪廓(定位目標區域)?
print('篩選票據輪廓...')?# 按輪廓面積降序排序,取面積最大的輪廓(票據通常是圖像中面積最大的物體)?screencnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]?print(f'最大輪廓原始點數:{screencnt.shape}') # 打印原始輪廓的點數(通常為數百個)??# 輪廓近似:將復雜輪廓簡化為多邊形(減少點數),參數0.02*perimeter為近似精度?perimeter = cv2.arcLength(screencnt, True) # 計算輪廓周長(True表示輪廓閉合)?screencnt = cv2.approxPolyDP(screencnt, 0.02 * perimeter, True)?print(f'近似后輪廓點數:{screencnt.shape}') # 若檢測正確,點數應為4(對應票據的四個角)??# 繪制篩選后的票據輪廓(紅色,線條寬度2,更醒目)?image_final_contour = cv2.drawContours(image.copy(), [screencnt], -1, (0, 0, 255), 2)?cv_show('票據輪廓', image_final_contour) # 預覽篩選后的輪廓?
- 核心邏輯:票據在圖像中通常是 “面積最大的閉合區域”,因此按面積排序取第一;輪廓近似通過 “減少點數”,將不規則的輪廓簡化為四邊形(票據的形狀),若近似后點數為 4,說明成功定位票據的四個頂點。?
6. 透視變換矯正與結果保存?
# 執行透視變換:screencnt是縮放圖像的頂點,需乘以ratio還原為原始圖像的頂點?warped = four_point_transform(orig, screencnt.reshape(4, 2) * ratio)??# 保存矯正后的圖像(路徑可自定義)?cv2.imwrite('corrected_bill.jpg', warped)?# 創建可縮放窗口(避免圖像過大/過小無法查看)?cv2.namedWindow('矯正后票據', cv2.WINDOW_NORMAL)?cv2.imshow('矯正后票據', warped) # 預覽矯正結果?cv2.waitKey(0) # 等待按鍵輸入??# 關閉所有OpenCV窗口,釋放內存資源?cv2.destroyAllWindows()?
- 為什么乘以ratio?screencnt是基于縮放圖像(高度 500 像素)的頂點坐標,而orig是原始尺寸圖像,乘以ratio可將頂點坐標還原為原始尺寸,確保矯正后的圖像與原始圖像比例一致。?
- 結果驗證:打開保存的corrected_bill.jpg,若票據平整、無傾斜,說明矯正成功;若仍有變形,需檢查頂點排序或透視變換參數。
四、功能擴展與實際應用?
該工具不僅適用于票據矯正,還可擴展到多個實際場景,滿足不同需求:?
1. 擴展場景?
- 證件矯正:身份證、銀行卡、護照等證件的傾斜矯正,解決拍攝時的角度問題,提升證件識別準確率。例如在銀行 APP 的 “證件上傳” 功能中,用戶拍攝的身份證常因手持角度導致傾斜,通過該工具矯正后,可減少 OCR 識別時的文字偏移誤差,提高信息提取正確率。?
- 文檔掃描:書籍、合同、報表等紙質文檔的掃描后矯正,替代傳統掃描儀的 “自動平整” 功能,降低硬件成本。例如企業員工用手機拍攝紙質合同后,通過工具矯正傾斜、去除背景陰影,可生成與掃描儀效果接近的電子文檔,方便后續存檔或編輯。?
- 工業檢測:零件、產品的圖像定位與矯正,為后續的尺寸測量、缺陷檢測提供準確的圖像輸入。例如在汽車零部件檢測中,攝像頭拍攝的零件圖像可能因擺放角度傾斜,導致尺寸測量偏差,通過該工具矯正后,可確保測量基準的準確性,提升檢測精度。?
2. 功能升級建議?
若需將工具從 “基礎版” 升級為 “實用版”,可新增以下功能:?
- 自動背景去除:在矯正后添加背景去除邏輯(如通過顏色閾值分割、邊緣檢測 + 掩碼操作),將票據從復雜背景中分離,生成 “白底黑字” 的清晰圖像,進一步提升 OCR 識別效果。?
- 批量處理功能:通過os庫遍歷指定文件夾下的所有票據圖像(如jpg“png” 格式),自動完成 “讀取→矯正→保存” 流程,適用于企業批量處理票據的場景,減少人工操作。?
- 傾斜角度判斷:通過cv2.minAreaRect計算票據輪廓的傾斜角度,若角度絕對值小于 3°(可自定義閾值),則跳過矯正步驟,避免不必要的計算,提升處理效率。?
五、完整代碼匯總(可直接運行)?
為方便大家快速使用,以下是完整的可運行代碼,包含所有功能模塊及注釋:
import numpy as np
import cv2
from PIL.ImageChops import screen # 預留擴展功能使用# 1. 圖像顯示函數:用于調試時查看中間結果
def cv_show(name, img):cv2.imshow(f'{name}', img)cv2.waitKey(0) # 按任意鍵關閉窗口# 2. 圖像縮放函數:保持寬高比,提升計算效率
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):dim = None(h, w) = image.shape[:2]# 未指定尺寸時返回原圖if width is None and height is None:return image# 僅指定高度,按比例計算寬度if width is None:r = height / float(h)dim = (int(w * r), height)# 僅指定寬度,按比例計算高度else:r = width / float(w)dim = (width, int(h * r))# 執行縮放resized = cv2.resize(image, dim, interpolation=inter)return resized# 3. 頂點排序函數:確保透視變換輸入頂點順序正確(左上→右上→右下→左下)
def order_points(pts):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 rect# 4. 透視變換函數:核心矯正邏輯,將傾斜四邊形轉為標準矩形
def four_point_transform(image, pts):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))# 定義目標頂點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 warped# 主流程:從圖像讀取到結果保存
if __name__ == "__main__":# 1. 讀取原始圖像image_path = "fapiao.jpg" # 替換為你的票據圖像路徑image = cv2.imread(image_path, cv2.IMREAD_COLOR)if image is None:raise ValueError(f"無法讀取圖像,請檢查路徑:{image_path}")cv_show("1. 原始票據", image)# 2. 圖像縮放(目標高度500,計算縮放比例)ratio = image.shape[0] / 500.0orig = image.copy() # 保存原始圖像image = resize(orig, height=500)cv_show("2. 縮放后票據", image)# 3. 預處理:灰度化→二值化(突出輪廓)print("正在進行圖像預處理...")gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 高斯模糊(可選,用于減少噪聲,根據圖像情況決定是否添加)# gray = cv2.GaussianBlur(gray, (5, 5), 0)edge = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]# 4. 輪廓檢測與可視化cnts = cv2.findContours(edge.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[-2]image_contours = cv2.drawContours(image.copy(), cnts, -1, (0, 0, 255), 1)cv_show("3. 所有輪廓", image_contours)# 5. 篩選票據輪廓(面積最大+近似為四邊形)print("正在篩選票據輪廓...")# 按面積降序排序,取最大輪廓screencnt = sorted(cnts, key=cv2.contourArea, reverse=True)[0]print(f"最大輪廓原始點數:{screencnt.shape}")# 輪廓近似(簡化為多邊形)perimeter = cv2.arcLength(screencnt, True)screencnt = cv2.approxPolyDP(screencnt, 0.02 * perimeter, True)print(f"近似后輪廓點數:{screencnt.shape}")# 檢查是否為四邊形(4個頂點)if len(screencnt) != 4:raise ValueError("未檢測到票據的4個頂點,請調整預處理參數或檢查圖像質量")# 繪制篩選后的票據輪廓image_final_contour = cv2.drawContours(image.copy(), [screencnt], -1, (0, 0, 255), 2)cv_show("4. 票據輪廓(4頂點)", image_final_contour)# 6. 透視變換矯正與結果保存print("正在進行票據矯正...")warped = four_point_transform(orig, screencnt.reshape(4, 2) * ratio)# 保存矯正后的圖像save_path = "corrected_bill.jpg"cv2.imwrite(save_path, warped)print(f"矯正完成,圖像已保存至:{save_path}")# 顯示矯正結果cv2.namedWindow("5. 矯正后票據", cv2.WINDOW_NORMAL)cv2.imshow("5. 矯正后票據", warped)cv2.waitKey(0)# 關閉所有窗口,釋放資源cv2.destroyAllWindows()