基于Python和OpenCV實(shí)現(xiàn)實(shí)時(shí)文檔掃描的全流程
在日常工作與學(xué)習(xí)中,我們經(jīng)常需要將紙質(zhì)文檔轉(zhuǎn)化為電子版本。雖然手機(jī)端有許多掃描 APP,但了解其背后的技術(shù)原理,并使用 OpenCV 手動(dòng)實(shí)現(xiàn)一個(gè)實(shí)時(shí)文檔掃描工具,不僅能加深對(duì)計(jì)算機(jī)視覺(jué)的理解,還能根據(jù)需求靈活定制功能。本文將詳細(xì)講解如何基于 Python 和 OpenCV 構(gòu)建實(shí)時(shí)文檔掃描系統(tǒng),涵蓋圖像預(yù)處理、輪廓檢測(cè)、透視變換、二值化等核心步驟。
一、項(xiàng)目核心原理與目標(biāo)
1.1 核心目標(biāo)
通過(guò)計(jì)算機(jī)攝像頭實(shí)時(shí)采集圖像,自動(dòng)識(shí)別畫(huà)面中的文檔區(qū)域,對(duì)文檔進(jìn)行透視矯正(解決傾斜、變形問(wèn)題),并轉(zhuǎn)化為清晰的二值化圖像(模擬掃描件效果),最終實(shí)現(xiàn)類似專業(yè)掃描儀的功能。
1.2 關(guān)鍵技術(shù)原理
文檔掃描的核心是解決 “從傾斜變形到正面對(duì)齊” 的問(wèn)題,主要依賴以下兩項(xiàng)計(jì)算機(jī)視覺(jué)技術(shù):
- 透視變換(Perspective Transformation):將不規(guī)則的四邊形文檔區(qū)域(如傾斜拍攝的文檔)映射為規(guī)則的矩形,消除視角帶來(lái)的變形,得到文檔的正視圖。
- 輪廓檢測(cè)(Contour Detection):從圖像中識(shí)別出文檔的邊緣輪廓,確定文檔的四個(gè)頂點(diǎn)坐標(biāo),為透視變換提供關(guān)鍵參數(shù)。
二、項(xiàng)目環(huán)境準(zhǔn)備
在開(kāi)始編寫(xiě)代碼前,需要搭建基礎(chǔ)的開(kāi)發(fā)環(huán)境,核心依賴兩個(gè) Python 庫(kù):
- OpenCV:用于圖像采集、預(yù)處理、輪廓檢測(cè)、透視變換等核心操作。
- NumPy:用于數(shù)值計(jì)算,尤其是矩陣運(yùn)算(透視變換需處理坐標(biāo)矩陣)。
安裝命令
打開(kāi)終端,執(zhí)行以下命令安裝依賴庫(kù):
pip install opencv-python numpy
三、核心功能模塊拆解與實(shí)現(xiàn)
整個(gè)實(shí)時(shí)文檔掃描系統(tǒng)分為 5 個(gè)核心模塊,我們將逐一講解每個(gè)模塊的代碼邏輯與實(shí)現(xiàn)思路。
模塊 1:坐標(biāo)排序(order_points)—— 確定文檔頂點(diǎn)順序
透視變換需要明確文檔的四個(gè)頂點(diǎn)的正確順序(左上、右上、右下、左下),否則會(huì)導(dǎo)致變換后圖像錯(cuò)亂。order_points函數(shù)通過(guò)坐標(biāo)的 “和” 與 “差” 特性,自動(dòng)排序四個(gè)頂點(diǎn)。
代碼實(shí)現(xiàn)
import numpy as np
import cv2
def order_points(pts):
# 初始化4個(gè)頂點(diǎn)的坐標(biāo)(左上、右上、右下、左下)
rect = np.zeros((4, 2), dtype="float32")
# 1. 計(jì)算每個(gè)點(diǎn)的x+y之和:左上點(diǎn)的和最小,右下點(diǎn)的和最大
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)] # 左上(tl)
rect[2] = pts[np.argmax(s)] # 右下(br)
# 2. 計(jì)算每個(gè)點(diǎn)的y-x之差(np.diff默認(rèn)是后減前,即y-x):右上點(diǎn)的差最小,左下點(diǎn)的差最大
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)] # 右上(tr)
rect[3] = pts[np.argmax(diff)] # 左下(bl)
return rect邏輯解析
- 坐標(biāo)和(s = x + y):對(duì)于傾斜的文檔,左上角頂點(diǎn)(x 小、y ?。┑暮妥钚?,右下角頂點(diǎn)(x 大、y 大)的和最大,可直接定位這兩個(gè)點(diǎn)。
- 坐標(biāo)差(diff = y - x):右上角頂點(diǎn)(x 大、y ?。┑牟钭钚。笙陆琼旤c(diǎn)(x 小、y 大)的差最大,以此定位剩余兩個(gè)點(diǎn)。
模塊 2:透視變換(four_point_transform)—— 矯正文檔形態(tài)
透視變換的核心是通過(guò)透視矩陣(M) 將不規(guī)則的文檔四邊形映射為規(guī)則矩形。該過(guò)程需要兩個(gè)關(guān)鍵參數(shù):
- 原始圖像中文檔的四個(gè)頂點(diǎn)(已通過(guò)
order_points排序)。 - 目標(biāo)矩形的四個(gè)頂點(diǎn)(通常設(shè)為左上角 (0,0)、右上角 (maxWidth,0)、右下角 (maxWidth,maxHeight)、左下角 (0,maxHeight))。
代碼實(shí)現(xiàn)
def four_point_transform(image, pts):
# 1. 獲取排序后的四個(gè)頂點(diǎn)
rect = order_points(pts)
(tl, tr, br, bl) = rect # 左上、右上、右下、左下
# 2. 計(jì)算目標(biāo)矩形的寬度(取文檔左右兩邊的最大長(zhǎng)度,避免變形)
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2)) # 下邊長(zhǎng)
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2)) # 上邊長(zhǎng)
maxWidth = max(int(widthA), int(widthB)) # 目標(biāo)寬度
# 3. 計(jì)算目標(biāo)矩形的高度(取文檔上下兩邊的最大長(zhǎng)度)
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2)) # 右邊長(zhǎng)
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2)) # 左邊長(zhǎng)
maxHeight = max(int(heightA), int(heightB)) # 目標(biāo)高度
# 4. 定義目標(biāo)矩形的四個(gè)頂點(diǎn)
dst = np.array([
[0, 0], # 左上
[maxWidth - 1, 0], # 右上(減1是因?yàn)橄袼厮饕龔?開(kāi)始)
[maxWidth - 1, maxHeight - 1], # 右下
[0, maxHeight - 1]], dtype="float32") # 左下
# 5. 計(jì)算透視矩陣M
M = cv2.getPerspectiveTransform(rect, dst)
# 6. 應(yīng)用透視變換,得到矯正后的圖像
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
return warped關(guān)鍵函數(shù)解析
cv2.getPerspectiveTransform(rect, dst):根據(jù)原始頂點(diǎn)(rect)和目標(biāo)頂點(diǎn)(dst),計(jì)算 3x3 的透視矩陣 M,該矩陣描述了從原始圖像到目標(biāo)圖像的映射關(guān)系。cv2.warpPerspective(image, M, (maxWidth, maxHeight)):應(yīng)用透視矩陣 M,將原始圖像變換為目標(biāo)尺寸(maxWidth, maxHeight)的正視圖。
模塊 3:圖像預(yù)處理與輪廓檢測(cè) —— 定位文檔區(qū)域
要從攝像頭實(shí)時(shí)圖像中識(shí)別文檔,需要先對(duì)圖像進(jìn)行預(yù)處理(降噪、邊緣檢測(cè)),再通過(guò)輪廓檢測(cè)提取文檔的邊緣。
預(yù)處理邏輯
- 灰度化(cvtColor):將彩色 圖像轉(zhuǎn)為灰度圖像,減少計(jì)算量(彩色 圖像有 3 個(gè)通道,灰度圖僅 1 個(gè)通道)。
- 高斯模糊(GaussianBlur):通過(guò)卷積操作平滑圖像,減少噪聲干擾,避免邊緣檢測(cè)時(shí)誤識(shí)別噪聲為邊緣。
- 邊緣檢測(cè)(Canny):通過(guò)計(jì)算像素梯度,提取圖像中的邊緣信息,為輪廓檢測(cè)做準(zhǔn)備。
輪廓檢測(cè)邏輯
- 提取輪廓(findContours):從邊緣圖像中提取所有外部輪廓(
cv2.RETR_EXTERNAL表示只取最外層輪廓)。 - 篩選輪廓(sorted + contourArea):按輪廓面積降序排序,取前 3 個(gè)最大輪廓(文檔通常是畫(huà)面中面積最大的物體)。
- 多邊形逼近(approxPolyDP):將不規(guī)則輪廓逼近為多邊形,通過(guò)判斷 “面積是否足夠大” 和 “是否為四邊形”,確定文檔輪廓(文檔通常是四邊形)。
核心代碼片段
# 圖像預(yù)處理
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 灰度化
gray = cv2.GaussianBlur(gray, (5, 5), 0) # 高斯模糊(5x5卷積核,標(biāo)準(zhǔn)差0)
edged = cv2.Canny(gray, 15, 45) # 邊緣檢測(cè)(低閾值15,高閾值45)
# 輪廓檢測(cè)與篩選
cnts = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] # 提取外部輪廓
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:3] # 按面積降序,取前3個(gè)
# 遍歷輪廓,判斷是否為文檔(四邊形+面積足夠大)
for c in cnts:
peri = cv2.arcLength(c, True) # 計(jì)算輪廓周長(zhǎng)(True表示輪廓閉合)
# 多邊形逼近:epsilon=0.05*peri(逼近精度,值越小越接近原輪廓)
approx = cv2.approxPolyDP(c, 0.05 * peri, True)
area = cv2.contourArea(approx) # 計(jì)算逼近后多邊形的面積
# 條件:面積>20000(過(guò)濾小物體)且頂點(diǎn)數(shù)=4(文檔為四邊形)
if area > 20000 and len(approx) == 4:
screenCnt = approx # 確定文檔輪廓
print("檢測(cè)到文檔,輪廓面積:", area)
break模塊 4:二值化處理 —— 生成掃描件效果
透視變換后的文檔圖像仍為灰度圖,通過(guò)二值化處理可將其轉(zhuǎn)化為 “黑底白字” 或 “白底黑字” 的清晰圖像,模擬專業(yè)掃描件的效果。
代碼實(shí)現(xiàn)
# 對(duì)透視變換后的圖像進(jìn)行二值化 warped_gray = cv2.cvtColor(warped_result, cv2.COLOR_BGR2GRAY) # 自動(dòng)閾值二值化(THRESH_OTSU:自動(dòng)計(jì)算最優(yōu)閾值,避免手動(dòng)調(diào)參) ref_result = cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
關(guān)鍵函數(shù)解析
cv2.threshold(src, thresh, maxval, type):src:輸入灰度圖。thresh:閾值(設(shè)為 0,由THRESH_OTSU自動(dòng)計(jì)算)。maxval:超過(guò)閾值的像素值(設(shè)為 255,即白色)。type:THRESH_BINARY表示 “超過(guò)閾值設(shè)為 maxval,否則設(shè)為 0”;THRESH_OTSU表示自動(dòng)計(jì)算最優(yōu)閾值。
模塊 5:實(shí)時(shí)攝像頭采集與窗口顯示
通過(guò)cv2.VideoCapture調(diào)用計(jì)算機(jī)攝像頭,實(shí)時(shí)采集圖像并處理,同時(shí)通過(guò)自定義的cv_show函數(shù)顯示各步驟的結(jié)果(原始圖像、邊緣檢測(cè)、輪廓、矯正后圖像、二值化圖像)。
代碼實(shí)現(xiàn)
# 自定義顯示函數(shù)(不自動(dòng)關(guān)閉窗口,需按q退出)
def cv_show(name, img):
cv2.imshow(name, img)
# 初始化攝像頭(0表示默認(rèn)攝像頭)
cap = cv2.VideoCapture(0)
# 檢查攝像頭是否正常打開(kāi)
if not cap.isOpened():
print("無(wú)法打開(kāi)攝像頭")
exit()
# 實(shí)時(shí)采集與處理循環(huán)
while True:
ret, image = cap.read() # 讀取一幀圖像(ret:是否讀取成功,image:圖像數(shù)據(jù))
orig = image.copy() # 保存原始圖像副本
if not ret:
print("無(wú)法讀取攝像頭幀")
break
# 1. 顯示原始圖像
cv_show("Original", image)
# 2. 圖像預(yù)處理與輪廓檢測(cè)(此處省略,見(jiàn)模塊3)
# ...(預(yù)處理、輪廓檢測(cè)代碼)...
# 3. 若檢測(cè)到文檔,顯示結(jié)果
if flag == 1:
# 繪制文檔輪廓
image_with_doc = cv2.drawContours(orig.copy(), [screenCnt], 0, (0, 255, 0), 2)
cv_show("Document Detection", image_with_doc)
# 透視變換
warped_result = four_point_transform(orig, screenCnt.reshape(4, 2))
cv_show("Warped", warped_result)
# 二值化
ref_result = cv2.threshold(cv2.cvtColor(warped_result, cv2.COLOR_BGR2GRAY),
0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show("Binarized", ref_result)
# 按下'q'鍵退出循環(huán)(waitKey(1):等待1ms,返回按鍵ASCII碼)
if cv2.waitKey(1) == ord('q'):
break
# 釋放攝像頭資源,關(guān)閉所有窗口
cap.release()
cv2.destroyAllWindows()四、完整代碼整合與運(yùn)行說(shuō)明
4.1 完整代碼
將上述模塊整合,得到完整的實(shí)時(shí)文檔掃描代碼:
import numpy as np
import cv2
def order_points(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
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
def cv_show(name, img):
cv2.imshow(name, img)
if __name__ == "__main__":
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("Cannot open camera")
exit()
while True:
flag = 0
ret, image = cap.read()
orig = image.copy()
if not ret:
print("不能讀取攝像頭")
break
# 顯示原始圖像
cv_show("Original", image)
# 圖像預(yù)處理
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 0)
edged = cv2.Canny(gray, 15, 45)
cv_show("Edge Detection", edged)
# 輪廓檢測(cè)與篩選
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.copy(), cnts, -1, (0, 255, 0), 2)
cv_show("Contours", image_contours)
# 識(shí)別文檔輪廓
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 = approx
flag = 1
print(f"檢測(cè)到文檔,周長(zhǎng):{peri:.2f},面積:{area:.2f}")
# 顯示文檔檢測(cè)結(jié)果
image_with_doc = cv2.drawContours(orig.copy(), [screenCnt], 0, (0, 255, 0), 2)
cv_show("Document Detection", image_with_doc)
# 透視變換
warped_result = four_point_transform(orig, screenCnt.reshape(4, 2))
cv_show("Warped", warped_result)
# 二值化
warped_gray = cv2.cvtColor(warped_result, cv2.COLOR_BGR2GRAY)
ref_result = cv2以上就是基于Python和OpenCV實(shí)現(xiàn)實(shí)時(shí)文檔掃描的全流程的詳細(xì)內(nèi)容,更多關(guān)于Python OpenCV實(shí)時(shí)文檔掃描的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python和mysql交互操作實(shí)例詳解【基于pymysql庫(kù)】
這篇文章主要介紹了python和mysql交互操作,結(jié)合實(shí)例形式詳細(xì)分析了Python基于pymysql庫(kù)實(shí)現(xiàn)mysql數(shù)據(jù)庫(kù)的連接、增刪改查等各種常見(jiàn)操作技巧,需要的朋友可以參考下2019-06-06
Python實(shí)現(xiàn)字符串中某個(gè)字母的替代功能
小編想實(shí)現(xiàn)這樣一個(gè)功能:將輸入字符串中的字母 “i” 變成字母 “p”。想著很簡(jiǎn)單,怎么實(shí)現(xiàn)呢?下面小編給大家?guī)?lái)了Python實(shí)現(xiàn)字符串中某個(gè)字母的替代功能,感興趣的朋友一起看看吧2019-10-10
利用Pandas讀取表格行數(shù)據(jù)判斷是否相同的方法
這篇文章主要給大家介紹了關(guān)于利用Pandas讀取表格行數(shù)據(jù)判斷是否相同的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Python學(xué)習(xí)筆記嵌套循環(huán)詳解
這篇文章主要介紹了Python學(xué)習(xí)筆記嵌套循環(huán)詳解,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-07-07
Keras:Unet網(wǎng)絡(luò)實(shí)現(xiàn)多類語(yǔ)義分割方式
本文主要利用U-Net網(wǎng)絡(luò)結(jié)構(gòu)實(shí)現(xiàn)了多類的語(yǔ)義分割,并展示了部分測(cè)試效果,希望對(duì)你有用!2020-06-06
Python實(shí)現(xiàn)OCR識(shí)別之pytesseract案例詳解
這篇文章主要介紹了Python實(shí)現(xiàn)OCR識(shí)別之pytesseract案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
python對(duì)兩個(gè)數(shù)組進(jìn)行合并排列處理的兩種方法
最近遇到數(shù)組合并問(wèn)題,以此記錄解決方法,供大家參考學(xué)習(xí),下面這篇文章主要給大家介紹了關(guān)于python對(duì)兩個(gè)數(shù)組進(jìn)行合并排列處理的兩種方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
python flask基于cookie和session來(lái)實(shí)現(xiàn)會(huì)話控制的實(shí)戰(zhàn)代碼
所謂的會(huì)話(session),就是客戶端瀏覽器和服務(wù)端網(wǎng)站之間一次完整的交互過(guò)程,本文介紹falsk通過(guò)cookie和session來(lái)控制http會(huì)話的全部解析,通常我們可以用cookie和session來(lái)保持用戶登錄等,感興趣的朋友一起看看吧2024-03-03

