戰(zhàn):從預(yù)處理到特征匹配的算法路線與調(diào)參心法)
1. 從零搭建圖像處理直覺先搞清楚OpenCV到底在算什么很多朋友一開始學(xué)OpenCV就扎進(jìn)各種函數(shù)的海洋里今天調(diào)一個(gè)cv2.cvtColor明天試一下cv2.Canny函數(shù)都跑通了但遇到真實(shí)項(xiàng)目還是懵。我最初也是這么過來的后來才意識(shí)到問題出在哪OpenCV不是一堆孤立函數(shù)的集合而是一套從像素出發(fā)解決問題的方法論。每個(gè)算法本質(zhì)上都在做同一件事——把一張由成百上千萬(wàn)個(gè)數(shù)字組成的像素矩陣逐步變成對(duì)我們有意義的信息。比如灰度化是把三維顏色壓縮成一維亮度濾波是在像素和鄰居之間做加權(quán)平均邊緣檢測(cè)是在找亮度變化最劇烈的那條分界線。想通這一點(diǎn)之后再看算法就非常順了它不是在處理圖片而是在用數(shù)學(xué)手段回答這張圖里哪部分值得關(guān)注。這篇總結(jié)并不是把OpenCV官方文檔的算法目錄抄一遍而是按我在實(shí)際項(xiàng)目里最常用的路線來梳理。你可能是剛?cè)腴T不久的學(xué)生可能是在做畢業(yè)設(shè)計(jì)或競(jìng)賽也可能是工作中突然要接手圖像相關(guān)模塊的開發(fā)者。無(wú)論哪種情況我的建議都是不要按官方文檔的順序?qū)W按預(yù)處理 → 特征提取 → 結(jié)構(gòu)分析 → 實(shí)際調(diào)優(yōu)這條主線學(xué)效率會(huì)高很多。下面每一章都會(huì)先講明白算法在解決什么問題再給關(guān)鍵參數(shù)和使用場(chǎng)景最后補(bǔ)充一些文檔里不會(huì)寫的實(shí)戰(zhàn)經(jīng)驗(yàn)。2. 圖像預(yù)處理三件套灰度化、濾波、直方圖均衡化2.1 灰度化為什么是第一步而不是可選項(xiàng)先說一個(gè)幾乎所有OpenCV教程都默認(rèn)你懂、但沒人真正講透的問題為什么大多數(shù)算法都要從彩色圖轉(zhuǎn)成灰度圖原因不光是計(jì)算量小了三倍更本質(zhì)的是很多算法在數(shù)學(xué)定義上就是建立在單通道上的。比如邊緣檢測(cè)的核心是計(jì)算亮度梯度——彩色圖有三個(gè)通道每個(gè)通道都有各自的梯度它們之間怎么加權(quán)合成反而會(huì)引入干擾。拿一個(gè)實(shí)際例子來說。我之前處理一批從外部設(shè)備抓取的工業(yè)零件圖像光源偏黃整個(gè)畫面白平衡不對(duì)。直接跑彩色圖的邊緣檢測(cè)黃光在金屬表面的反射會(huì)造成大量偽邊緣。轉(zhuǎn)成灰度后雖然白平衡問題依然存在但至少梯度計(jì)算只針對(duì)亮度單一維度干擾源少了一個(gè)。再用下面的濾波和均衡化把亮度分布拉平問題基本就解決了。import cv2 img cv2.imread(industrial_part.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)2.2 濾波選型均值、高斯、中值到底差在哪濾波是所有預(yù)處理里最考驗(yàn)經(jīng)驗(yàn)感的一步因?yàn)樗谋举|(zhì)是模糊而模糊多少、怎么模糊直接影響后面所有算法的輸入質(zhì)量。均值濾波cv2.blur窗口內(nèi)所有像素取平均值。速度快但對(duì)噪聲敏感因?yàn)樵肼朁c(diǎn)也被平均進(jìn)去了。高斯濾波cv2.GaussianBlur按距離給窗口內(nèi)像素賦權(quán)重離中心越近權(quán)重越大。這是用得最廣的因?yàn)樗献匀粓D像中相鄰像素相關(guān)性隨距離衰減的規(guī)律。絕大多數(shù)場(chǎng)景先試它準(zhǔn)沒錯(cuò)。中值濾波cv2.medianBlur取窗口內(nèi)像素的中位數(shù)。這是去椒鹽噪聲黑白孤點(diǎn)噪聲的利器但對(duì)邊緣細(xì)節(jié)的保留也最差。我自己在項(xiàng)目里的選型經(jīng)驗(yàn)是不知道噪聲類型時(shí)優(yōu)先高斯濾波。它能在去噪和保留邊緣之間取得一個(gè)比較穩(wěn)妥的平衡。如果圖像里明顯有黑白顆粒狀的噪聲就換中值濾波如果對(duì)速度要求極高且圖像質(zhì)量本身不錯(cuò)才考慮均值濾波。這里有一個(gè)我踩過的坑——高斯濾波的核大小ksize必須是正奇數(shù)而且核越大圖像越模糊但不是越大越好。對(duì)一張1080p的圖像我用(5, 5)的核做預(yù)處理邊緣檢測(cè)效果最好用(11, 11)之后細(xì)小的邊緣直接被抹平了后面怎么調(diào)閾值都救不回來。核大小要跟目標(biāo)特征尺寸匹配這是個(gè)需要反復(fù)試的經(jīng)驗(yàn)值。2.3 直方圖均衡化把對(duì)比度拉滿的數(shù)學(xué)原理直方圖均衡化是我早期最容易忽略、后來覺得真香的一個(gè)算法。它的原理一句話能講清楚把像素值的分布從集中在某個(gè)區(qū)間拉伸到均勻分布在整個(gè)0-255范圍。但為什么這樣能增強(qiáng)對(duì)比度因?yàn)槿搜酆退惴▽?duì)細(xì)節(jié)的感知都依賴于亮度差異——如果整張圖的灰度值都堆在100到120之間那這20個(gè)級(jí)別的差異肉眼幾乎看不出區(qū)別拉開到0到255之后原本微小的差異就變成明顯可辨的了。# 全局直方圖均衡化 equ cv2.equalizeHist(gray) # CLAHE限制對(duì)比度自適應(yīng)直方圖均衡化 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) clahe_img clahe.apply(gray)在實(shí)際項(xiàng)目中我?guī)缀醪挥胏v2.equalizeHist處理照片因?yàn)槿志饣瘜?duì)光照不均勻的圖效果很差——它會(huì)把整體亮度的變化也當(dāng)成細(xì)節(jié)去增強(qiáng)導(dǎo)致畫面發(fā)灰、過曝感嚴(yán)重。更實(shí)用的是CLAHE自適應(yīng)直方圖均衡化它把圖像分成若干個(gè)小塊tile每個(gè)塊內(nèi)單獨(dú)做均衡化再用插值把塊之間的邊界做平滑。這樣能保留局部的細(xì)節(jié)增強(qiáng)效果又不會(huì)讓全局亮度失真。舉個(gè)真實(shí)案例我處理過一批室外拍攝的交通指示牌圖像白天強(qiáng)光下牌子亮部過曝、暗部欠曝。用全局均衡化后整張圖變得灰蒙蒙的換成CLAHE之后牌面上的文字和圖案細(xì)節(jié)都清晰了不少。clipLimit這個(gè)參數(shù)控制對(duì)比度增強(qiáng)的強(qiáng)度我一般設(shè)2.0到3.0之間太大會(huì)出現(xiàn)水彩畫效果顏色過度飽和太小則跟沒處理一樣。3. 邊緣檢測(cè)算法Sobel、Laplacian、Canny的共性與差異3.1 邊緣的本質(zhì)是變化率不是邊界線很多新手對(duì)邊緣檢測(cè)有個(gè)誤區(qū)以為它是像人的眼睛一樣看到物體的輪廓。實(shí)際上邊緣檢測(cè)算法全是在做同一件事——計(jì)算像素亮度值的導(dǎo)數(shù)變化率。平坦區(qū)域的像素變化率接近0不是邊緣變化劇烈的地方導(dǎo)數(shù)大就是候選邊緣。理解這一點(diǎn)之后再看各種算子就不亂了Sobel算子計(jì)算一階導(dǎo)數(shù)區(qū)分水平方向和垂直方向分別用cv2.Sobel的dx和dy參數(shù)控制。因?yàn)橹凰懔艘浑A它對(duì)噪聲比較敏感但實(shí)現(xiàn)簡(jiǎn)單、速度快。Laplacian算子計(jì)算二階導(dǎo)數(shù)。二階導(dǎo)數(shù)為0的點(diǎn)對(duì)應(yīng)一階導(dǎo)數(shù)的極值也就是邊緣的位置。它對(duì)孤立噪聲點(diǎn)特別敏感噪聲點(diǎn)的二階導(dǎo)數(shù)極大所以一般要先做高斯模糊再用。Canny算子不是單一算子而是一個(gè)多階段流程——先用高斯濾波去噪再算梯度幅值和方向然后做非極大值抑制把梯度方向上不是局部最大值的點(diǎn)去掉最后用雙閾值連接邊緣。3.2 為什么實(shí)戰(zhàn)中Canny是默認(rèn)首選我自己在項(xiàng)目里90%的場(chǎng)景直接選Canny不是因?yàn)樗呒?jí)而是因?yàn)樗堰吘墮z測(cè)變成了一個(gè)可調(diào)參的工程問題。Sobel的閾值和核大小對(duì)結(jié)果的影響不太好控制Canny的兩個(gè)閾值threshold1和threshold2給了你一個(gè)明確的調(diào)參維度小于threshold1的梯度不算邊緣大于threshold2的必算邊緣兩者之間的按連通性決定是否保留。# Canny邊緣檢測(cè) edges cv2.Canny(blurred, threshold150, threshold2150)這段代碼里50和150的比例經(jīng)驗(yàn)值是1:2到1:3之間。threshold1設(shè)得低邊緣會(huì)多但噪聲也多設(shè)得高邊緣干凈但一些小細(xì)節(jié)可能丟失。調(diào)參的起點(diǎn)我一般是先跑一遍看結(jié)果然后盯著圖像里想保留的最弱邊緣來反推threshold1。比如希望保留一條細(xì)縫的邊緣而它的梯度幅值在60左右那threshold1就設(shè)在50上下留一點(diǎn)余量。3.3 邊緣檢測(cè)前要不要先濾波這是一個(gè)先有雞還是先有蛋的問題Canny內(nèi)部自帶高斯濾波所以很多人會(huì)忽略提前濾波這一步。但我在實(shí)踐中發(fā)現(xiàn)處理真實(shí)噪聲較多的圖像時(shí)先對(duì)原圖做一次中值濾波或高斯濾波再進(jìn)Canny效果遠(yuǎn)比只用Canny內(nèi)建濾波好。原因在于Canny內(nèi)置的高斯濾波核大小是固定的而不同圖像的噪聲尺度差異很大提前濾波可以針對(duì)性地消除特定尺度的噪聲。需要強(qiáng)調(diào)的是這篇博文里的所有示例、參數(shù)和調(diào)優(yōu)思路都來自我在模擬項(xiàng)目和公開數(shù)據(jù)集上的復(fù)現(xiàn)經(jīng)驗(yàn)不是某個(gè)具體商業(yè)產(chǎn)品的參數(shù)。圖像處理本身就是三分算法、七分調(diào)參的活兒參數(shù)要跟著你的數(shù)據(jù)和目標(biāo)走。4. 形態(tài)學(xué)操作腐蝕、膨脹、開運(yùn)算、閉運(yùn)算以及它們?nèi)绾涡蘩矶祱D4.1 形態(tài)學(xué)不是在處理圖像是在處理形狀形態(tài)學(xué)操作處理的輸入一般是經(jīng)過閾值分割或邊緣檢測(cè)后得到的二值圖只有0和255兩種值。它的核心操作只有兩個(gè)腐蝕和膨脹。其他所有操作都是這兩個(gè)的組合。用最直觀的話解釋腐蝕用一個(gè)結(jié)構(gòu)元素核掃過圖像只有當(dāng)核完全落在白色區(qū)域內(nèi)時(shí)中心點(diǎn)才保留白色。效果是白色區(qū)域縮小一圈能去掉小塊的白色噪點(diǎn)。膨脹只要核與白色區(qū)域有交集中心點(diǎn)就變白。效果是白色區(qū)域擴(kuò)大一圈能填補(bǔ)空洞、連接斷開的區(qū)域。4.2 開運(yùn)算和閉運(yùn)算解決去噪與補(bǔ)洞這對(duì)矛盾開運(yùn)算 先腐蝕后膨脹。它能消除白色區(qū)域周圍的小凸起和孤立噪點(diǎn)但整體形狀大小基本不變。我第一次用開運(yùn)算處理二值圖時(shí)驚訝于它能把被打碎的字符邊緣修得干干凈凈而字符的粗細(xì)幾乎沒變化。閉運(yùn)算 先膨脹后腐蝕。它能填補(bǔ)白色區(qū)域內(nèi)部的小孔洞、連接斷裂的筆畫。比如檢測(cè)印刷體文字時(shí)如果字體內(nèi)部有白色小點(diǎn)干擾連通域分析用閉運(yùn)算就能解決。import cv2 import numpy as np kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 開運(yùn)算消除白色小噪點(diǎn) opening cv2.morphologyEx(binary_img, cv2.MORPH_OPEN, kernel) # 閉運(yùn)算填補(bǔ)白色區(qū)域內(nèi)部空洞 closing cv2.morphologyEx(binary_img, cv2.MORPH_CLOSE, kernel)核大小的選擇直接影響效果。我用(5, 5)的核做開運(yùn)算把一些細(xì)小的印刷噪點(diǎn)去掉了但當(dāng)我換成(9, 9)的核發(fā)現(xiàn)字符筆畫的主體也開始被吞噬。結(jié)構(gòu)元素尺寸應(yīng)略小于你想保留的最小特征尺寸——這是我在多次調(diào)整后總結(jié)的一句話放這里最合適。4.3 形態(tài)學(xué)梯度被很多人忽略的邊緣增強(qiáng)神器形態(tài)學(xué)梯度 膨脹結(jié)果 減 腐蝕結(jié)果。它提取的是二值圖的前景邊緣效果類似于邊緣檢測(cè)但因?yàn)橹换谛螒B(tài)學(xué)操作速度非常快而且對(duì)粗細(xì)變化不敏感。在需要快速提取形狀外輪廓的場(chǎng)景比如實(shí)時(shí)視頻目標(biāo)跟蹤的預(yù)處理形態(tài)學(xué)梯度比Canny更省計(jì)算資源。gradient cv2.morphologyEx(binary_img, cv2.MORPH_GRADIENT, kernel)這段代碼在很多教科書里只是被提了一句但實(shí)際用起來效果出奇地穩(wěn)定。我把形態(tài)學(xué)梯度作為檢查二值圖質(zhì)量的輔助手段——如果梯度結(jié)果里出現(xiàn)了大量雜散的孤立邊緣說明二值圖噪聲偏多需要回頭調(diào)整閾值分割的參數(shù)。5. 輪廓檢測(cè)與幾何特征計(jì)算從一堆點(diǎn)組成有意義的對(duì)象5.1findContours的返回值陷阱OpenCV版本差異必須搞清輪廓檢測(cè)是圖像處理中最常用的結(jié)構(gòu)化工具它能把二值圖中連通的白色區(qū)域提取成一組點(diǎn)的集合輪廓從這以后圖像處理就從像素操作升級(jí)為幾何分析。但這里有一個(gè)非常經(jīng)典的坑。OpenCV的cv2.findContours在OpenCV 3.x和4.x中返回值的數(shù)量不同老版本返回3個(gè)值image, contours, hierarchy新版本返回2個(gè)值contours, hierarchy。# 適用于OpenCV 4.x的寫法 contours, hierarchy cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)如果從網(wǎng)上復(fù)制了老版本的代碼直接跑多半會(huì)報(bào)ValueError: not enough values to unpack。這類語(yǔ)法層面的差異最煩人因?yàn)閳?bào)錯(cuò)和實(shí)際算法無(wú)關(guān)純粹是版本適配問題。另外兩個(gè)關(guān)鍵參數(shù)也值得展開。cv2.RETR_EXTERNAL只提取最外層的輪廓忽略內(nèi)層洞比如一個(gè)圓環(huán)只取外圓不取內(nèi)圓。適合目標(biāo)彼此分離的場(chǎng)景。cv2.RETR_CCOMP和cv2.RETR_TREE提取所有層級(jí)的輪廓適合處理有嵌套結(jié)構(gòu)的圖像比如文字內(nèi)還有小洞的情況。cv2.CHAIN_APPROX_SIMPLE壓縮水平、垂直和對(duì)角方向的輪廓點(diǎn)只保留端點(diǎn)能大幅減少點(diǎn)數(shù)。如果不壓縮cv2.CHAIN_APPROX_NONE輪廓點(diǎn)數(shù)巨大后續(xù)計(jì)算復(fù)雜度也高。5.2 輪廓篩選的實(shí)用套路面積、長(zhǎng)寬比、凸包一個(gè)都不能少提取到輪廓之后下一步通常不是直接用而是篩選。我整理了最常用的三個(gè)篩選維度面積cv2.contourArea過濾掉太小或太大的噪聲區(qū)域。比如檢測(cè)工件上的螺絲孔時(shí)面積小于20像素的輪廓基本可以斷定是灰塵或噪點(diǎn)。寬高比cv2.boundingRect用外接矩形寬高比排除不符合長(zhǎng)寬約束的輪廓。比如檢測(cè)車牌區(qū)域字符輪廓的寬高比一般在一個(gè)固定范圍內(nèi)超出范圍的直接丟棄。凸包c(diǎn)v2.convexHull判斷輪廓是否近似凸形。對(duì)檢測(cè)圓形、矩形等規(guī)則目標(biāo)時(shí)很有用——偏離凸形程度大的輪廓大概率不是目標(biāo)。areas [cv2.contourArea(c) for c in contours] # 按面積從大到小排序取前N個(gè) sorted_contours sorted(contours, keycv2.contourArea, reverseTrue)[:N]5.3 形狀匹配與輪廓近似當(dāng)你要判斷它是不是一個(gè)圓時(shí)輪廓近似的本質(zhì)是用更少的點(diǎn)去逼近原始輪廓關(guān)鍵是cv2.approxPolyDP的epsilon參數(shù)。這個(gè)參數(shù)表示近似結(jié)果與原始輪廓之間的最大距離值越大近似后的點(diǎn)多邊形越粗糙但也越精簡(jiǎn)。epsilon 0.01 * cv2.arcLength(contour, True) approx cv2.approxPolyDP(contour, epsilon, True) if len(approx) 3: shape triangle elif len(approx) 4: shape rectangle elif len(approx) 5: shape pentagon else: # 一般用圓度來判斷 area cv2.contourArea(contour) perimeter cv2.arcLength(contour, True) circularity 4 * np.pi * area / (perimeter * perimeter) if circularity 0.85: shape circle這段代碼展示了一個(gè)很常見的業(yè)務(wù)邏輯檢測(cè)物體是三角形、矩形、五邊形還是圓形。circularity圓度是一個(gè)非常經(jīng)典的幾何度量值在0到1之間越接近1說明形狀越接近圓。這個(gè)方法我在做形狀識(shí)別的小工具時(shí)反復(fù)用過它的優(yōu)勢(shì)是不依賴機(jī)器學(xué)習(xí)模型純幾何計(jì)算快且直觀。5.4 坐標(biāo)排序的細(xì)節(jié)做OCR項(xiàng)目前必須掌握的定位順序輪廓檢測(cè)在OCR項(xiàng)目里經(jīng)常用來定位單個(gè)字符但有個(gè)問題檢測(cè)到的輪廓順序完全取決于圖像掃描的順序不一定是按閱讀順序從左到右、從上到下排列的。我早期做OCR demo時(shí)就栽在這里——輪廓順序一亂最后拼出來的文本順序完全不對(duì)。解決辦法是用外接矩形框的中心點(diǎn)坐標(biāo)先按y坐標(biāo)排序從上到下再把同一行內(nèi)按x坐標(biāo)排序從左到右。rectangles [cv2.boundingRect(c) for c in contours] # 按 y 排序然后按 x 排序 sorted_rects sorted(rectangles, keylambda r: (r[1] // h, r[0]))這里對(duì)y除以行高h(yuǎn)再取整是為了把同一行內(nèi)的字符歸為一組避免因?yàn)檩p微的傾斜導(dǎo)致同一行的字符y坐標(biāo)有差異而被拆成多行。這個(gè)技巧對(duì)傾斜校正后的圖像尤其有效。6. 特征點(diǎn)檢測(cè)與匹配ORB、SIFT同臺(tái)競(jìng)技怎么選6.1 特征點(diǎn)到底特征在哪里邊緣檢測(cè)關(guān)注的是整條線特征點(diǎn)檢測(cè)關(guān)注的是圖像中獨(dú)特且容易被再次找到的局部位置。一個(gè)典型的特征點(diǎn)通常是角點(diǎn)、紋理豐富的區(qū)域中心、或者在光照變化下依然穩(wěn)定的圖像塊。特征點(diǎn)檢測(cè)算法的輸出一般包括三樣?xùn)|西特征點(diǎn)的位置x, y、描述子一個(gè)向量用來描述這個(gè)點(diǎn)周圍區(qū)域的紋理特征、以及匹配時(shí)需要的距離度量。描述子的意義在于——當(dāng)你需要判斷兩張圖中的兩個(gè)點(diǎn)是否對(duì)應(yīng)同一位置時(shí)只需比較它們的描述子是否足夠相似。6.2 ORB vs SIFT授權(quán)問題之外性能差異才是關(guān)鍵SIFT尺度不變特征變換在特征點(diǎn)檢測(cè)里的地位不用多說它能在圖像縮放、旋轉(zhuǎn)、光照變化下保持很強(qiáng)的穩(wěn)定性。但它有一個(gè)不少人都知道的痛點(diǎn)SIFT算法是有專利限制的在商業(yè)應(yīng)用中需要授權(quán)。ORBOriented FAST and Rotated BRIEF就是為了解決這個(gè)問題而生的完全開源免費(fèi)。但除了授權(quán)問題速度差異也是關(guān)鍵。我之前在一個(gè)實(shí)時(shí)圖像匹配的小項(xiàng)目里對(duì)比過算法耗時(shí)毫秒/幀匹配穩(wěn)定性適用場(chǎng)景ORB15-30中等實(shí)時(shí)預(yù)覽、嵌入式設(shè)備、快速匹配SIFT80-200高離線匹配、高精度拼接、物體識(shí)別從表格能直觀看出ORB速度優(yōu)勢(shì)極大SIFT穩(wěn)定性優(yōu)勢(shì)極大。如果你做的是實(shí)時(shí)視頻幀匹配ORB是首選如果做的是圖像拼接這類對(duì)精度要求極高的離線任務(wù)SIFT更穩(wěn)妥。# ORB特征提取與匹配 orb cv2.ORB_create(nfeatures500) kp1, des1 orb.detectAndCompute(img1, None) kp2, des2 orb.detectAndCompute(img2, None) bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) matches bf.match(des1, des2) matches sorted(matches, keylambda x: x.distance) # 過濾匹配取距離最近的前N個(gè) good_matches matches[:40]注意ORB的描述子距離用cv2.NORM_HAMMING漢明距離因?yàn)镺RB生成的是二進(jìn)制描述子SIFT用cv2.NORM_L2歐式距離。這個(gè)細(xì)節(jié)很多人會(huì)忽略——匹配距離度量沒用對(duì)結(jié)果可能完全沒法看。6.3 用單應(yīng)性矩陣過濾誤匹配讓結(jié)果穩(wěn)下來的關(guān)鍵一步特征匹配的結(jié)果里總會(huì)有錯(cuò)誤匹配尤其是當(dāng)兩張圖存在大量相似紋理時(shí)。一個(gè)非常實(shí)用的過濾思路是用cv2.findHomography配合RANSAC算法在匹配點(diǎn)中找出一個(gè)能解釋大多數(shù)匹配的透視變換矩陣而那些不符合這個(gè)變換的匹配點(diǎn)就是離群點(diǎn)可以被剔除。# 從匹配點(diǎn)中提取坐標(biāo) src_pts np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) dst_pts np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) # RANSAC 計(jì)算單應(yīng)性矩陣并過濾誤匹配 M, mask cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) matches_mask mask.ravel().tolist()RANSAC的第二個(gè)參數(shù)是重投影誤差閾值默認(rèn)我習(xí)慣設(shè)5.0。這個(gè)值越小要求匹配點(diǎn)越精確過濾越嚴(yán)太嚴(yán)會(huì)丟掉有效匹配太松又會(huì)混入誤匹配。這是一個(gè)需要根據(jù)匹配質(zhì)量動(dòng)態(tài)調(diào)整的平衡參數(shù)我第一次用時(shí)調(diào)了不下十次才找到合適的值。7. 閾值分割的進(jìn)階之路全局閾值、自適應(yīng)閾值與Otsu方法7.1 閾值分割的三個(gè)層次對(duì)應(yīng)三種圖像條件閾值分割的目標(biāo)是把灰度圖變成二值圖也就是前景vs背景。最簡(jiǎn)單的是全局固定閾值——整張圖用一個(gè)固定的t值大于t的設(shè)為白色小于t的設(shè)為黑色。全局閾值對(duì)光照均勻的圖像有效但一旦遇到光線從左到右漸變、或者局部有陰影的圖像就會(huì)出問題。這時(shí)候需要用自適應(yīng)閾值cv2.adaptiveThreshold它對(duì)每個(gè)像素都單獨(dú)計(jì)算一個(gè)閾值依據(jù)是它周圍一個(gè)小窗口內(nèi)的像素的加權(quán)平均值或高斯加權(quán)和。這樣能自動(dòng)適配局部的亮度變化。第三個(gè)層次是Otsu方法cv2.THRESH_OTSU。它不是另一種閾值而是自動(dòng)幫你選最優(yōu)閾值的方法。它的原理是用統(tǒng)計(jì)方法找到一個(gè)閾值使得前景和背景兩類的類內(nèi)方差最小、類間方差最大。簡(jiǎn)單說它是全局閾值的最優(yōu)解。# Otsu自動(dòng)閾值 ret, thresh_otsu cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 自適應(yīng)閾值 adaptive_thresh cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 )這里有兩個(gè)參數(shù)需要重點(diǎn)解釋。blockSize決定計(jì)算每個(gè)像素閾值時(shí)使用的鄰域大小必須為奇數(shù)一般從11到15之間開始試C是一個(gè)常數(shù)表示最終閾值用鄰域均值減去這個(gè)常數(shù)C越大二值化后保留的白色像素越少。7.2 三種方法怎么選給出我的決策流程我的經(jīng)驗(yàn)是拿到灰度圖后先試全局閾值看一下整體情況如果前景和背景對(duì)比足夠強(qiáng)直接Otsu就行。如果有明顯的漸變陰影用自適應(yīng)閾值。如果兩種情況混合存在比如一張圖里既有強(qiáng)光區(qū)又有暗角優(yōu)先考慮先做直方圖均衡化再用Otsu均衡化能把亮度分布拉平讓Otsu的統(tǒng)計(jì)效果更好。我自己在實(shí)際測(cè)試中遇到過一個(gè)有意思的情況一張掃描文檔背景是淺黃色文字是深灰色光照均勻但背景有紋路。Otsu全局閾值的效果不太理想因?yàn)榧y路的灰度值和文字差距不夠大。用了自適應(yīng)閾值之后因?yàn)樗诰植坑?jì)算閾值背景紋路被完全忽略了只留下了文字輪廓。自適應(yīng)閾值在處理背景不均勻但前景是局部深色結(jié)構(gòu)的場(chǎng)景時(shí)優(yōu)勢(shì)非常明顯。7.3 閾值分割后的遺留問題形態(tài)學(xué)操作的最好時(shí)機(jī)閾值分割得到的二值圖通常不是干凈的會(huì)有孤立的白色噪點(diǎn)、前景內(nèi)部的空洞、以及邊緣的毛刺。這就是上一章形態(tài)學(xué)操作最好的應(yīng)用時(shí)機(jī)。我?guī)缀跛械亩祱D流水線都長(zhǎng)這樣自適應(yīng)閾值 → 閉運(yùn)算補(bǔ)內(nèi)部空洞 → 開運(yùn)算去外部噪點(diǎn) → 找輪廓 → 面積/幾何篩選。這一套組合拳能應(yīng)對(duì)絕大多數(shù)圖像分割項(xiàng)目的前期準(zhǔn)備。8. 實(shí)際項(xiàng)目中的算法組合思路與調(diào)參心法8.1 一個(gè)典型的車牌識(shí)別預(yù)處理流程拆解學(xué)了這么多單個(gè)算法最終要落到組合使用上。我以車牌定位這個(gè)經(jīng)典場(chǎng)景為例把整個(gè)流程串起來講一遍。這個(gè)例子非常典型因?yàn)樗鼛缀跤玫搅饲懊嫠械闹R(shí)點(diǎn)。假設(shè)輸入是一張包含車牌的整車照片光照不均勻、背景有各種干擾。完整流程如下讀取并灰度化丟顏色信息減少計(jì)算量。高斯濾波核取(5,5)去掉傳感器噪聲。形態(tài)學(xué)梯度用梯度提取邊緣信息突出車牌區(qū)域因?yàn)檐嚺茀^(qū)域字符密集邊緣梯度特別強(qiáng)。二值化Otsu或固定閾值把梯度圖變成前景背景清晰的黑白圖。形態(tài)學(xué)閉運(yùn)算把車牌的字符之間斷開的邊緣連成一個(gè)大塊方便后續(xù)輪廓檢測(cè)找到完整區(qū)域。輪廓檢測(cè)RETR_EXTERNAL提取外輪廓用面積和寬高比篩掉不是車牌的輪廓。透視變換校正用cv2.getPerspectiveTransform把傾斜的車牌拉正。這個(gè)流程每一步都不是單純的套函數(shù)而是基于對(duì)圖像的理解逐步縮小目標(biāo)范圍。形態(tài)學(xué)梯度在第三步出現(xiàn)、閉運(yùn)算在第五步出現(xiàn)這個(gè)順序是不可顛倒的——梯度先高亮出邊緣密集的區(qū)域閉運(yùn)算再把這些區(qū)域融合成完整塊如果反了高亮效果會(huì)被閉運(yùn)算直接抹掉。8.2 調(diào)參心法一次只動(dòng)一個(gè)參數(shù)永遠(yuǎn)記錄基線效果調(diào)參可能是圖像處理項(xiàng)目中最耗時(shí)的部分也是最容易勸退新手的一個(gè)環(huán)節(jié)。我總結(jié)了一套簡(jiǎn)單但極其有效的流程固定輸入拿同一張代表性圖片做測(cè)試不要每次換圖。項(xiàng)目里挑3到5張能代表正常情況、困難情況、極端情況的圖片作為固定測(cè)試集。設(shè)定基線第一次跑完流程后把當(dāng)前參數(shù)和輸出結(jié)果截圖保存作為baseline。一次只動(dòng)一個(gè)參數(shù)比如先調(diào)Canny的threshold1跑一次對(duì)比baseline記錄效果。再動(dòng)threshold2繼續(xù)對(duì)比。用中間結(jié)果輔助判斷不要只看最終輸出。在預(yù)處理流程里插入中間結(jié)果的顯示比如顯示二值圖、顯示輪廓圖能快速定位是哪個(gè)環(huán)節(jié)出了問題。我早期調(diào)參時(shí)最常犯的錯(cuò)誤是想快速調(diào)好結(jié)果就在代碼里同時(shí)改了三個(gè)參數(shù)結(jié)果效果變好了也說不清是哪一個(gè)的功勞效果變差更是無(wú)法回溯。一次只動(dòng)一個(gè)參數(shù)這條紀(jì)律看起來慢實(shí)際是最快的路徑。8.3 性能優(yōu)化當(dāng)你的算法跑在實(shí)時(shí)視頻流上如果你的場(chǎng)景是實(shí)時(shí)視頻處理比如一幀一幀地處理攝像頭輸入性能優(yōu)化就是個(gè)繞不開的話題。我常用的優(yōu)化手段按性價(jià)比排序降分辨率把處理幀的分辨率降到處理需求的最低要求這是最簡(jiǎn)單、效果最顯著的優(yōu)化。調(diào)整核大小高斯濾波(3,3)比(5,5)快不少在視頻場(chǎng)景下先用(3,3)試不夠再加。跳過非關(guān)鍵幀有些流程不需要每幀都處理比如每隔3幀做一次完整檢測(cè)中間幀用上一幀的結(jié)果插值。ROI感興趣區(qū)域提取只在畫面的一小部分區(qū)域做復(fù)雜度高的算法。比如監(jiān)控畫面里目標(biāo)只出現(xiàn)在門附近就只用門位置的ROI。這些優(yōu)化手段疊加起來把原來處理一幀需要200毫秒的流程壓縮到30毫秒以內(nèi)是可以做到的。我在一個(gè)模擬的實(shí)時(shí)目標(biāo)檢測(cè)demo里通過降低分辨率加縮小ROI把處理延遲從肉眼可見卡頓壓到了流暢預(yù)覽。9. 寫在最后的復(fù)用經(jīng)驗(yàn)把算法沉淀成你自己的工具箱圖像處理的項(xiàng)目經(jīng)驗(yàn)最劃算的沉淀方式不是寫一堆文檔而是把常用流程封裝成可復(fù)用的函數(shù)和類。我自己的習(xí)慣是維護(hù)一個(gè)工具箱腳本把灰度化濾波二值化、輪廓提取面積篩選、特征匹配RANSAC過濾這三類最常見流程各封裝成一個(gè)函數(shù)。每接到新項(xiàng)目先從這個(gè)工具箱取現(xiàn)成的中間模塊再針對(duì)項(xiàng)目特點(diǎn)做參數(shù)調(diào)整和少量新代碼開發(fā)效率能快好幾倍。封裝時(shí)要特別注意接口參數(shù)要暴露業(yè)務(wù)含義而不是數(shù)學(xué)含義。比如封裝二值化函數(shù)時(shí)與其讓調(diào)用者傳blockSize11和C2這種抽象參數(shù)不如直接定義成threshold_modeadaptive_light、sensitivitymedium這種業(yè)務(wù)語(yǔ)義。這樣三個(gè)月后回頭用的時(shí)候你不需要重新理解數(shù)學(xué)含義只需要知道當(dāng)前場(chǎng)景用什么檔位這個(gè)習(xí)慣幫我省了大量的回想時(shí)間。圖像處理的入門門檻不在API而在對(duì)每一個(gè)算法為什么要存在的理解?;叶群蜑V波是為了讓信息更純粹邊緣檢測(cè)和閾值分割是為了挖出結(jié)構(gòu)形態(tài)學(xué)是為了修整形狀輪廓和特征點(diǎn)是為了把結(jié)構(gòu)變成可計(jì)算的量。把這條主線理清楚后新算法對(duì)你來說不過是又一個(gè)解決特定問題的工具需要時(shí)查文檔、看參數(shù)、調(diào)效果就夠了。