戰(zhàn):代碼驗(yàn)證與環(huán)境兼容的九十九篇積累)
這個系列做到第九十九篇的時候心里是有些感慨的。最初接手 PyImgSearch 博客中文翻譯這個項(xiàng)目時我以為最費(fèi)心思的是術(shù)語怎么翻、長難句怎么處理真正做了幾十篇之后才發(fā)現(xiàn)工作量最大的根本不是翻譯而是讓每一段示例代碼在新的環(huán)境里重新跑上一遍。圖像處理類的技術(shù)內(nèi)容和其他編程教程有個不太一樣的特性它的結(jié)果是可視化的代碼到底有沒有問題丟一張圖進(jìn)去立刻見分曉。這篇把目前做到第九十九篇時積累下來的方法和教訓(xùn)整理了出來適合三類人讀。一類是正在做技術(shù)博客、技術(shù)文檔本地化的人想知道怎么保證譯稿里的代碼真的能跑一類是剛接觸 OpenCV 的初學(xué)者可以順著驗(yàn)證流程學(xué)到一套完整的圖像處理操作鏈路還有一類是長期維護(hù)內(nèi)容倉庫的朋友可以直接對照文末的審查清單檢查自己的發(fā)布規(guī)范。1. 系列定位與翻譯思路的成型1.1 代碼驅(qū)動型內(nèi)容為什么值得系統(tǒng)翻譯PyImgSearch 這個博客的內(nèi)容幾乎全部圍繞圖像處理展開最大的特點(diǎn)是“代碼驅(qū)動”每篇文章都自帶可運(yùn)行的腳本照著敲一遍就能得到結(jié)果圖不需要花錢買數(shù)據(jù)集也不需要多高級的硬件普通筆記本就能跑。這種風(fēng)格天然適合翻譯因?yàn)樗幸粋€明確的驗(yàn)收標(biāo)準(zhǔn)——譯文要讓讀者跑得起來。我做了頭幾篇之后意識到中文翻譯比原文還多一層價值。原文讀者大多是英文環(huán)境運(yùn)行代碼時會少很多阻礙中文讀者經(jīng)常在配置環(huán)境這一步就卡住尤其是 OpenCV 這種依賴較多的庫光一個“安裝”就能勸退一批人。所以翻譯這類技術(shù)內(nèi)容不能只對著文字工作得把環(huán)境、版本、兼容性這些“隱形障礙”一并處理掉。這也是為什么做到第七十篇之后我逐步放棄了逐字逐句的翻譯方式換成了“驗(yàn)證優(yōu)先”的模式先跑代碼再翻文字。代碼跑不通的時候文字翻譯得再順也沒有意義因?yàn)樽x者一定會按照譯稿敲一遍敲完發(fā)現(xiàn)報錯那這篇翻譯就白做了。1.2 三層結(jié)構(gòu)精講、驗(yàn)證、注釋我現(xiàn)在的處理方式可以拆成三層結(jié)構(gòu)每一層解決不同的問題。第一層是原文精講。保留原作者的核心論述和代碼邏輯不追求字字對應(yīng)但求把“為什么這樣做”講清楚。圖像處理文章常常在幾個關(guān)鍵步驟之間埋著設(shè)計意圖比如為什么先高斯模糊再做邊緣檢測這類邏輯必須保留不能因?yàn)椴眉舳鴣G失。第二層是本地驗(yàn)證。每段代碼我都要在 Windows 和 Linux 兩個環(huán)境各跑一遍確認(rèn)依賴版本、接口寫法、圖像路徑都沒有問題。驗(yàn)證時用的測試圖盡量換成常見尺寸的自制圖片避免讀者下載原圖失敗。跑通過之后才允許這段代碼進(jìn)入譯稿。第三層是譯注。把版本差異、棄用函數(shù)、安裝命令的本地化提示補(bǔ)充進(jìn)去。這一步其實(shí)是全篇最有價值的部分原文從不會告訴你 OpenCV 3 和 OpenCV 4 的findContours返回值不一樣但中文讀者恰恰最容易在這里踩坑。這三層缺一不可。只做第一層讀起來很快但讀者一運(yùn)行就露餡只做第二層沒有解釋讀者不知道為什么要這么寫只做第三層沒有主干注釋就成了空中樓閣。1.3 翻譯過程中不該做的事踩了這么多篇的坑最重要的一條經(jīng)驗(yàn)是不要對代碼做“過度翻譯”。變量名、函數(shù)名、算法名盡量保留英文原文只翻譯注釋和輸出信息。有些剛?cè)胄械淖g者喜歡把contours改成“輪廓”把img改成“圖像”表面上很親切實(shí)際給讀者制造了大麻煩。因?yàn)榇a一旦和原文對不上找問題的時候會非?;靵y網(wǎng)上查資料也沒法對應(yīng)。另一件不該做的事是擅自改算法。原作者用了cv2.Canny(blurred, 30, 150)那譯稿里就必須保留這組閾值。翻譯不是改進(jìn)算法的場合讀者是為了復(fù)現(xiàn)原文效果來的參數(shù)一動結(jié)果就變了這不是翻譯該干的事。2. 環(huán)境搭建與工具選型解析2.1 隔離環(huán)境是效率的基礎(chǔ)做圖像處理內(nèi)容驗(yàn)證最忌諱在全局 Python 環(huán)境里亂裝包。我前二十篇就是在全局環(huán)境里直接跑的結(jié)果不同項(xiàng)目之間的依賴互相污染OpenCV 從 3.4 升到 4.5 之后之前驗(yàn)證過的代碼突然跑不通了排查了半天才發(fā)現(xiàn)是版本被頂?shù)袅?。后來所有篇目都改用?dú)立環(huán)境。Windows 上我用venvLinux 服務(wù)器上我也用venv偶爾需要記筆記時會用conda建環(huán)境但主力還是官方自帶的工具原因很簡單——依賴簡單、遷移方便、不容易出幺蛾子。創(chuàng)建環(huán)境的操作很簡單python -m venv pyimgsearch_99Windows 下激活pyimgsearch_99\Scripts\activateLinux 下激活source pyimgsearch_99/bin/activate每個篇目一個環(huán)境驗(yàn)證完這篇環(huán)境留著下一篇重新建。這樣即使兩篇文章依賴的 OpenCV 版本不同也不會互相影響。唯一要注意的是環(huán)境不要建在項(xiàng)目目錄里不然 Git 提交的時候會把幾十兆的依賴文件帶進(jìn)去。2.2 OpenCV 安裝最容易踩的兩個坑OpenCV 的安裝是讀者反饋?zhàn)疃嗟膯栴}幾乎每篇譯稿的評論區(qū)都會出現(xiàn)相關(guān)提問。最常見的兩個坑一個是裝錯包另一個是版本不匹配。PyPI 上有opencv-python和opencv-contrib-python兩個包前者只包含主模塊后者額外包含擴(kuò)展模塊。某些算法比如特征匹配用的SIFT在舊版本里需要擴(kuò)展模塊才可用直接裝opencv-python會導(dǎo)致AttributeError: module cv2 has no attribute xfeatures2d。我現(xiàn)在的做法是統(tǒng)一提示讀者安裝opencv-contrib-python這個包覆蓋了主模塊和擴(kuò)展模塊一次到位pip install opencv-contrib-python另一個坑是版本問題。OpenCV 的接口變動比較頻繁同一段代碼在 3.x 和 4.x 下的表現(xiàn)可能完全不同。翻譯時我特意在譯注明細(xì)里標(biāo)注“本文驗(yàn)證于 OpenCV 4.5.3”并在開頭加一段環(huán)境說明讓讀者先對齊版本再體驗(yàn)內(nèi)容。如果讀者用的是舊版 OpenCV跑第 96 篇那類涉及findContours的文章時大概率會看到返回值數(shù)量報錯not enough values to unpack (expected 3, got 2)這句話幾乎成了圖像處理新手的集體回憶。解決思路在 2.3 節(jié)里講這里先給出最穩(wěn)妥的安裝命令pip install opencv-contrib-python4.5.3.56固定具體版本比安裝最新版穩(wěn)定得多。2.3 imutils 這個庫到底要不要裝很多讀者第一次看到imutils是在某個輪廓檢測腳本里代碼寫的是cnts imutils.grab_contours(cnts)第一反應(yīng)是這什么庫沒見過啊。其實(shí)imutils不是 OpenCV 的一部分是一個第三方的圖像處理輔助庫專門用來簡化一些常見操作比如旋轉(zhuǎn)、裁剪、縮放、骨架化等。我最初覺得這個庫可有可無直到自己寫代碼時發(fā)現(xiàn)在不同 OpenCV 版本之間處理findContours的返回值實(shí)在煩人。OpenCV 3 里findContours返回三個值OpenCV 4 里返回兩個值代碼寫起來像這樣try: _, cnts, hierarchy cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) except ValueError: cnts, hierarchy cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)每篇都寫這段太累imutils 的grab_contours就是干這個的它會按照 OpenCV 的返回值數(shù)量自動適配cnts cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts imutils.grab_contours(cnts)翻譯這類博客時我建議統(tǒng)一依賴imutils理由有三個代碼簡潔、兼容多版本、讀者安裝負(fù)擔(dān)低。萬一讀者不想裝也可以在譯注里給出替代寫法但正文代碼用 imutils讓原文和譯文保持一致。3. 一個典型 Demo 的完整驗(yàn)證流程3.1 準(zhǔn)備測試圖與基礎(chǔ)讀取翻譯中有一篇關(guān)于“利用邊緣檢測和輪廓檢測統(tǒng)計圖中物體數(shù)量”的文章非常典型我拿它來演示完整的驗(yàn)證流程。準(zhǔn)備工作是找一張適合測試的圖。我一般會生成一張自己畫的簡單圖比如白底上有幾個黑色圓形也可以直接拍一張桌面上放幾本書的照片關(guān)鍵在于目標(biāo)物體和背景要對比明顯這樣邊緣檢測效果才直觀。讀取圖像是第一步這里藏著一個容易忽視的細(xì)節(jié)。cv2.imread默認(rèn)按 BGR 順序讀取彩色圖像很多新手接下來用cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)轉(zhuǎn)換灰度圖時會疑問為什么不是 RGB。這個問題在譯注里經(jīng)常出現(xiàn)我會特意說明OpenCV 的默認(rèn)顏色空間是 BGR不是 RGB轉(zhuǎn)灰度時用CV_BGR2GRAY而不是CV_RGB2GRAY。基礎(chǔ)讀取和灰度化代碼import cv2 import imutils image cv2.imread(objects.jpg) if image is None: raise ValueError(圖片讀取失敗請檢查路徑) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)如果加載后image是None九成是路徑問題。原文作者用圖片時通常寫在腳本同目錄中文讀者如果把圖路徑寫成絕對路徑的話Windows 下還要處理反斜杠轉(zhuǎn)義相當(dāng)麻煩。我統(tǒng)一建議把測試圖和腳本放在同一目錄用相對路徑。3.2 圖像預(yù)處理鏈路拿到灰度圖之后接下來不是直接做邊緣檢測而是先做降噪。這一步的原理是邊緣檢測對噪聲極其敏感一粒噪點(diǎn)就可能產(chǎn)生一整條偽邊緣所以要用高斯模糊把細(xì)膩紋理磨平blurred cv2.GaussianBlur(gray, (5, 5), 0)(5, 5)是高斯核的大小必須為正奇數(shù)0是標(biāo)準(zhǔn)差寫成 0 表示讓 OpenCV 根據(jù)核大小自動計算。核越大圖像越模糊邊緣越少但過大也會把真實(shí)邊緣一起抹掉所以不是越大越好。接著做 Canny 邊緣檢測兩個閾值分別是低閾值和高閾值edged cv2.Canny(blurred, 30, 150)低于低閾值的像素會被直接判定為非邊緣高于高閾值的像素會被判定為強(qiáng)邊緣介于兩者之間的像素只有在連接到強(qiáng)邊緣時才被保留。所以高低閾值決定了邊緣檢測的敏感程度。30 和 150 這組參數(shù)在背景簡單的情況下表現(xiàn)很好背景復(fù)雜時需要調(diào)低高閾值讓更多細(xì)邊緣保留下來。cv2.imshow可以直觀查看預(yù)處理每一步的結(jié)果cv2.imshow(Gray, gray) cv2.imshow(Blurred, blurred) cv2.imshow(Edged, edged) cv2.waitKey(0) cv2.destroyAllWindows()驗(yàn)證到這里我會停一下檢查每一步輸出圖片是否符合預(yù)期。如果邊緣圖出現(xiàn)了大量我們不想要的碎線就不會接著往下做了先回頭調(diào)整模糊核大小或 Canny 閾值直到邊緣圖干凈為止。這個過程比寫代碼本身更加重要。3.3 輪廓提取與結(jié)果可視化邊緣圖準(zhǔn)備好之后下一步是提取輪廓。這里就是前面提到的版本差異最容易爆發(fā)的地方直接按原文寫的cnts, hierarchy cv2.findContours(...)在 OpenCV 3 下運(yùn)行會直接拋出TypeError。我的驗(yàn)證流程中統(tǒng)一用 imutils 來兜底cnts cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts imutils.grab_contours(cnts)這里必須拷貝邊緣圖因?yàn)閒indContours會修改輸入圖像。如果不拷貝后面想再用edged做其他分析時會發(fā)現(xiàn)圖像內(nèi)容已經(jīng)被破壞了。拿到輪廓列表之后就可以統(tǒng)計數(shù)量并繪制外接矩形print(f檢測到 {len(cnts)} 個輪廓) for c in cnts: x, y, w, h cv2.boundingRect(c) cv2.rectangle(image, (x, y), (x w, y h), (0, 255, 0), 2)cv2.boundingRect計算輪廓的最小外接矩形cv2.rectangle在原始圖上繪制綠色矩形框。最后顯示結(jié)果cv2.imshow(Output, image) cv2.waitKey(0) cv2.destroyAllWindows()這個流程跑下來如果測試圖上有 5 個目標(biāo)物體終端就應(yīng)該輸出 5。這一步是整個驗(yàn)證流程的核心環(huán)節(jié)數(shù)字對不上就說明預(yù)處理環(huán)節(jié)還有問題不是輪廓提取寫錯了。3.4 回寫譯稿時的代碼規(guī)范驗(yàn)證通過之后代碼并不是直接粘貼進(jìn)譯稿就行還需要做幾項(xiàng)規(guī)范化處理。第一去掉和邏輯無關(guān)的調(diào)試代碼。比如上面cv2.imshow(Gray, gray)這行在驗(yàn)證階段很有用在正式譯文里就沒必要保留了不然讀者每一步都要手動關(guān)窗口非常影響體驗(yàn)。但我會在正文里用文字描述“建議在每一步后使用cv2.imshow查看中間結(jié)果”把調(diào)試的意識教給讀者而不把調(diào)試代碼留在正文。第二補(bǔ)充異常處理。原文章節(jié)里往往不做image is None的判斷但譯稿里我會加上因?yàn)橹形淖x者經(jīng)常遇到中文路徑、文件名拼寫錯誤等問題。多寫一行判斷能讓讀者遇到問題時立刻定位到“路徑不對”而不是陷入別的困惑。第三統(tǒng)一縮進(jìn)和引號風(fēng)格。原文可能混合雙引號單引號、2 空格 4 空格縮進(jìn)我都會統(tǒng)一成 4 空格縮進(jìn)和單引號讓代碼風(fēng)格收斂。第四代碼塊的注釋保留英文還是翻譯成中文我的選擇是翻譯但保留原文變量名。注釋是給人看的翻譯注釋也是本地化的一部分。比如# loop over the contours譯成# 遍歷所有輪廓讀者理解起來更直接。4. 常見問題與排查技巧實(shí)錄4.1 中文路徑導(dǎo)致圖像讀取失敗運(yùn)行cv2.imread(測試圖.jpg)返回None是評論區(qū)出現(xiàn)頻率最高的問題沒有之一。這不是圖像本身的問題而是 OpenCV 的imread在 Windows 下對中文路徑處理得不好底層用的是窄字節(jié)字符串 API遇到非 ASCII 字符就容易失敗。第一種解決方案是改用imdecodeimport numpy as np import cv2 def imread_unicode(path): stream open(path, rb) bytes bytearray(stream.read()) numpy_array np.asarray(bytes, dtypenp.uint8) return cv2.imdecode(numpy_array, cv2.IMREAD_COLOR)第二種方案更簡單就是讓讀者把所有路徑都改成英文。我自己驗(yàn)證時也用英文路徑但考慮到很多讀者把測試圖片放在桌面的中文文件夾里我會在譯注里附上imread_unicode這個函數(shù)方便他們照用。處理路徑問題直接改代碼比要求讀者改變習(xí)慣更有效率。4.2 OpenCV 版本差異引發(fā)的接口報錯findContours的返回值差異只是冰山一角還有幾個常見差異值得記錄。cv2.drawContours的調(diào)用方式在不同版本之間沒有太大變化但cnts的數(shù)據(jù)類型在不同版本中可能不一樣某些老代碼里會先調(diào)用cnts[0]取輪廓列表新版本直接返回一個列表再取[0]就變成了第一條輪廓而不是輪廓列表導(dǎo)致畫出來只有一兩個圖形。解決辦法是在譯稿開頭寫明“本文代碼基于 OpenCV 4.x 驗(yàn)證”并給出統(tǒng)一兼容寫法。用imutils.grab_contours就能消解掉返回值差異省去一堆try ... except。4.3 顯示窗口出現(xiàn)亂碼或空白cv2.imshow(結(jié)果, image)運(yùn)行后窗口標(biāo)題亂碼這個常見于 Windows 控制臺環(huán)境。原因是 OpenCV 的imshow窗口標(biāo)題使用系統(tǒng)編碼中文在這種環(huán)境下不支持。驗(yàn)證時用中文標(biāo)題出現(xiàn)過亂碼之后我的處理方法是正文代碼統(tǒng)一用英文窗口標(biāo)題譯注里再加一句“窗口標(biāo)題建議用英文否則可能亂碼”。另一種更隱蔽的問題是cv2.imshow之后畫面卡住不動或者一閃而過。前者通常是waitKey(0)沒寫后者通常是窗口顯示后立即被銷毀。cv2.waitKey(0)會一直等待鍵盤輸入是顯示靜態(tài)圖片的標(biāo)準(zhǔn)寫法不能省。4.4 原文代碼依賴舊版 Python 的寫法PyImgSearch 系列的早期文章有不少是為 Python 2 時代寫的代碼特征是大量使用print語句print number of contours: %d % (len(cnts))這行代碼在 Python 3 下直接報語法錯誤。翻譯時我會統(tǒng)一升級為print(fnumber of contours: {len(cnts)})順手把xrange改成range把.iteritems()改成.items()這屬于老代碼移植的例行工作。翻譯時改這種代碼不需要和原作者商量因?yàn)樽x者用的是 Python 3遷就老語法沒有意義。處理這類問題時我養(yǎng)成了一個習(xí)慣每篇譯稿在代碼塊下方都附帶一行“環(huán)境說明”注明 Python 版本、OpenCV 版本、imutils 版本。讀者復(fù)現(xiàn)時報錯第一件事就是對比環(huán)境說明。5. 術(shù)語統(tǒng)一與發(fā)布前審查清單5.1 高頻術(shù)語翻譯對照表圖像處理領(lǐng)域的術(shù)語翻譯比較混亂同一個詞在不同博文里可能有好幾種譯法。為了避免整個系列自相矛盾我從第五十篇開始建立了術(shù)語表到目前為止已經(jīng)積累了五十多條。這里挑最常用的列出來英文術(shù)語統(tǒng)一譯法備注blurring模糊/平滑兩者通用指向高斯模糊時優(yōu)先“平滑”thresholding閾值化不譯作“二值化”避免與 binary 混淆contour輪廓不譯作“邊緣”邊緣是 edgeedge detection邊緣檢測固定譯法image pyramid圖像金字塔固定譯法feature extraction特征提取固定譯法sliding window滑窗不譯作“滑動窗口算法”non-maxima suppression非極大值抑制縮寫 NMS首次出現(xiàn)時給出全稱histogram直方圖固定譯法convolution卷積固定譯法kernel卷積核單獨(dú)出現(xiàn)時可譯“核”padding填充固定譯法stride步長固定譯法anchor錨點(diǎn)固定譯法descriptor描述子不譯作“描述符”與操作系統(tǒng)概念區(qū)分keypoint關(guān)鍵點(diǎn)固定譯法ROI感興趣區(qū)域首次出現(xiàn)給出全稱 Region of Interestaspect ratio寬高比也可譯“縱橫比”二選一統(tǒng)一morphological operations形態(tài)學(xué)操作固定譯法erosion / dilation腐蝕 / 膨脹固定譯法每次翻譯新稿之前我會先對照術(shù)語表查一遍這篇涉及哪些高頻詞翻譯時直接套用。寧可花兩分鐘查表也不要臨時發(fā)明新譯法術(shù)語不統(tǒng)一是長系列項(xiàng)目最致命的慢性病。5.2 發(fā)布前的檢查順序發(fā)布一篇譯稿之前我會按照固定順序過一遍每一步都有明確目的。第一步代碼運(yùn)行檢查。從頭到尾把每段代碼在獨(dú)立環(huán)境里執(zhí)行一遍確認(rèn)沒有報錯結(jié)果圖和預(yù)期一致。這一步發(fā)現(xiàn)問題就直接改改完必須重新完整跑一遍不能只跑修改過的片段。第二步術(shù)語一致性檢查。用搜索功能全文檢索術(shù)語表里的詞確認(rèn)沒有遺漏或混用。這個檢查可以寫個小腳本把“模糊”和“平滑”在檢索結(jié)果里翻一遍看上下文是否符合語境。第三步代碼塊規(guī)范性檢查。確認(rèn)所有代碼塊都是完整可復(fù)制的沒有省略號沒有不匹配的括號注釋都是中文函數(shù)名都是原文。第四步圖片鏈接檢查。確認(rèn)文中引用的圖片路徑存在圖片能正常打開cv2.imread能讀到有效圖像。第五步版本提示檢查。確認(rèn)文章開頭有環(huán)境說明譯注中涉及差異的部分都標(biāo)注了適用版本。第六步閱讀流暢度檢查。從頭到尾默讀一遍重點(diǎn)關(guān)注連接詞是否生硬公式轉(zhuǎn)述是否清晰有沒有明顯的直譯腔。通常這一步會順手改掉十幾個句子。這套流程走完一篇譯稿從啟動到發(fā)布大概需要四到六個小時其中驗(yàn)證代碼和修 bug 占一半以上。越到后期越覺得翻譯的功夫其實(shí)花在代碼上文字部分反而順暢。寫在系列第九十九篇之后做到現(xiàn)在這個篇數(shù)我對技術(shù)內(nèi)容中文化的理解已經(jīng)和最初完全不同了。最初以為翻譯是把一種語言轉(zhuǎn)換成另一種語言現(xiàn)在更愿意把它理解成“讓內(nèi)容在新的環(huán)境里重新有效”。圖像處理代碼尤其明顯一段代碼在原作者的環(huán)境里跑得好好的換到讀者這邊的版本、系統(tǒng)、路徑就可能廢掉。所以每一篇譯稿里我做的其實(shí)不完全是翻譯而是幫讀者把實(shí)驗(yàn)環(huán)境“翻譯”了過來。個人經(jīng)驗(yàn)是做這類工作最忌諱趕進(jìn)度寧可一篇慢一點(diǎn)也要保證讀者閉著眼睛照做就能跑出結(jié)果。第九十九篇做完我已經(jīng)不記得到底改了多少個findContours的報錯又加了多少條imread的判斷但有一件事是確定的技術(shù)內(nèi)容的價值最終要看它能不能讓別人用起來而不是看它寫得有多華麗。