
簡介基于OpenCV的二維碼檢測識別基礎示例源碼包面向計算機視覺入門者與初級開發者演示如何通過OpenCV自帶的二維碼檢測器完成二維碼的定位與內容解析。整套資源以C工程形式組織包含圖像讀取、灰度轉換、二維碼檢測、邊界框繪制、解碼信息展示等關鍵環節并且提供說明文檔和已編譯好的可執行程序方便對照源代碼理解運行效果也可自行修改參數觀察檢測變化還可借此了解二維碼部分缺失時的錯誤糾正機制。代碼結構注釋清晰各步驟輸出明確便于調試和二次開發。壓縮包共58個文件主要類型為源代碼文件、工程配置文件、可執行程序、說明文檔以及編譯中間文件包體大小僅2.69MB目錄結構清晰便于按需查閱和快速定位關鍵代碼。目前已有1728人學習下載適合希望掌握OpenCV二維碼檢測基礎流程并遷移到實際視覺項目中的開發者使用。1. 為什么還要自己寫二維碼識別OpenCV 的二維碼檢測能做什么拿手機對著包裝盒上的二維碼掃了一下結果十幾秒才彈出內容屏幕上的轉圈圖標轉得人發慌。這種延遲往往不是網絡問題而是二維碼本身被壓皺、反光或者拍糊了。很多入門項目一開始就接云端識別 API識別率確實高但離線設備和內網環境根本離不開本地處理。基于 OpenCV 的二維碼檢測識別是一個完全本地、不依賴外部服務的方案只需要 OpenCV 4.x 自帶的一個二維碼檢測器就能在電腦或嵌入式設備上跑出一個能用的基礎 demo。下面會把環境搭建、核心 API、實時攝像頭識別和排錯思路一次講清楚適合剛接觸圖像處理的新手也適合想從接口調用切換到本地識別的工程師。2. 環境準備與最小可運行工程OpenCV 二維碼 demo 的依賴和項目結構2.1 安裝 OpenCV 的版本選擇與常見坑OpenCV 從 4.1.0 開始把二維碼檢測模塊從 opencv_contrib 挪到了主倉庫。也就是說標準安裝的 opencv-python 就自帶 QRCodeDetector不需要像早期教程那樣源碼編譯 contrib。版本上我建議至少用 4.5.2因為 detectAndDecodeMulti 在 4.5.2 之后的行為更穩定修復了部分單碼誤報和返回空數組的問題。如果你只是做靜態圖識別4.4 也能用但為了省心還是裝新不裝舊。操作系統方面Windows、Ubuntu、樹莓派都行只要 Python 是 3.7 以上。安裝就兩條命令# 方式一直接安裝官方 Python 包 pip install opencv-python4.5.5.64 # 方式二指定鏡像源下載速度更快 pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple第一段命令用固定版本目的是保持開發環境與生產一致避免某天 pip 自動升級到新版帶來行為變化。第二段-i參數表示從清華鏡像站拉取安裝包國內網絡環境下能明顯減少超時概率。安裝完成后執行python -c import cv2; print(cv2.__version__)驗證。如果提示ModuleNotFoundError: no module named cv2先去檢查當前解釋器屬于哪個環境而不是急著重新下載。我吃過不少虧明明 pip install 成功卻在 Python 交互式窗口里 import 失敗原因通常是終端里開了多個 conda 環境命令裝進了 base運行在另一個環境。關于版本選擇給一個簡單的對照需求OpenCV 3.xOpenCV 4.x二維碼檢測contrib 里手動編主倉庫自帶單碼識別detectAndDecode 可用更快更穩多碼識別基本不可用4.34.5.2 穩定安裝復雜度要 cmake 編譯pip install 即可這個表不是我編出來的。4.x 把二維碼模塊內置確實讓基礎 demo 的門檻低了一大截。如果你需要更精準的深度學習二維碼識別還可以裝 opencv-contrib-python里面有 WeChatQRCode但那是后話后面章節再提。2.2 用 CMake 或 pip 搭出一個最小項目Python demo 的工程結構很簡單一個 src 目錄加一個 requirements.txt 就夠。C 工程就需要 CMake 來管理因為直接手動配置 VS 屬性頁太容易漏依賴。下面是一個最小 CMakeLists.txtcmake_minimum_required(VERSION 3.10) project(qr_demo) find_package(OpenCV REQUIRED) add_executable(qr_demo main.cpp) target_link_libraries(qr_demo ${OpenCV_LIBS})這段 CMake 配置的核心是find_package它會自動探測 OpenCV 的頭文件和庫路徑。target_link_libraries把 OpenCV 庫鏈接到可執行文件。在 Ubuntu 上如果報找不到 OpenCV先運行sudo apt install libopencv-dev它會把 /usr/include 下的頭文件和庫都裝上。Windows 下用 CMake 時記得設置OpenCV_DIR環境變量指向installed/opencv/build目錄否則 find_package 也會失敗。我用這個方式在 Windows 10 上配置 VS2019 和 MinGW 都沒出過大問題。Python 端的結構更簡單qr_demo/ ├── main.py ├── requirements.txt └── images/ └── qr.png這樣的項目層級對“基礎 demo 程序源代碼”來說足夠清晰。main.py 里放識別主流程requirements.txt 記錄 opencv-python 版本images 目錄放測試圖片。看起來極簡但同事拿到手后不需要問“怎么跑”就能直接復現。2.3 驗證安裝讀取一張帶二維碼的圖片在開始識別前先確認圖像 I/O 正常。用下面這段代碼讀圖import cv2 img cv2.imread(qr.png) if img is None: print(圖片讀取失敗檢查路徑) else: print(讀取成功分辨率:, img.shape)cv2.imread 返回一個 numpy 數組讀取失敗時返回 None。img.shape 是個元組例如 (720, 1280, 3)分別代表高、寬、顏色通道數。這段代碼沒什么高深技巧但能快速排除路徑錯誤和中文文件名問題。注意 OpenCV 的 imread 不支持中文路徑目錄或文件名帶中文時很容易讀到 None這個問題在 Windows 上最常見。解決方法是先用 cv2.imdecode 讀取字節流比如 np.fromfile 配合 imdecode這里不展開但你要知道有這個坑。3. 核心 API 拆解OpenCV 的 QRCodeDetector 與 DetectionResult3.1 QRCodeDetector 的檢測與解碼流程OpenCV 把二維碼識別拆成檢測和解碼兩步。檢測是找到圖形解碼是讀出字符串。實際使用中不用關心內部細節直接調用 detectAndDecode 即可。這個接口一次性返回解碼內容、角點坐標和規范化碼圖。看代碼import cv2 detector cv2.QRCodeDetector() img cv2.imread(qr.png) data, points, straight_qrcode detector.detectAndDecode(img) print(解碼內容:, data) print(角點形狀:, points.shape if points is not None else None)detectAndDecode 返回值先說清楚第一個 data 是字符串沒有識別到就是空字符串第二個 points 是 numpy 數組形狀是 (1, 4, 2)里面是四個角點坐標第三個 straight_qrcode 是標準化的 40x40 碼圖可以用來調試。這里容易出問題的是 points 可能為 None直接訪問 points.shape 會報 AttributeError。所以業務代碼里必須加 None 判斷。為什么要關心 straight_qrcode因為它能讓我們看到 OpenCV 內部的歸一化效果。你可以用cv2.imwrite(debug.png, straight_qrcode)保存出來觀察二維碼是否被拉正。如果 straight_qrcode 看起來扭曲說明原圖變形嚴重即使 data 為空也能有個直觀排查方向。3.2 識別多個二維碼detectAndDecodeMulti 的參數與返回貨架上的訂單號好幾個碼一張圖里總要一次全識別出來。detectAndDecodeMulti 就是干這個的import cv2 det cv2.QRCodeDetector() img cv2.imread(multi_qr.png) retval, decoded_info, points, straight_qrcode det.detectAndDecodeMulti(img) for i, info in enumerate(decoded_info): if info: print(f位置 {i}, 內容: {info})這里的 retval 是個布爾值表示是否檢測到了至少一個二維碼decoded_info 是字符串列表每個元素對應一個二維碼的內容識別失敗的會留空字符串points 是形狀為 (N, 4, 2) 的數組N 是檢測到的二維碼數量。要注意的是retval 為 True 并不代表 decoded_info 里每個字符串都是非空的。多碼識別對圖像尺度的要求比較苛刻。我試過在一張 1080p 圖片上放六個大小不一的二維碼中等尺寸的能識別最小的那個經常丟。解決辦法是把圖片先縮放一半再傳入檢測器讓最小模塊的像素寬度增大但代價是耗時增加。另一個做法是直接使用 multi 的變體函數并且傳入一個縮放因子比如scale_factor 2.0 scaled cv2.resize(img, None, fxscale_factor, fyscale_factor, interpolationcv2.INTER_CUBIC) retval, decoded_info, points, _ det.detectAndDecodeMulti(scaled)這樣可以讓小二維碼更容易被找出來。interpolation 用 INTER_CUBIC 比默認的 INTER_LINEAR 更平滑適合放大場景。但放大后 points 坐標會相對于縮放圖需要除以 scale_factor 映射回原圖。3.3 矩形框與透視變換定位二維碼的四個角點基礎 demo 最常見的輸出是畫框。先用 points 在圖上畫出多邊形區域import numpy as np def draw_qr_result(image, points, text): if points is None: return pts points[0].astype(np.int32) cv2.polylines(image, [pts], isClosedTrue, color(0, 255, 0), thickness2) if text: x, y pts[0] cv2.putText(image, text[:30], (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)這段函數把第一個二維碼的角點轉為 int32polylines 畫出閉合框putText 在左上角顯示識別出的內容。為什么是畫整個四邊形因為二維碼的區域是四邊形用 polylines 連起來比 rectangle 更準確。如果你的二維碼旋轉了 45 度rectangle 會把無關區域也框進去polylines 則嚴格貼著邊界。points 坐標順序是逆時針還是順時針OpenCV 文檔沒有明確承諾但通常是一致的。用 polylines 畫閉合線不用擔心順序。透視變換在這里的用途是解決傾斜識別錯誤。比如你拍了一張 A4 紙上面的二維碼以一定角度朝向攝像頭直接 detectAndDecode 可能失敗但 points 已經拿回來了。你可以通過 getPerspectiveTransform 把四邊形區域投影成正方形再對投影后的圖調用一次 detect。這個技巧放到第 5 章細說這里只需要知道 points 是透視變換賴以執行的關鍵參數就夠了。4. 從圖片到攝像頭讓基礎 demo 支持實時識別4.1 攝像頭畫面預處理灰度、二值化與尺寸控制靜態圖片識別跑通后你會發現把同一套代碼搬到攝像頭會有新的問題畫面抖動、光照閃爍、幀率不足。所以實時識別需要先對原始幀做預處理。我的固定流程是先轉灰度再縮放到 640 寬最后再做識別。下面是一個最小閉環import cv2 cap cv2.VideoCapture(0) det cv2.QRCodeDetector() while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray_resized cv2.resize(gray, (640, 480)) data, points, _ det.detectAndDecode(gray_resized) if data: cv2.putText(frame, data, (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(result, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()這段代碼中VideoCapture(0) 打開系統默認攝像頭ret 讀取成功標志。detectAndDecode 的輸入是縮放過后的灰度圖而不是原始彩色圖。為什么要縮放因為攝像頭輸出 1280x720 時二維碼像素數量已經足夠再大只是增加計算量而且 OpenCV 的二維碼檢測器內置了金字塔搜索原圖過大反而會引入很多尺度的候選區域導致誤檢。縮放成 640 寬后大部分場景識別率不變耗時卻能降一半。如果你用樹莓派、香橙派這類單板機這個縮放就是壓垮性能的最后一根稻草不做不行。還有一點識別前要不要二值化我的經驗是不要全局二值化。二維碼本身有足夠對比度二值化碰上光照不均勻反而會把模塊連成一片。如果確實偏暗用 CLAHE 而不是 Otsu。下面演示clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(gray)CLAHE 能增強局部對比度同時避免全局閾值把白色區域過曝。對于攝像頭迎著窗戶光、二維碼在陰影里這種情況效果很明顯。4.2 實時視頻流中的二維碼跟蹤與穩定顯示識別率再高每一幀都重畫框也會閃。一個基礎 demo 可以在幀間保持結果連續達到“跟蹤”的效果。常見做法是保存上一幀的 points當當前幀數據為空時繼續用上一幀的位置畫框。這樣手一抖框不會立刻消失觀感穩定很多。實現如下prev_points None while True: ret, frame cap.read() gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) data, points, _ det.detectAndDecode(gray) if data: prev_points points display_text data elif prev_points is not None: display_text tracking else: display_text None if prev_points is not None: draw_qr_result(frame, prev_points, display_text) cv2.imshow(frame, frame)這個邏輯其實就是一個簡化的一階跟蹤器。prev_points 在成功識別時刷新在失敗時沿用。注意這里沒有校驗新一幀的 points 是不是真的在同樣位置如果攝像頭轉到很遠處prev_points 就會漂移。加一個超時機制連續 N 幀失敗就清空 prev_points。我一般設 30 幀大約一秒足夠擋住瞬時遮擋又不會在二維碼離開視野后還留著舊框。4.3 性能調優分辨率、幀率與檢測間隔在 PC 上跑 demo 不在乎 CPU但在嵌入式設備上CPU 占用率直接決定能不能跑滿 30 幀。最有效的優化不是換更快的算法而是降低輸入分辨率和減少檢測頻率。下表是我常用的一組參數參數建議值說明輸入寬度640 或 480再小會影響小模塊識別檢測間隔每 2 到 3 幀每幀檢測會吃滿 CPU顯示分辨率原始幀 1:1畫框只消耗極少算力線程模式單獨識別線程避免 imshow 阻塞檢測間隔用幀計數實現frame_count 0 interval 3 while True: ret, frame cap.read() frame_count 1 if frame_count % interval 0: data, points, _ det.detectAndDecode(gray) frame_count 0 ...這樣做之后識別邏輯只在前一幀的 key frame 上運行其余幀直接畫框。由于識別結果每三幀才刷新一次框的移動會顯得有點“粘”但換來的性能提升非常可觀。在樹莓派 4B 上這個優化可以把 CPU 占用率從 300% 降到 150% 以下幀率依然接近 30。我實際調優時還發現OpenCV 的 detectAndDecode 在灰度圖上的耗時是彩圖的 0.8 倍左右但準確率幾乎不變。所以無論你怎么調轉灰度這一步別省。至于二值化只在實驗階段配合 CLAHE 做完過最終 demo 里我還是只用灰度圖。5. 排錯與進階識別失敗時的診斷思路和一個實用技巧5.1 常見識別失敗原因與 OpenCV 報錯對照工程化過程中識別失敗是常態。最常見的表現有三種data 為空、points 為 None、報異常。data 為空但 points 不為空說明檢測器找到了疑似區域但解碼失敗這通常和透視變形、模糊、遮罩或三個定位角的損傷有關。points 也為空說明壓根沒檢測到大概率是二維碼太小、圖像中占比不足或者是打印質量問題。異常情況多半是輸入圖不是三通道或 points 的維度不對。下面這個表能幫你快速定位現象直接原因優先嘗試data 為空points 有內容二維碼模糊、透視嚴重用 warpPerspective 校正后重識別data 為空points 也為空二維碼占比小于 5%放大圖像或拉近鏡頭多碼識別漏檢多個碼靠得太近縮小圖或降低 scale 步長cv2.error 輸入類型錯誤傳入的不是 8UC3cvtColor 轉回 BGR5.2 用可視化角點定位識別失敗的環節調試點位置最直觀的方法是畫出來。與其盯著坐標數組不如把角點序號直接畫在圖像上import numpy as np pts points[0].astype(np.int32) for i, (x, y) in enumerate(pts): cv2.circle(frame, (x, y), 5, (0, 0, 255), -1) cv2.putText(frame, str(i), (x 8, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)這段代碼會在每個角點位置畫紅色圓點并在旁邊標出序號。順序信息對透視變換非常重要因為 getPerspectiveTransform 的輸入和輸出頂點順序必須一一對應。如果畫出來發現 0 號角點在右下角而不是左上角你就要在構造 src_pts 時手動交換順序。我通常用這個方式判斷是“檢測失敗”還是“解碼失敗”有圓點而沒內容說明是解碼階段的問題沒有圓點說明檢測階段就沒找到二維碼。5.3 進階用透視歸一化提高單張圖片識別率最后一個實用技巧把傾斜的二維碼先透視校正再交給識別器。很多識別失敗的場景用這一點就能救回來import numpy as np src_pts points[0].astype(float32) width 300 dst_pts np.array([[0, 0], [width - 1, 0], [width - 1, width - 1], [0, width - 1]], dtypefloat32) M cv2.getPerspectiveTransform(src_pts, dst_pts) warped cv2.warpPerspective(img, M, (width, width)) data, warped_points, _ det.detectAndDecode(warped) if data: print(校正后識別成功:, data) else: print(校正后仍然失敗)這里 getPerspectiveTransform 的作用是根據兩套對應點求出一個 3x3 變換矩陣 MwarpPerspective 再按 M 把原圖的這個區域投影到正方形。width 決定輸出圖的分辨率300 是我反復測試后的折中值。如果 width 太小模塊會糊在一起太大會放大噪點。如果你的二維碼很細密可以把 width 調到 400但別超過 500否則耗時增加卻沒有明顯收益。這個技巧對打印在紙上的二維碼特別有效。原因是 OpenCV 自帶的 QRCodeDetector 采用基于特征搜索的定位一旦透視導致定位圖案形變解碼會失敗。而手工校正后二維碼以正面視圖進入解碼器成功率會顯著上升。如果校正后仍然失敗再看圖像是否有反光或覆蓋對 warped 調用一次 CLAHE 增強對比度常常是最后一根救命稻草。本文還有配套的精品資源點擊獲取