文章目錄
- 引言
- 一、系統概述
- 二、核心代碼解析
- 1. 導入必要庫
- 2. 輔助函數定義
- 3. 坐標點排序函數
- 4. 透視變換函數
- 5. 主程序流程
- 三、完整代碼
- 四、結語
引言
在日常工作和學習中,我們經常需要將紙質文檔數字化。手動拍攝文檔照片常常會出現角度傾斜、透視變形等問題,影響后續使用。本文將介紹如何使用Python和OpenCV構建一個實時文檔掃描與矯正系統,能夠通過攝像頭自動檢測文檔邊緣并進行透視變換矯正。
一、系統概述
該系統主要實現以下功能:
- 實時攝像頭捕獲圖像
- 邊緣檢測和輪廓查找
- 文檔輪廓識別
- 透視變換矯正文檔
- 二值化處理增強可讀性
二、核心代碼解析
1. 導入必要庫
import numpy as np
import cv2
我們主要使用NumPy進行數值計算,OpenCV進行圖像處理。
2. 輔助函數定義
首先定義了一個簡單的圖像顯示函數,方便調試:
def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(10)
3. 坐標點排序函數
order_points
函數用于將檢測到的文檔四個角點按順序排列(左上、右上、右下、左下):
def order_points(pts):rect = np.zeros((4,2),dtype="float32")s = pts.sum(axis=1)rect[0] = pts[np.argmin(s)] # 左上點(x+y最小)rect[2] = pts[np.argmax(s)] # 右下點(x+y最大)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個二維坐標點進行排序,使其按照左上、右上、右下、左下的順序排列。這在文檔掃描、圖像矯正等應用中非常重要,因為我們需要知道每個角點的確切位置才能正確地進行透視變換。
函數詳細解析
(1)排序邏輯說明
-
左上點(rect[0]):選擇x+y值最小的點
- 因為左上角在坐標系中 x 和 y 值都較小,相加結果最小
-
右下點(rect[2]):選擇x+y值最大的點
- 因為右下角在坐標系中 x 和 y 值都較大,相加結果最大
-
右上點(rect[1]):選擇y-x值最小的點
- 右上角的特點是 y 相對較小而 x 相對較大,所以 y-x 值最小
-
左下點(rect[3]):選擇y-x值最大的點
- 左下角的特點是 y 相對較大而 x 相對較小,所以 y-x 值最大
(2)示例
假設有4個點:
A(10, 20) # 假設是左上B(50, 20) # 右上C(50, 60) # 右下D(10, 60) # 左下
計算過程:
-
x+y值:[30, 70, 110, 70]
- 最小30 → A(左上)
- 最大110 → C(右下)
-
y-x值:[10, -30, 10, 50]
- 最小-30 → B(右上)
- 最大50 → D(左下)
最終排序結果:[A, B, C, D] 即 [左上, 右上, 右下, 左下]
(3)為什么這種方法有效
這種方法利用了二維坐標點的幾何特性:
- 在標準坐標系中,左上角的x和y值都較小
- 右下角的x和y值都較大
- 右上角的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))# 定義目標圖像坐標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
這個函數實現了透視變換(Perspective Transformation),用于將圖像中的任意四邊形區域矯正為一個矩形(即"去透視"效果)。
函數詳細解析
- 輸入參數
def four_point_transform(image, pts):
image
: 原始圖像pts
: 包含4個點的數組,表示要轉換的四邊形區域
- 坐標點排序
rect = order_points(pts)
(tl, tr, br, bl) = rect # 分解為左上(top-left)、右上(top-right)、右下(bottom-right)、左下(bottom-left)
使用之前介紹的order_points
函數將4個點按順序排列
- 計算輸出圖像的寬度
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")
定義變換后的矩形角點坐標(從(0,0)開始的正矩形)
- 計算透視變換矩陣并應用
M = cv2.getPerspectiveTransform(rect, dst) # 計算變換矩陣
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 應用變換
getPerspectiveTransform
: 計算從原始四邊形到目標矩形的3x3變換矩陣warpPerspective
: 應用這個變換矩陣到原始圖像
- 返回結果
return warped
返回矯正后的矩形圖像
- 透視變換原理圖示
原始圖像中的四邊形 變換后的矩形tl--------tr 0--------maxWidth\ / | |\ / | |bl----br maxHeight
- 為什么需要這樣計算寬度和高度?
取最大值的原因:
- 原始四邊形可能有透視變形,兩條對邊長度可能不等
- 選擇較大的值可以確保所有內容都能包含在輸出圖像中
減1的原因:
- 圖像坐標從0開始,所以寬度為maxWidth的圖像,最大x坐標是maxWidth-1
5. 主程序流程
主程序實現了實時文檔檢測和矯正的完整流程:
- 初始化攝像頭
cap = cv2.VideoCapture(0)
if not cap.isOpened():print("Cannot open camera")exit()
- 實時處理循環
while True:flag = 0ret,image = cap.read()orig = image.copy()if not ret:print("不能讀取攝像頭")break
- 圖像預處理
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray,(5,5),0) # 高斯濾波降噪
edged = cv2.Canny(gray,75,200) # Canny邊緣檢測
- 輪廓檢測與篩選
cnts = cv2.findContours(edged,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:3] # 取面積最大的3個輪廓for c in cnts:peri = cv2.arcLength(c,True) # 計算輪廓周長approx = cv2.approxPolyDP(c,0.05 * peri,True) # 多邊形近似area = cv2.contourArea(approx)# 篩選四邊形且面積足夠大的輪廓if area > 20000 and len(approx) == 4:screenCnt = approxflag = 1break
- 文檔矯正與顯示
if flag == 1:# 繪制輪廓image_contours = cv2.drawContours(image,[screenCnt],0,(0,255,0),2)# 透視變換warped = four_point_transform(orig,screenCnt.reshape(4,2))# 二值化處理warped = cv2.cvtColor(warped,cv2.COLOR_BGR2GRAY)ref = cv2.threshold(warped,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
三、完整代碼
# 導入工具包
import numpy as np
import cv2def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(10)
def order_points(pts):# 一共4個坐標點rect = np.zeros((4,2),dtype="float32") # 用來存儲排序之后的坐標位置# 按順序找到對應坐標0123分別是 左上、右上、右下、左下s = pts.sum(axis=1) #對pts矩陣的每一行進行求和操作,(x+y)rect[0] = pts[np.argmin(s)]rect[2] = pts[np.argmax(s)]diff = np.diff(pts,axis=1) #對pts矩陣的每一行進行求差操作,(y-x)rect[1] = pts[np.argmin(diff)]rect[3] = pts[np.argmax(diff)]return rectdef 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)warped = cv2.warpPerspective(image,M,(maxWidth,maxHeight))# 返回變換后的結果return warped# 讀取輸入
import cv2
cap = cv2.VideoCapture(0) # 確保攝像頭是可以啟動的狀態
if not cap.isOpened(): #打開失敗print("Cannot open camera")exit()while True:flag = 0 # 用于標時 當前是否檢測到文檔ret,image = cap.read() # 如果正確讀取幀,ret為Trueorig = image.copy()if not ret: #讀取失敗,則退出循環print("不能讀取攝像頭")breakcv_show("image",image)gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)# 預處理gray = cv2.GaussianBlur(gray,(5,5),0) # 高斯濾波edged = cv2.Canny(gray,75,200)cv_show('1',edged)# 輪廓檢測cnts = cv2.findContours(edged,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:3]image_contours = cv2.drawContours(image,cnts,-1,(0,255,0),2)cv_show("image_contours",image_contours)# 遍歷輪廓for c in cnts:# 計算輪廓近似peri = cv2.arcLength(c,True) # 計算輪廓的周長# C 表示輸入的點集# epsilon表示從原始輪廓到近似輪廓的最大距離,它是一個準確度參數# True表示封閉的approx = cv2.approxPolyDP(c,0.05 * peri,True) # 輪廓近似area = cv2.contourArea(approx)# 4個點的時候就拿出來if area > 20000 and len(approx) == 4:screenCnt = approxflag = 1print(peri,area)print("檢測到文檔")breakif flag == 1:# 展示結果# print("STEP 2: 獲取輪廓")image_contours = cv2.drawContours(image,[screenCnt],0,(0,255,0),2)cv_show("image",image_contours)# 透視變換warped = four_point_transform(orig,screenCnt.reshape(4,2))cv_show("warped",warped)# 二值處理warped = cv2.cvtColor(warped,cv2.COLOR_BGR2GRAY)# ref = cv2.threshold(warped,220,255,cv2.THRESH_BINARY)[1]ref = cv2.threshold(warped,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]cv_show("ref",ref)
cap.release() # 釋放捕捉器
cv2.destroyAllWindows() #關閉圖像窗口
四、結語
本文介紹了一個基于OpenCV的實時文檔掃描與矯正系統,通過邊緣檢測、輪廓分析和透視變換等技術,實現了文檔的自動檢測和矯正。該系統可以方便地應用于日常文檔數字化工作,提高工作效率。
完整代碼已在上文中給出,讀者可以根據自己的需求進行修改和擴展。OpenCV提供了強大的圖像處理能力,結合Python的簡潔語法,使得開發這樣的實用系統變得簡單高效。