學(xué)操作實(shí)戰(zhàn):從膨脹腐蝕到開閉運(yùn)算的完整指南)
1. 形態(tài)學(xué)操作到底在折騰什么第一次接觸“形態(tài)學(xué)操作”這個(gè)詞很多人會(huì)以為跟生物課上的細(xì)胞形態(tài)有關(guān)。其實(shí)在數(shù)字圖像處理里它指的是一套基于形狀的像素級(jí)運(yùn)算工具核心思路是用一個(gè)叫“結(jié)構(gòu)元素”的小模板去掃描整張圖像根據(jù)模板覆蓋區(qū)域內(nèi)像素的分布規(guī)律決定輸出像素的取值。聽起來有點(diǎn)繞換個(gè)說法你就明白了假設(shè)你有一張布滿噪點(diǎn)的二值圖想把這些孤立的噪點(diǎn)去掉同時(shí)保留主體輪廓形態(tài)學(xué)操作就是干這個(gè)的。我做圖像預(yù)處理項(xiàng)目這些年形態(tài)學(xué)操作幾乎是繞不開的一環(huán)。不管是工業(yè)質(zhì)檢里找劃痕、醫(yī)學(xué)影像里分割細(xì)胞、還是文檔掃描件里提取文字行形態(tài)學(xué)都能用極低的計(jì)算成本換來相當(dāng)干凈的結(jié)果。它不像深度學(xué)習(xí)那樣需要大量標(biāo)注數(shù)據(jù)和算力幾個(gè)簡(jiǎn)單的膨脹腐蝕組合就能解決很多實(shí)際問題。這篇文章適合誰(shuí)看如果你正在做圖像分割、缺陷檢測(cè)、OCR預(yù)處理或者單純想搞懂OpenCV里那幾個(gè)形態(tài)學(xué)函數(shù)到底在干什么那接下來的內(nèi)容應(yīng)該能幫你省下不少翻文檔和試參數(shù)的時(shí)間。形態(tài)學(xué)操作最常用的場(chǎng)景是二值圖像但其實(shí)灰度圖也能用。二值圖里像素只有0和1操作邏輯很直觀灰度圖里則是取鄰域內(nèi)的最大值或最小值效果類似于局部提亮或壓暗。我個(gè)人的經(jīng)驗(yàn)是先把二值形態(tài)學(xué)吃透再去看灰度形態(tài)學(xué)理解成本會(huì)低很多。下面我從整體設(shè)計(jì)思路開始拆把每一步的選擇邏輯和踩過的坑都攤開來講。2. 核心思路與方案選型拆解2.1 結(jié)構(gòu)元素形態(tài)學(xué)操作的“刷子”結(jié)構(gòu)元素是形態(tài)學(xué)操作的靈魂。你可以把它想象成一把刷子刷子的形狀和大小決定了你刷墻的效果。常見的結(jié)構(gòu)元素有矩形、十字形、橢圓形尺寸從3×3到幾十不等。選結(jié)構(gòu)元素的時(shí)候核心考量就兩點(diǎn)你要保留的目標(biāo)形狀是什么你要去掉的噪聲形狀是什么。舉個(gè)例子如果你要提取水平方向的文字行用1×15的矩形結(jié)構(gòu)元素做膨脹文字行會(huì)連成一片而垂直方向的干擾線基本不受影響。反過來如果你要消除水平細(xì)線用同樣的結(jié)構(gòu)元素做腐蝕就能達(dá)到目的。我見過不少新手直接拿默認(rèn)的3×3矩形去處理所有情況結(jié)果要么噪聲沒去干凈要么目標(biāo)被啃掉一塊。結(jié)構(gòu)元素的選擇沒有萬能公式但有一個(gè)基本原則結(jié)構(gòu)元素的形狀要跟目標(biāo)的形狀匹配尺寸要略大于噪聲尺寸但小于目標(biāo)尺寸。在OpenCV里結(jié)構(gòu)元素通過cv2.getStructuringElement()生成參數(shù)是形狀類型和尺寸。矩形用cv2.MORPH_RECT十字形用cv2.MORPH_CROSS橢圓形用cv2.MORPH_ELLIPSE。橢圓形在消除各向同性噪聲時(shí)表現(xiàn)更自然矩形在保留直角邊緣時(shí)更有優(yōu)勢(shì)。我一般會(huì)先用矩形試效果不理想再換橢圓形。2.2 膨脹與腐蝕一對(duì)相反相成的操作膨脹和腐蝕是形態(tài)學(xué)的基礎(chǔ)操作其他操作都是它倆的組合。膨脹的效果是讓目標(biāo)區(qū)域“長(zhǎng)胖”腐蝕的效果是讓目標(biāo)區(qū)域“變瘦”。在二值圖里膨脹就是把結(jié)構(gòu)元素覆蓋范圍內(nèi)只要有一個(gè)白色像素中心像素就變成白色腐蝕則是要求覆蓋范圍內(nèi)全是白色像素中心像素才保持白色。這兩個(gè)操作看起來簡(jiǎn)單但背后的邏輯很值得琢磨。膨脹可以用來填補(bǔ)目標(biāo)內(nèi)部的小孔洞、連接斷裂的區(qū)域腐蝕可以用來消除小尺寸的噪聲、分離粘連的目標(biāo)。我經(jīng)常用一個(gè)生活化的類比來解釋膨脹就像給氣球充氣氣球會(huì)變大但形狀基本保持腐蝕就像給氣球放氣氣球會(huì)縮小小的氣球可能直接消失。實(shí)際操作中膨脹和腐蝕的順序很關(guān)鍵。先膨脹后腐蝕叫閉運(yùn)算用來填充小孔和連接斷口先腐蝕后膨脹叫開運(yùn)算用來消除小噪點(diǎn)和分離粘連。這個(gè)順序不能反反了效果完全不一樣。我早期做PCB板缺陷檢測(cè)的時(shí)候就因?yàn)榘验_運(yùn)算和閉運(yùn)算的順序搞反了導(dǎo)致細(xì)小的劃痕被當(dāng)成噪聲消掉而真正的噪聲卻被保留下來排查了大半天才找到原因。2.3 開運(yùn)算與閉運(yùn)算組合拳的威力開運(yùn)算是先腐蝕后膨脹閉運(yùn)算是先膨脹后腐蝕。這兩個(gè)組合操作在實(shí)際項(xiàng)目中的使用頻率遠(yuǎn)高于單獨(dú)的膨脹或腐蝕。開運(yùn)算的典型應(yīng)用是去除背景噪聲比如在文檔掃描件里紙張紋理和灰塵會(huì)產(chǎn)生很多孤立的小白點(diǎn)用開運(yùn)算處理一下這些小點(diǎn)就被腐蝕掉了而文字主體因?yàn)槌叽巛^大腐蝕后還能保留下來再膨脹回去就恢復(fù)了原樣。閉運(yùn)算的典型應(yīng)用是填充目標(biāo)內(nèi)部的小孔洞。比如在細(xì)胞分割任務(wù)中細(xì)胞核內(nèi)部可能因?yàn)槿旧痪霈F(xiàn)暗斑二值化后這些暗斑變成小黑洞用閉運(yùn)算就能填上。我做過一個(gè)細(xì)胞計(jì)數(shù)項(xiàng)目原始圖像二值化后細(xì)胞內(nèi)部有大量孔洞直接做連通域分析會(huì)把一個(gè)細(xì)胞拆成好幾個(gè)用5×5的橢圓形結(jié)構(gòu)元素做閉運(yùn)算后孔洞基本消失計(jì)數(shù)準(zhǔn)確率從78%提升到了94%。開運(yùn)算和閉運(yùn)算也可以級(jí)聯(lián)使用先開運(yùn)算去噪再閉運(yùn)算填孔這是很多預(yù)處理流水線的標(biāo)準(zhǔn)配置。但要注意每次操作都會(huì)對(duì)圖像造成一定程度的形變級(jí)聯(lián)次數(shù)越多形變累積越明顯。我的建議是能用一次操作解決的就不要用兩次參數(shù)調(diào)好比堆操作更有效。2.4 形態(tài)學(xué)梯度、頂帽與黑帽進(jìn)階工具除了開閉運(yùn)算形態(tài)學(xué)還有幾個(gè)進(jìn)階操作值得了解。形態(tài)學(xué)梯度是膨脹圖減去腐蝕圖得到的是目標(biāo)的輪廓邊緣。這個(gè)操作在邊緣檢測(cè)里很有用尤其是當(dāng)目標(biāo)邊緣比較模糊的時(shí)候形態(tài)學(xué)梯度能給出比Canny更粗但更連續(xù)的邊緣。頂帽運(yùn)算是原圖減去開運(yùn)算圖得到的是比結(jié)構(gòu)元素小的亮區(qū)域。這個(gè)操作常用來提取背景中的小亮點(diǎn)比如在暗背景下檢測(cè)微小目標(biāo)。黑帽運(yùn)算是閉運(yùn)算圖減去原圖得到的是比結(jié)構(gòu)元素小的暗區(qū)域常用來提取亮背景中的暗細(xì)節(jié)。這幾個(gè)操作我用的頻率不如開閉運(yùn)算高但在特定場(chǎng)景下效果很好。比如做液晶屏缺陷檢測(cè)時(shí)屏幕背景是均勻的亮色微小的暗點(diǎn)缺陷用黑帽運(yùn)算一抓一個(gè)準(zhǔn)比閾值分割穩(wěn)定得多。頂帽運(yùn)算在熒光顯微鏡圖像里提取微弱熒光斑點(diǎn)也很實(shí)用。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)3.1 二值形態(tài)學(xué)的像素級(jí)邏輯要真正理解形態(tài)學(xué)得從像素級(jí)邏輯入手。以3×3的矩形結(jié)構(gòu)元素為例膨脹操作會(huì)檢查每個(gè)像素的3×3鄰域只要鄰域內(nèi)有一個(gè)白色像素中心像素就置為白色。這意味著膨脹后白色區(qū)域會(huì)向外擴(kuò)張一圈。腐蝕操作則要求3×3鄰域內(nèi)全是白色像素中心像素才保持白色否則置為黑色。這意味著腐蝕后白色區(qū)域會(huì)向內(nèi)收縮一圈。這個(gè)邏輯在邊界處需要特殊處理。OpenCV默認(rèn)對(duì)邊界進(jìn)行復(fù)制填充也就是說圖像邊緣外的像素值等于邊緣像素的值。這個(gè)默認(rèn)行為在大多數(shù)情況下沒問題但如果你處理的目標(biāo)緊貼圖像邊緣可能會(huì)得到意外的結(jié)果。我遇到過一張目標(biāo)剛好在圖像邊界的圖膨脹后目標(biāo)被截?cái)嗔艘徊糠趾髞碓趫D像外圍補(bǔ)了一圈黑色邊框才解決。結(jié)構(gòu)元素的錨點(diǎn)位置也需要注意。OpenCV默認(rèn)錨點(diǎn)在結(jié)構(gòu)元素中心對(duì)于偶數(shù)尺寸的結(jié)構(gòu)元素錨點(diǎn)會(huì)偏向左上。這個(gè)細(xì)節(jié)在精確對(duì)齊的場(chǎng)景下很重要比如做圖像配準(zhǔn)的時(shí)候錨點(diǎn)偏移一個(gè)像素就可能導(dǎo)致配準(zhǔn)失敗。我的習(xí)慣是盡量用奇數(shù)尺寸的結(jié)構(gòu)元素錨點(diǎn)自然落在中心省去很多麻煩。3.2 灰度形態(tài)學(xué)的擴(kuò)展邏輯灰度形態(tài)學(xué)把二值邏輯擴(kuò)展到了灰度級(jí)。膨脹變成取鄰域內(nèi)的最大值腐蝕變成取鄰域內(nèi)的最小值。這個(gè)擴(kuò)展很自然二值圖里白色是1黑色是0取最大值就是只要有一個(gè)1就輸出1跟膨脹邏輯一致取最小值就是要求全是1才輸出1跟腐蝕邏輯一致。灰度形態(tài)學(xué)在實(shí)際項(xiàng)目中的應(yīng)用場(chǎng)景不太一樣?;叶扰蛎浛梢杂脕碓鰪?qiáng)亮細(xì)節(jié)灰度腐蝕可以用來增強(qiáng)暗細(xì)節(jié)?;叶乳_運(yùn)算可以消除比結(jié)構(gòu)元素小的亮區(qū)域灰度閉運(yùn)算可以消除比結(jié)構(gòu)元素小的暗區(qū)域。我做過一個(gè)金屬表面缺陷檢測(cè)的項(xiàng)目表面有微小的凹坑灰度閉運(yùn)算處理后凹坑被填平然后用原圖減去閉運(yùn)算圖凹坑區(qū)域就凸顯出來了效果比直接閾值分割好很多?;叶刃螒B(tài)學(xué)的計(jì)算量比二值形態(tài)學(xué)大因?yàn)槊總€(gè)像素要做比較而不是邏輯運(yùn)算。但在現(xiàn)代硬件上這個(gè)計(jì)算量完全可以接受。OpenCV的形態(tài)學(xué)函數(shù)對(duì)二值圖和灰度圖是統(tǒng)一的接口通過輸入圖像的類型自動(dòng)判斷用起來很方便。3.3 結(jié)構(gòu)元素尺寸的選擇策略結(jié)構(gòu)元素尺寸的選擇是形態(tài)學(xué)操作里最需要經(jīng)驗(yàn)的部分。尺寸太小噪聲去不干凈尺寸太大目標(biāo)被過度腐蝕或膨脹。我的一般策略是先估計(jì)噪聲的尺寸和目標(biāo)的最小尺寸結(jié)構(gòu)元素尺寸取兩者之間偏向噪聲尺寸一側(cè)。具體怎么估計(jì)如果噪聲是孤立的點(diǎn)尺寸大概是1到3個(gè)像素那結(jié)構(gòu)元素用3×3就夠了。如果噪聲是細(xì)線寬度大概2到5個(gè)像素那結(jié)構(gòu)元素尺寸要大于噪聲寬度比如7×7。目標(biāo)的最小尺寸可以通過連通域分析來測(cè)量取所有目標(biāo)連通域的最小外接矩形尺寸結(jié)構(gòu)元素尺寸不要超過這個(gè)值的一半。還有一個(gè)經(jīng)驗(yàn)法則結(jié)構(gòu)元素尺寸每增加2計(jì)算量大概增加一倍。所以在滿足效果的前提下盡量用小尺寸。我見過有人直接用15×15的結(jié)構(gòu)元素處理所有圖像結(jié)果小目標(biāo)全被消掉了還抱怨形態(tài)學(xué)不好用。其實(shí)不是形態(tài)學(xué)不好用是參數(shù)沒選對(duì)。3.4 迭代次數(shù)的控制形態(tài)學(xué)操作可以迭代多次效果相當(dāng)于用更大的結(jié)構(gòu)元素。比如3×3的結(jié)構(gòu)元素迭代兩次效果接近5×5的結(jié)構(gòu)元素迭代一次。但兩者并不完全等價(jià)迭代多次的形狀擴(kuò)展更平滑而大結(jié)構(gòu)元素一次操作可能產(chǎn)生更方正的形狀。迭代次數(shù)的控制原則跟尺寸選擇類似能用一次大結(jié)構(gòu)元素解決的就不要迭代多次小結(jié)構(gòu)元素。因?yàn)榈鷷?huì)累積邊界效應(yīng)尤其是目標(biāo)靠近圖像邊緣的時(shí)候多次迭代可能導(dǎo)致目標(biāo)被完全侵蝕掉。我一般優(yōu)先調(diào)整結(jié)構(gòu)元素尺寸迭代次數(shù)保持在1到2次。但在某些場(chǎng)景下迭代是必要的。比如要連接兩個(gè)相距較遠(yuǎn)的斷裂區(qū)域用大結(jié)構(gòu)元素一次膨脹可能把周圍無關(guān)區(qū)域也連進(jìn)來而用小結(jié)構(gòu)元素迭代多次可以更精細(xì)地控制連接過程。這個(gè)需要根據(jù)具體圖像多試幾次找到最佳平衡點(diǎn)。4. 完整實(shí)操流程與代碼實(shí)現(xiàn)4.1 環(huán)境準(zhǔn)備與依賴安裝我平時(shí)用Python加OpenCV做形態(tài)學(xué)操作環(huán)境搭建很簡(jiǎn)單。Python 3.8以上版本都可以O(shè)penCV用opencv-python包安裝命令就一行pip install opencv-python numpy matplotlibnumpy用來做數(shù)組運(yùn)算matplotlib用來顯示圖像對(duì)比。如果你需要處理視頻流再加裝opencv-python-headless或者直接用完整版。我建議在虛擬環(huán)境里安裝避免跟系統(tǒng)里的其他包沖突。驗(yàn)證安裝是否成功import cv2 import numpy as np print(cv2.__version__)能打印出版本號(hào)就說明環(huán)境沒問題。OpenCV的形態(tài)學(xué)函數(shù)在cv2命名空間下主要用到cv2.getStructuringElement()、cv2.erode()、cv2.dilate()、cv2.morphologyEx()這幾個(gè)。4.2 圖像讀取與預(yù)處理形態(tài)學(xué)操作通常在二值圖上進(jìn)行所以第一步是把圖像轉(zhuǎn)成二值圖。流程是讀取圖像、轉(zhuǎn)灰度、閾值分割。代碼大概長(zhǎng)這樣img cv2.imread(input.png, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)閾值的選擇很關(guān)鍵。如果圖像光照不均勻固定閾值效果會(huì)很差這時(shí)候可以用自適應(yīng)閾值binary cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)自適應(yīng)閾值的兩個(gè)參數(shù)需要調(diào)block size是鄰域大小C是常數(shù)偏移。block size一般取11到31之間的奇數(shù)C取2到10之間的值。我一般先用固定閾值試效果不好再換自適應(yīng)。如果圖像噪聲比較嚴(yán)重閾值化之前可以先做一次高斯模糊blurred cv2.GaussianBlur(img, (5, 5), 0)高斯核大小取5×5或7×7標(biāo)準(zhǔn)差取0讓OpenCV自動(dòng)計(jì)算。模糊會(huì)損失一些細(xì)節(jié)但能顯著減少閾值化后的噪點(diǎn)對(duì)后續(xù)形態(tài)學(xué)操作有幫助。4.3 結(jié)構(gòu)元素的創(chuàng)建與選擇OpenCV創(chuàng)建結(jié)構(gòu)元素的函數(shù)是cv2.getStructuringElement()三個(gè)參數(shù)分別是形狀、尺寸、錨點(diǎn)可選。常用寫法kernel_rect cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) kernel_ellipse cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) kernel_cross cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))矩形結(jié)構(gòu)元素適合處理有直角邊緣的目標(biāo)橢圓形適合處理圓形或自然形狀的目標(biāo)十字形適合處理細(xì)線狀目標(biāo)。我一般先用矩形因?yàn)橛?jì)算最快效果不理想再換橢圓形。尺寸的選擇前面已經(jīng)講過策略這里補(bǔ)充一個(gè)實(shí)操技巧可以寫一個(gè)循環(huán)遍歷幾個(gè)候選尺寸把結(jié)果拼成一張對(duì)比圖肉眼選最好的。代碼大概這樣sizes [3, 5, 7, 9] results [] for s in sizes: kernel cv2.getStructuringElement(cv2.MORPH_RECT, (s, s)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) results.append(opened)然后把results里的圖橫向拼接顯示。這個(gè)方法雖然土但選參數(shù)特別高效比盲猜快得多。4.4 開運(yùn)算去除噪點(diǎn)的完整流程開運(yùn)算是先腐蝕后膨脹OpenCV里用cv2.morphologyEx()一個(gè)函數(shù)搞定kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)這里用3×3的橢圓形結(jié)構(gòu)元素適合去除孤立的噪點(diǎn)。如果噪點(diǎn)比較大可以增大到5×5。開運(yùn)算之后圖像里的小白點(diǎn)基本消失目標(biāo)主體保留完整。我做過一個(gè)文檔二值化的項(xiàng)目原始圖像閾值化后背景有很多細(xì)小的黑點(diǎn)噪聲用3×3開運(yùn)算處理后噪聲減少了約90%而文字筆畫基本沒受影響。這里的關(guān)鍵是結(jié)構(gòu)元素尺寸要小于文字筆畫寬度否則文字會(huì)被腐蝕斷。開運(yùn)算也可以迭代多次但一般一次就夠了。如果一次開運(yùn)算后還有殘留噪聲說明噪聲尺寸比結(jié)構(gòu)元素大應(yīng)該增大結(jié)構(gòu)元素而不是迭代。4.5 閉運(yùn)算填充孔洞的完整流程閉運(yùn)算的代碼跟開運(yùn)算類似只是把cv2.MORPH_OPEN換成cv2.MORPH_CLOSEkernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)閉運(yùn)算的結(jié)構(gòu)元素尺寸要大于孔洞尺寸但小于目標(biāo)之間的間距。如果孔洞比較大結(jié)構(gòu)元素也要相應(yīng)增大。我處理細(xì)胞圖像時(shí)孔洞直徑大概5到10個(gè)像素用7×7的橢圓形結(jié)構(gòu)元素效果最好。閉運(yùn)算的一個(gè)副作用是可能把相鄰的目標(biāo)連在一起。如果目標(biāo)之間距離很近閉運(yùn)算后可能粘連。這時(shí)候可以先做一次開運(yùn)算分離目標(biāo)再做閉運(yùn)算填孔但要注意順序和參數(shù)配合。我的經(jīng)驗(yàn)是先用小結(jié)構(gòu)元素開運(yùn)算分離再用大結(jié)構(gòu)元素閉運(yùn)算填孔兩者尺寸不要差太多。4.6 形態(tài)學(xué)梯度提取輪廓形態(tài)學(xué)梯度的代碼kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) gradient cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel)梯度圖里白色是邊緣黑色是平坦區(qū)域。結(jié)構(gòu)元素尺寸決定了邊緣的粗細(xì)3×3得到細(xì)邊緣5×5得到粗邊緣。我一般用3×3因?yàn)榧?xì)邊緣定位更準(zhǔn)確。形態(tài)學(xué)梯度跟Canny邊緣檢測(cè)的區(qū)別在于Canny給出的是單像素寬度的邊緣形態(tài)學(xué)梯度給出的是多像素寬度的邊緣帶。如果你需要精確的邊緣位置用Canny如果你需要連續(xù)的邊緣區(qū)域用形態(tài)學(xué)梯度。我在做目標(biāo)計(jì)數(shù)時(shí)先用形態(tài)學(xué)梯度得到邊緣帶再做連通域分析比直接用Canny更穩(wěn)定因?yàn)镃anny容易斷邊。4.7 頂帽與黑帽提取細(xì)節(jié)頂帽運(yùn)算的代碼kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) tophat cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel)注意頂帽和黑帽通常用在灰度圖上不是二值圖。結(jié)構(gòu)元素尺寸要大于你要提取的細(xì)節(jié)尺寸。比如你要提取直徑小于10像素的亮斑結(jié)構(gòu)元素用15×15或更大。黑帽運(yùn)算把cv2.MORPH_TOPHAT換成cv2.MORPH_BLACKHAT即可。黑帽提取的是暗細(xì)節(jié)頂帽提取的是亮細(xì)節(jié)。我做過一個(gè)液晶屏暗點(diǎn)檢測(cè)的項(xiàng)目屏幕背景均勻發(fā)亮暗點(diǎn)缺陷用黑帽運(yùn)算處理后非常明顯再用簡(jiǎn)單閾值就能分割出來比直接閾值分割的誤檢率低很多。5. 常見問題與排查技巧實(shí)錄5.1 形態(tài)學(xué)操作后目標(biāo)消失或斷裂這是最常見的問題原因通常是結(jié)構(gòu)元素尺寸過大或腐蝕次數(shù)過多。排查思路先檢查結(jié)構(gòu)元素尺寸是否超過了目標(biāo)的最小尺寸如果是減小尺寸。再檢查是否迭代了多次如果是減少迭代次數(shù)。如果目標(biāo)本身就很細(xì)比如細(xì)線或小點(diǎn)腐蝕操作很容易把它完全消掉這時(shí)候應(yīng)該改用開運(yùn)算而不是單獨(dú)腐蝕或者用更小的結(jié)構(gòu)元素。我遇到過一個(gè)案例處理指紋圖像時(shí)用5×5結(jié)構(gòu)元素做腐蝕結(jié)果指紋紋線全斷了。后來?yè)Q成3×3結(jié)構(gòu)元素紋線保留完好噪聲也去掉了。所以尺寸選擇真的是差之毫厘謬以千里。5.2 噪聲去不干凈如果開運(yùn)算后還有噪聲殘留說明噪聲尺寸大于結(jié)構(gòu)元素尺寸。解決辦法是增大結(jié)構(gòu)元素但要注意不要超過目標(biāo)尺寸。如果噪聲和目標(biāo)尺寸接近形態(tài)學(xué)可能無能為力需要換其他方法比如連通域面積過濾。另一個(gè)可能的原因是噪聲不是孤立的點(diǎn)而是連成片的區(qū)域。開運(yùn)算對(duì)孤立噪點(diǎn)效果好對(duì)連片噪聲效果差。這時(shí)候可以先做一次腐蝕把連片噪聲打散再做開運(yùn)算。或者直接用連通域分析按面積過濾掉噪聲區(qū)域。5.3 目標(biāo)粘連在一起閉運(yùn)算或膨脹操作可能導(dǎo)致相鄰目標(biāo)粘連。解決辦法是在閉運(yùn)算之前先做開運(yùn)算分離目標(biāo)或者減小閉運(yùn)算的結(jié)構(gòu)元素尺寸。如果目標(biāo)本身距離就很近形態(tài)學(xué)可能無法完美分離需要結(jié)合分水嶺算法或距離變換。我處理過一批細(xì)胞圖像細(xì)胞之間有些粘連直接用閉運(yùn)算填孔后粘連更嚴(yán)重了。后來改成先開運(yùn)算分離再對(duì)每個(gè)連通域單獨(dú)填孔效果好很多。這個(gè)思路值得借鑒不要試圖用一次形態(tài)學(xué)操作解決所有問題分步驟處理往往更可控。5.4 邊界效應(yīng)導(dǎo)致結(jié)果異常圖像邊界處的形態(tài)學(xué)操作容易出問題因?yàn)檫吔缤獾南袼厥翘畛涞?。如果目?biāo)緊貼邊界膨脹后可能被截?cái)喔g后可能被過度侵蝕。解決辦法是在圖像外圍補(bǔ)一圈黑色邊框操作完成后再裁掉。補(bǔ)邊框的寬度要大于結(jié)構(gòu)元素半徑。border 10 padded cv2.copyMakeBorder(binary, border, border, border, border, cv2.BORDER_CONSTANT, value0) # 形態(tài)學(xué)操作 result cv2.morphologyEx(padded, cv2.MORPH_OPEN, kernel) # 裁掉邊框 result result[border:-border, border:-border]這個(gè)技巧我?guī)缀踉诿總€(gè)項(xiàng)目里都會(huì)用尤其是處理邊緣有目標(biāo)的情況。補(bǔ)邊框的成本很低但能避免很多莫名其妙的邊界問題。5.5 灰度形態(tài)學(xué)與二值形態(tài)學(xué)混淆有些新手會(huì)把灰度圖直接拿去做二值形態(tài)學(xué)操作或者把二值圖拿去做灰度形態(tài)學(xué)操作結(jié)果不符合預(yù)期。OpenCV的形態(tài)學(xué)函數(shù)會(huì)根據(jù)輸入圖像類型自動(dòng)選擇二值或灰度邏輯所以關(guān)鍵是要確保輸入圖像類型正確。二值圖應(yīng)該是uint8類型且只有0和255兩個(gè)值灰度圖是uint8類型且有多個(gè)灰度級(jí)。如果你不確定圖像類型可以用img.dtype和img.shape檢查。二值圖經(jīng)過閾值化后通常是uint8但如果你用cv2.threshold()的返回值第二個(gè)返回值才是二值圖第一個(gè)是閾值。這個(gè)細(xì)節(jié)容易搞錯(cuò)我見過有人把閾值當(dāng)成圖像傳進(jìn)去結(jié)果當(dāng)然不對(duì)。5.6 常見問題速查表問題現(xiàn)象可能原因排查方法解決方案目標(biāo)消失結(jié)構(gòu)元素過大檢查結(jié)構(gòu)元素尺寸與目標(biāo)尺寸減小結(jié)構(gòu)元素或減少迭代次數(shù)目標(biāo)斷裂腐蝕過度檢查是否單獨(dú)使用腐蝕改用開運(yùn)算或減小結(jié)構(gòu)元素噪聲殘留結(jié)構(gòu)元素過小測(cè)量噪聲尺寸增大結(jié)構(gòu)元素或改用連通域過濾目標(biāo)粘連膨脹過度檢查閉運(yùn)算結(jié)構(gòu)元素尺寸先開運(yùn)算分離或減小結(jié)構(gòu)元素邊界異常邊界填充效應(yīng)檢查目標(biāo)是否緊貼邊界補(bǔ)邊框后操作再裁剪結(jié)果全黑圖像類型錯(cuò)誤檢查圖像dtype和值范圍確保二值圖為0和255結(jié)果全白閾值設(shè)置錯(cuò)誤檢查閾值化參數(shù)調(diào)整閾值或改用自適應(yīng)閾值6. 參數(shù)調(diào)優(yōu)與性能優(yōu)化經(jīng)驗(yàn)6.1 結(jié)構(gòu)元素形狀的對(duì)比測(cè)試不同形狀的結(jié)構(gòu)元素對(duì)同一張圖的效果差異可能很大。我做過一組對(duì)比測(cè)試用3×3、5×5、7×7三種尺寸矩形、橢圓形、十字形三種形狀共9種組合處理同一張含噪點(diǎn)的二值圖。結(jié)果顯示3×3矩形去噪效果一般但保留細(xì)節(jié)最好5×5橢圓形去噪效果和細(xì)節(jié)保留最平衡7×7十字形去噪最強(qiáng)但目標(biāo)邊緣有明顯形變。這個(gè)測(cè)試給我的啟發(fā)是沒有最好的結(jié)構(gòu)元素只有最適合當(dāng)前圖像的結(jié)構(gòu)元素。我的建議是至少測(cè)試三種形狀和三種尺寸用對(duì)比圖肉眼判斷。雖然多花幾分鐘但比反復(fù)試錯(cuò)效率高得多。6.2 形態(tài)學(xué)操作的順序優(yōu)化當(dāng)需要同時(shí)使用開運(yùn)算和閉運(yùn)算時(shí)順序很重要。先開后閉是去噪加填孔先閉后開是填孔加去噪。兩者的區(qū)別在于先開后閉會(huì)在去噪的同時(shí)可能擴(kuò)大孔洞再填孔時(shí)需要的結(jié)構(gòu)元素更大先閉后開會(huì)在填孔的同時(shí)可能連接噪聲再去噪時(shí)可能把連接處斷開。我的經(jīng)驗(yàn)是如果噪聲是主要問題先開運(yùn)算如果孔洞是主要問題先閉運(yùn)算。如果兩者都嚴(yán)重先開后閉但閉運(yùn)算的結(jié)構(gòu)元素要比開運(yùn)算大一號(hào)。這個(gè)順序不是絕對(duì)的還是要根據(jù)具體圖像測(cè)試。6.3 性能優(yōu)化減少不必要的操作形態(tài)學(xué)操作的計(jì)算量與圖像尺寸、結(jié)構(gòu)元素尺寸、迭代次數(shù)都成正比。優(yōu)化性能的核心是減少不必要的操作。具體做法能用小結(jié)構(gòu)元素就不用大結(jié)構(gòu)元素能一次操作完成就不迭代能在低分辨率圖像上做的操作就不要在高分辨率上做。我處理高分辨率圖像時(shí)經(jīng)常先降采樣到一半尺寸做形態(tài)學(xué)操作得到結(jié)果后再上采樣回原尺寸。這樣做計(jì)算量減少到四分之一而形態(tài)學(xué)操作對(duì)分辨率不敏感降采樣后的效果跟原尺寸差別很小。當(dāng)然如果目標(biāo)尺寸接近像素級(jí)降采樣會(huì)丟失目標(biāo)這時(shí)候就不能降。6.4 與其他預(yù)處理方法的配合形態(tài)學(xué)操作很少單獨(dú)使用通常跟濾波、閾值化、連通域分析配合。我的標(biāo)準(zhǔn)預(yù)處理流水線是高斯濾波去高頻噪聲自適應(yīng)閾值二值化開運(yùn)算去殘留噪點(diǎn)閉運(yùn)算填孔連通域分析提取目標(biāo)。這個(gè)流水線在大多數(shù)場(chǎng)景下都能給出不錯(cuò)的結(jié)果。如果圖像對(duì)比度低閾值化之前可以先做直方圖均衡化。如果圖像光照不均可以用頂帽運(yùn)算做背景校正。如果目標(biāo)邊緣模糊可以用形態(tài)學(xué)梯度增強(qiáng)邊緣。這些組合方法需要根據(jù)具體圖像靈活調(diào)整沒有固定套路。7. 實(shí)際項(xiàng)目中的應(yīng)用案例拆解7.1 工業(yè)零件表面缺陷檢測(cè)我做過一個(gè)金屬零件表面缺陷檢測(cè)的項(xiàng)目目標(biāo)是找出零件表面的劃痕和凹坑。原始圖像是灰度圖表面有反光不均和加工紋理干擾。處理流程是先用頂帽運(yùn)算校正光照不均再用自適應(yīng)閾值二值化然后用開運(yùn)算去除細(xì)小紋理噪聲最后用連通域分析提取缺陷區(qū)域。頂帽運(yùn)算的結(jié)構(gòu)元素用了25×25的橢圓形因?yàn)楣庹詹痪谴蟪叨鹊淖兓枰蠼Y(jié)構(gòu)元素才能估計(jì)背景。開運(yùn)算用了3×3的矩形因?yàn)榧y理噪聲是細(xì)小的點(diǎn)狀和線狀。這個(gè)組合下來缺陷檢測(cè)的準(zhǔn)確率達(dá)到了92%誤檢率控制在5%以內(nèi)。這個(gè)項(xiàng)目的關(guān)鍵經(jīng)驗(yàn)是形態(tài)學(xué)操作在缺陷檢測(cè)里的角色是預(yù)處理不是最終檢測(cè)。它負(fù)責(zé)把圖像清理干凈讓后續(xù)的閾值分割和連通域分析能穩(wěn)定工作。不要指望形態(tài)學(xué)直接給出缺陷位置它沒這個(gè)能力。7.2 醫(yī)學(xué)細(xì)胞圖像分割另一個(gè)項(xiàng)目是細(xì)胞圖像分割目標(biāo)是統(tǒng)計(jì)細(xì)胞數(shù)量和測(cè)量細(xì)胞面積。原始圖像是熒光顯微鏡圖像細(xì)胞核發(fā)亮背景暗但細(xì)胞核內(nèi)部有暗斑細(xì)胞之間有粘連。處理流程是高斯濾波去噪Otsu閾值二值化開運(yùn)算分離粘連細(xì)胞閉運(yùn)算填充內(nèi)部暗斑連通域分析計(jì)數(shù)。開運(yùn)算用了5×5的橢圓形閉運(yùn)算用了7×7的橢圓形。開運(yùn)算分離了大約30%的粘連細(xì)胞閉運(yùn)算填充了約80%的內(nèi)部孔洞。最終計(jì)數(shù)準(zhǔn)確率從直接閾值化的78%提升到了94%。這個(gè)項(xiàng)目的經(jīng)驗(yàn)是開運(yùn)算和閉運(yùn)算的尺寸要配合開運(yùn)算尺寸小于細(xì)胞間距閉運(yùn)算尺寸大于孔洞尺寸但小于細(xì)胞尺寸。7.3 文檔掃描件文字提取文檔掃描件的處理是形態(tài)學(xué)的經(jīng)典應(yīng)用。原始圖像是紙質(zhì)文檔的掃描件背景有紙張紋理和灰塵噪點(diǎn)文字有斷筆和粘連。處理流程是灰度化自適應(yīng)閾值二值化開運(yùn)算去噪閉運(yùn)算連接斷筆最后用投影法切分文字行。開運(yùn)算用了2×2的矩形因?yàn)槲淖止P畫很細(xì)結(jié)構(gòu)元素大了會(huì)把筆畫腐蝕斷。閉運(yùn)算用了3×3的矩形連接斷筆的同時(shí)不會(huì)讓相鄰文字粘連。這個(gè)項(xiàng)目讓我深刻體會(huì)到文檔圖像的形態(tài)學(xué)參數(shù)要非常保守因?yàn)槲淖旨?xì)節(jié)太精細(xì)稍大一點(diǎn)的結(jié)構(gòu)元素就會(huì)造成不可逆的損失。7.4 液晶屏缺陷檢測(cè)液晶屏缺陷檢測(cè)對(duì)形態(tài)學(xué)的要求又不一樣。屏幕背景均勻缺陷是微小的暗點(diǎn)或亮點(diǎn)。處理流程是灰度化黑帽運(yùn)算提取暗點(diǎn)頂帽運(yùn)算提取亮點(diǎn)閾值分割連通域分析定位缺陷。黑帽和頂帽的結(jié)構(gòu)元素用了15×15的橢圓形因?yàn)槿毕莩叽绱蟾?到10個(gè)像素結(jié)構(gòu)元素要大于缺陷尺寸才能把缺陷從背景中分離出來。這個(gè)項(xiàng)目的經(jīng)驗(yàn)是頂帽和黑帽在均勻背景下的缺陷檢測(cè)效果極好但前提是背景要足夠均勻。如果背景有漸變或紋理需要先做背景校正。8. 進(jìn)階技巧與個(gè)人實(shí)操心得8.1 用形態(tài)學(xué)做背景估計(jì)形態(tài)學(xué)的一個(gè)高級(jí)用法是做背景估計(jì)。對(duì)灰度圖做開運(yùn)算如果結(jié)構(gòu)元素尺寸大于所有前景目標(biāo)得到的就是背景估計(jì)圖。然后用原圖減去背景圖就得到前景增強(qiáng)圖。這個(gè)方法在光照不均的場(chǎng)景下特別有效比直方圖均衡化更自然。我做過一個(gè)車牌識(shí)別的預(yù)處理車牌圖像光照不均左邊暗右邊亮。用51×51的橢圓形結(jié)構(gòu)元素做開運(yùn)算得到背景圖原圖減背景后車牌字符的對(duì)比度顯著提升后續(xù)二值化效果好了很多。這個(gè)技巧的關(guān)鍵是結(jié)構(gòu)元素要足夠大大到能覆蓋所有字符。8.2 形態(tài)學(xué)與距離變換結(jié)合距離變換是計(jì)算每個(gè)前景像素到最近背景像素的距離。形態(tài)學(xué)跟距離變換結(jié)合可以做目標(biāo)分離。具體做法先對(duì)二值圖做距離變換然后對(duì)距離圖做閾值化得到目標(biāo)中心的種子點(diǎn)再用分水嶺算法從種子點(diǎn)生長(zhǎng)出完整目標(biāo)。這個(gè)方法在細(xì)胞分割和顆粒分割里很常用。我處理過一批粘連嚴(yán)重的顆粒圖像直接用形態(tài)學(xué)開運(yùn)算無法分離因?yàn)轭w粒之間沒有明顯的間隙。后來用距離變換加閾值化找到每個(gè)顆粒的中心再用分水嶺算法分割成功分離了95%以上的粘連顆粒。這個(gè)組合方法的威力比單純形態(tài)學(xué)大得多但參數(shù)也更復(fù)雜需要多調(diào)。8.3 形態(tài)學(xué)操作的并行化如果處理視頻流或大批量圖像形態(tài)學(xué)操作可能成為性能瓶頸。OpenCV的形態(tài)學(xué)函數(shù)已經(jīng)做了SIMD優(yōu)化單張圖像的處理速度很快。但如果圖像分辨率很高或者需要實(shí)時(shí)處理可以考慮用GPU加速。OpenCV的CUDA模塊提供了形態(tài)學(xué)操作的GPU版本速度可以提升10倍以上。不過GPU加速的前提是你有NVIDIA顯卡并且編譯了CUDA版本的OpenCV。如果沒有GPU也可以用多進(jìn)程并行處理多張圖像Python的multiprocessing模塊就能做到。我的經(jīng)驗(yàn)是單張圖像處理用CPU就夠了批量處理用多進(jìn)程實(shí)時(shí)視頻流才需要考慮GPU。8.4 形態(tài)學(xué)參數(shù)的自動(dòng)化搜索手動(dòng)調(diào)形態(tài)學(xué)參數(shù)很費(fèi)時(shí)間尤其是結(jié)構(gòu)元素尺寸和迭代次數(shù)。我寫過一個(gè)簡(jiǎn)單的網(wǎng)格搜索腳本遍歷幾個(gè)候選尺寸和迭代次數(shù)用連通域數(shù)量或目標(biāo)面積作為評(píng)價(jià)指標(biāo)自動(dòng)選出最優(yōu)參數(shù)。評(píng)價(jià)指標(biāo)的選擇取決于任務(wù)如果是去噪任務(wù)用噪聲連通域數(shù)量如果是分割任務(wù)用目標(biāo)面積與真實(shí)面積的交并比。這個(gè)自動(dòng)化搜索雖然不能完全替代人工判斷但能快速縮小參數(shù)范圍。我一般先用網(wǎng)格搜索找到大致范圍再在這個(gè)范圍內(nèi)手動(dòng)微調(diào)。這樣比純手動(dòng)調(diào)參效率高很多尤其是需要處理多張圖像的時(shí)候。8.5 幾個(gè)容易忽略的細(xì)節(jié)第一個(gè)細(xì)節(jié)是形態(tài)學(xué)操作的原地執(zhí)行。OpenCV的cv2.erode()和cv2.dilate()支持原地操作把輸出圖像設(shè)為輸入圖像本身可以節(jié)省內(nèi)存。但cv2.morphologyEx()不支持原地操作需要另外分配輸出圖像。這個(gè)細(xì)節(jié)在內(nèi)存受限的環(huán)境下很重要。第二個(gè)細(xì)節(jié)是結(jié)構(gòu)元素的連續(xù)性。cv2.getStructuringElement()生成的結(jié)構(gòu)元素是連續(xù)的但如果你手動(dòng)構(gòu)造結(jié)構(gòu)元素要確保它是連續(xù)的。不連續(xù)的結(jié)構(gòu)元素會(huì)導(dǎo)致形態(tài)學(xué)操作出現(xiàn)空洞結(jié)果不可預(yù)期。第三個(gè)細(xì)節(jié)是形態(tài)學(xué)操作的邊界填充類型。OpenCV默認(rèn)用cv2.BORDER_CONSTANT填充邊界填充值為0。但你可以通過borderType參數(shù)指定其他填充方式比如cv2.BORDER_REPLICATE復(fù)制邊界像素。不同的填充方式對(duì)邊界處的形態(tài)學(xué)結(jié)果有影響需要根據(jù)場(chǎng)景選擇。第四個(gè)細(xì)節(jié)是形態(tài)學(xué)操作對(duì)彩色圖像的處理。OpenCV的形態(tài)學(xué)函數(shù)對(duì)彩色圖像是逐通道處理的這可能導(dǎo)致顏色失真。如果要在彩色圖像上做形態(tài)學(xué)操作建議先轉(zhuǎn)灰度或轉(zhuǎn)HSV空間在亮度通道上操作再轉(zhuǎn)回彩色空間。我見過有人直接在RGB圖像上做形態(tài)學(xué)操作結(jié)果顏色變得很奇怪排查半天才發(fā)現(xiàn)是通道處理的問題。8.6 從形態(tài)學(xué)看圖像處理的本質(zhì)做了這么多形態(tài)學(xué)項(xiàng)目我最大的體會(huì)是形態(tài)學(xué)操作的本質(zhì)是用形狀先驗(yàn)來過濾信息。你告訴算法“我要保留什么形狀、去掉什么形狀”算法就按這個(gè)規(guī)則去執(zhí)行。這個(gè)思路跟深度學(xué)習(xí)的思路完全不同深度學(xué)習(xí)是從數(shù)據(jù)里學(xué)形狀先驗(yàn)形態(tài)學(xué)是人工指定形狀先驗(yàn)。兩者各有優(yōu)劣。形態(tài)學(xué)的優(yōu)勢(shì)是簡(jiǎn)單、快速、可解釋、不需要訓(xùn)練數(shù)據(jù)劣勢(shì)是需要人工調(diào)參、對(duì)復(fù)雜形狀的適應(yīng)能力有限。在實(shí)際項(xiàng)目里我通常先用形態(tài)學(xué)做快速原型看看能不能解決問題如果形態(tài)學(xué)搞不定再考慮上深度學(xué)習(xí)。很多時(shí)候形態(tài)學(xué)加簡(jiǎn)單的閾值分割就能達(dá)到90%的效果沒必要上復(fù)雜的模型。這個(gè)內(nèi)容后續(xù)還可以這樣擴(kuò)展把形態(tài)學(xué)操作跟輪廓分析結(jié)合做更精細(xì)的形狀測(cè)量或者把形態(tài)學(xué)操作跟頻域?yàn)V波結(jié)合處理周期性噪聲或者把形態(tài)學(xué)操作擴(kuò)展到三維體數(shù)據(jù)做醫(yī)學(xué)影像的形態(tài)學(xué)分析。這些方向我都在探索中有機(jī)會(huì)再單獨(dú)寫文章分享。