:Sobel、Canny原理與參數(shù)調(diào)優(yōu))
做計算機(jī)視覺尤其是用 OpenCV 做圖像處理的人幾乎都會和邊緣檢測打交道。邊緣檢測要做的事情很單純找出圖像里亮度突然變化的位置。但這件事幾乎是一切后續(xù)高級任務(wù)的起點(diǎn)OCR 里的字符切分、工業(yè)視覺里的缺陷定位、醫(yī)學(xué)圖像里的組織邊界、自動化設(shè)備里的工件定位底層都離不開這一步。哪怕你后面不用 Canny你也得知道梯度、閾值、非極大值抑制這些概念是從哪來的。這篇文章按照我自己做圖像處理 Demo 的實操習(xí)慣來寫先從“邊緣到底是什么”聊起再逐一把 Sobel、Scharr、Laplacian、Canny 的原理和 OpenCV 參數(shù)拆開最后整理一份常見問題的排查記錄。剛開始學(xué)的朋友可以把代碼直接抄走跑通做過一陣子項目的人建議重點(diǎn)看后面“邊緣斷裂”“偽邊緣太多”這兩個小節(jié)這些是我實際調(diào)試?yán)锓磸?fù)踩過的坑。1. 邊緣檢測到底在解決什么問題1.1 從“看懂圖像”到“計算像素變化”人眼看到一個杯子下意識就能說出它的輪廓在哪里。但計算機(jī)看到的只是一個二維數(shù)組每個像素點(diǎn)存著一個亮度值。要讓它“看到”邊緣本質(zhì)上是讓它去計算像素之間的變化程度。如果把一張圖看成亮度函數(shù)I(x, y)那么邊緣就是函數(shù)發(fā)生劇烈跳變的位置。數(shù)學(xué)上用梯度來描述這種變化梯度是一個向量包含兩個信息變化的大小幅值和變化最快的方向方向。方向恰好就是邊緣的法線方向而邊緣本身沿著垂直于梯度的方向延展。這里有一個很直觀的例子一堵垂直的黑白分界墻水平方向亮度變化劇烈豎直方向基本不變。所以用 Sobel 算子計算 x 方向梯度時分界線上的響應(yīng)會非常大y 方向梯度則幾乎為零。這就是為什么 Sobel 要分成 dx 和 dy 兩個方向分別計算而不是一上來就抹成一個數(shù)。不過梯度大并不代表一定就是物體邊緣。噪點(diǎn)造成的偽變化、花紋紋理的細(xì)小對比、陰影交界處的柔和過渡都會產(chǎn)生不小的梯度響應(yīng)。所以邊緣檢測從來不是“算一次梯度就完事”后面還跟著降噪、抑制、閾值、連接等一系列處理。1.2 邊緣檢測和輪廓提取不是一回事我見過不少初學(xué)者把邊緣檢測和輪廓提取混在一起甚至以為拿到 Canny 結(jié)果就等于完成了目標(biāo)分割。這個誤區(qū)得先掰開。邊緣檢測的輸出是一張和原圖同樣大小的響應(yīng)圖每個像素代表該位置的邊緣響應(yīng)強(qiáng)度或者經(jīng)過閾值化之后用 0 和 255 表示“是否屬于邊緣”。它是一個像素級的判別結(jié)果不一定連續(xù)也不一定閉合。輪廓提取的輸出則是一組有序的點(diǎn)集每個輪廓是一個向量自帶拓?fù)浣Y(jié)構(gòu)可以計算面積、周長、外接矩形還能做形狀匹配。輪廓通常是在邊緣圖的基礎(chǔ)上通過連通域分析得到的。換句話說邊緣是“像素屬性”輪廓是“對象屬性”。Canny 出來的圖不能直接算面積但用它做輸入去調(diào)findContours就能把一堆離散邊緣點(diǎn)拼成真正意義上的工件輪廓。后面第 5 節(jié)會專門講這個銜接流程。明白這個區(qū)別之后你就不會因為“邊緣檢測結(jié)果看起來斷斷續(xù)續(xù)”而覺得算法出了問題。邊緣檢測本來就是預(yù)處理不是終點(diǎn)。2. 核心算法選型從梯度算子到 Canny2.1 Sobel 與 Scharr一階梯度算子的分工Sobel 是最常用的一階梯度算子核心思想是差分加一點(diǎn)局部平滑。它用 3x3 的卷積核對圖像做卷積x 方向和 y 方向的核分別是Gx: -1 0 1 -2 0 2 -1 0 1 Gy: -1 -2 -1 0 0 0 1 2 1注意中間行的系數(shù)是 2不是 1這一點(diǎn)的作用是讓核自帶輕微的高斯平滑效果對噪聲有一點(diǎn)抑制作用。實際的 OpenCV 調(diào)用是這樣的import cv2 import numpy as np gray cv2.imread(demo.jpg, cv2.IMREAD_GRAYSCALE) gx cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize3) gy cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize3) mag cv2.magnitude(gx, gy) mag cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX) edge_sobel cv2.convertScaleAbs(mag)這里最關(guān)鍵的參數(shù)是ddepthcv2.CV_64F。梯度計算可能出現(xiàn)負(fù)值如果直接用 uint8負(fù)梯度會被截斷成 0半個邊緣就沒了。用CV_64F保留符號和精度最后再用convertScaleAbs轉(zhuǎn)回 8 位圖是標(biāo)準(zhǔn)操作。如果覺得 Sobel 在 3x3 窗口下的梯度精度不夠OpenCV 提供了 Scharr 算子。它同樣是 3x3但系數(shù)設(shè)計得更合理對方向變化的響應(yīng)更敏銳Gx: -3 0 3 -10 0 10 -3 0 3 Gy: -3 -10 -3 0 0 0 3 10 3調(diào)用方式有兩種一種直接用cv2.Scharr另一種是把cv2.Sobel的ksize設(shè)為 -1gx_s cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize-1) # 實際是 Scharr gy_s cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize-1) edge_scharr cv2.convertScaleAbs(cv2.magnitude(gx_s, gy_s))Scharr 的優(yōu)勢是精度高尤其適合對細(xì)小紋理、弱邊緣敏感的任務(wù)但代價是對噪聲也更敏感。先降噪再計算這一點(diǎn)不能省。2.2 Laplacian二階導(dǎo)數(shù)為什么又愛又恨Sobel 和 Scharr 都在算一階導(dǎo)數(shù)Laplacian 則直接跳到二階導(dǎo)數(shù)。它把 x 方向和 y 方向的二階偏導(dǎo)加起來得到一個各向同性的響應(yīng)不區(qū)分方向。3x3 的核長這樣0 1 0 1 -4 1 0 1 0各向同性的意思是不管邊緣朝向哪個方向Laplacian 都能給出響應(yīng)不需要像 Sobel 那樣做 dx、dy 兩次計算再合成。在 OpenCV 里調(diào)用非常簡單lap cv2.Laplacian(gray, cv2.CV_64F) lap cv2.normalize(lap, None, 0, 255, cv2.NORM_MINMAX) edge_lap cv2.convertScaleAbs(lap)但二階導(dǎo)數(shù)有個致命弱點(diǎn)它把高頻噪聲放得更大。一階梯度對噪聲的響應(yīng)已經(jīng)夠令人頭疼二階梯度直接翻倍。所以用 Laplacian 之前降噪幾乎成了強(qiáng)制步驟。常規(guī)做法是先用高斯模糊相當(dāng)于把 Laplacian of Gaussian也就是 LoG 的思想拆成兩步blur cv2.GaussianBlur(gray, (5, 5), 0) lap cv2.Laplacian(blur, cv2.CV_64F)原因也很直白高斯模糊先把高頻噪聲壓下去再求二階導(dǎo)才不會得到一張全是雪花點(diǎn)的噪聲圖。實際項目里我很少單獨(dú)拿 Laplacian 做邊緣圖它更適合做圖像銳化、輔助檢測興趣點(diǎn)這類任務(wù)。真要輸出干凈的邊緣Canny 是更合適的選擇。2.3 Canny為什么它成了事實標(biāo)準(zhǔn)Canny 之所以被叫做事實標(biāo)準(zhǔn)不是因為它發(fā)明了什么高深莫測的數(shù)學(xué)而是它把邊緣檢測拆成了一個完整的處理管線每一步都在解決一個具體問題高斯平滑降低梯度計算對噪聲的敏感度。計算梯度幅值和方向通常用 Sobel 實現(xiàn)。非極大值抑制只保留梯度方向上的局部最大值讓邊緣變細(xì)。雙閾值處理把像素分成強(qiáng)邊緣、弱邊緣、非邊緣三類。滯后連接弱邊緣只有和強(qiáng)邊緣相連才保留孤立弱響應(yīng)直接丟棄。OpenCV 的調(diào)用非常簡潔edges cv2.Canny(blur, 50, 150)這一步輸出就是一張二值邊緣圖。注意一個容易忽略的點(diǎn)OpenCV 的cv2.Canny不會替你完成高斯平滑雖然原始論文里第一步就是高斯濾波但實際函數(shù)并沒有內(nèi)置這步操作。所以調(diào)用前手動cv2.GaussianBlur不僅僅是為了降噪也是在還原完整的 Canny 流程。Canny 另一個值得用的特性是可以直接接收梯度圖作為輸入。比如你想用 Scharr 的梯度替代內(nèi)部默認(rèn)的 Sobel 梯度dx cv2.Scharr(blur, cv2.CV_64F, 1, 0) dy cv2.Scharr(blur, cv2.CV_64F, 0, 1) edges cv2.Canny(dx, dy, 50, 150)這種寫法適合對弱邊緣要求更高的場景屬于進(jìn)階玩法。理解到這個層面你對 Canny 的把握就和只會調(diào)兩行 API 的用法拉開差距了。3. 實操用 OpenCV 跑通一條完整的邊緣檢測流水線3.1 圖像預(yù)處理灰度化與降噪邊緣檢測的輸入理論上可以是彩色圖但實際項目里我?guī)缀醵际窍劝褕D像轉(zhuǎn)成灰度圖。因為亮度梯度是邊緣響應(yīng)最核心的信息彩色通道反而會引入色調(diào)變化帶來的干擾。除非任務(wù)明確要求按顏色分層否則直接gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)如果原圖本身就是灰度圖用cv2.imread(demo.jpg, cv2.IMREAD_GRAYSCALE)直接讀取更省事。降噪這一步和最終效果的關(guān)系比很多人想象的大。高斯核越大邊緣越粗越穩(wěn)定但細(xì)節(jié)丟失也越嚴(yán)重。折中方案一般從 5x5 開始試blur cv2.GaussianBlur(gray, (5, 5), 0)如果噪聲是椒鹽類型也就是圖像里有大量孤立的黑白點(diǎn)高斯模糊的效果不如中值濾波。紋理復(fù)雜的場景雙邊濾波能保住邊緣的同時壓掉平坦區(qū)域的噪點(diǎn)但速度稍慢。沒有萬能選項我的習(xí)慣是通用場景高斯、強(qiáng)噪點(diǎn)場景中值、紋理復(fù)雜且邊緣需要精確定位的場合上雙邊濾波。3.2 Sobel、Scharr、Laplacian 的 OpenCV 參數(shù)拆解先看一段能直接跑的完整代碼把三種算法放在一起對比import cv2 import numpy as np img cv2.imread(demo.jpg, cv2.IMREAD_GRAYSCALE) blur cv2.GaussianBlur(img, (5, 5), 0) # Sobel 邊緣幅值 gx cv2.Sobel(blur, cv2.CV_64F, 1, 0, ksize3) gy cv2.Sobel(blur, cv2.CV_64F, 0, 1, ksize3) sobel_mag cv2.magnitude(gx, gy) sobel_edges cv2.normalize(sobel_mag, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # Scharr 邊緣幅值 gx_s cv2.Sobel(blur, cv2.CV_64F, 1, 0, ksize-1) gy_s cv2.Sobel(blur, cv2.CV_64F, 0, 1, ksize-1) scharr_edges cv2.normalize(cv2.magnitude(gx_s, gy_s), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # Laplacian lap cv2.Laplacian(blur, cv2.CV_64F) lap_edges cv2.normalize(lap, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # Canny canny_edges cv2.Canny(blur, 50, 150) cv2.imshow(sobel, sobel_edges) cv2.imshow(scharr, scharr_edges) cv2.imshow(laplacian, lap_edges) cv2.imshow(canny, canny_edges) cv2.waitKey(0) cv2.destroyAllWindows()Sobel 的ksize可以取 1、3、5、7數(shù)值越大核覆蓋范圍越大邊緣響應(yīng)越平滑但定位精度會下降。常規(guī)圖像用 3 就夠了。scale和delta這兩個參數(shù)日常很少動它們的作用是在卷積后對結(jié)果做一次線性縮放和偏移常用于把梯度結(jié)果直接疊加到原圖做增強(qiáng)。cv2.convertScaleAbs是調(diào)試階段最容易被忽略的函數(shù)。它不只是做類型轉(zhuǎn)換還會對負(fù)值取絕對值再把浮點(diǎn)結(jié)果截斷到 0 到 255。如果你用dst np.uint8(gx)這種寫法負(fù)的梯度會被取?;乩@產(chǎn)生大量錯誤的亮區(qū)這個坑我見過不止一次。3.3 Canny 參數(shù)調(diào)優(yōu)高低閾值和 L2 范數(shù)Canny 的兩個閾值是兩個完全不同角色的門衛(wèi)。threshold2也就是高閾值決定什么梯度算“強(qiáng)邊緣”只有高于它才會被當(dāng)作邊緣起點(diǎn)。threshold1是低閾值決定什么梯度算“弱邊緣”弱邊緣必須和強(qiáng)邊緣連在一起才保留。所以調(diào)參順序很重要。先調(diào)高閾值把“絕對肯定是邊緣”的地方找出來再調(diào)低閾值把和強(qiáng)邊緣相連的弱響應(yīng)撿回來。低閾值不是越多越好給太低了本來被抑制的噪聲響應(yīng)會通過連接關(guān)系“復(fù)活”。常見起點(diǎn)是threshold1 : threshold2 1 : 2或者1 : 3也就是edges cv2.Canny(blur, 50, 150)如果不知道閾值怎么給有一個我在 OpenCV 示例里學(xué)到的中位數(shù)啟發(fā)式方法v np.median(blur) sigma 0.33 lower int(max(0, (1 - sigma) * v)) upper int(min(255, (1 sigma) * v)) edges cv2.Canny(blur, lower, upper)它的思路是用圖像中值灰度估算整體背景亮度再往兩側(cè)擴(kuò)出一個范圍。這個方法對光照均勻、前景背景灰度區(qū)分明顯的圖像效果不錯但對雙峰直方圖或者大面積高光的圖像會失效。所以它只是起點(diǎn)不是標(biāo)準(zhǔn)答案。L2gradient參數(shù)也需要關(guān)注。False時梯度幅值按|gx| |gy|算計算快True時按sqrt(gx^2 gy^2)算更接近真實梯度模長但閾值需要重新調(diào)。同樣的 50 和 150在兩種模式下效果會有可見差異。3.4 用 trackbar 做實時調(diào)參別靠瞎猜邊緣檢測最忌猛改一通代碼然后重新運(yùn)行。我強(qiáng)烈建議在調(diào)試階段加兩個滑動條直接觀察閾值變化對結(jié)果的影響cv2.namedWindow(canny) cv2.createTrackbar(low, canny, 0, 255, lambda x: None) cv2.createTrackbar(high, canny, 0, 255, lambda x: None) while True: low cv2.getTrackbarPos(low, canny) high cv2.getTrackbarPos(high, canny) if high low: high low 1 edges cv2.Canny(blur, low, high) cv2.imshow(canny, edges) if cv2.waitKey(1) ord(q): break cv2.destroyAllWindows()把low和high綁到兩個滑動條上邊拖邊看通常幾分鐘就能找到一組合適的參數(shù)。這個小工具比任何理論公式都直觀也是我調(diào)所有視覺算法時的固定開場。4. 常見問題與排查技巧實錄4.1 邊緣斷裂是閾值高了還是細(xì)節(jié)沒了Canny 結(jié)果出現(xiàn)斷裂幾乎是所有項目都會遇到的第一個問題。很多人第一反應(yīng)是把閾值調(diào)低但如果斷的是弱邊緣單純調(diào)低threshold1可能沒用因為弱邊緣只有在和強(qiáng)邊緣連接時才會保留。排查順序我記得很清楚先看斷口附近有沒有強(qiáng)邊緣沒有強(qiáng)邊緣就調(diào)低threshold2有強(qiáng)邊緣但中間一段梯度很弱就調(diào)低threshold1兩種都調(diào)下來還是斷說明是降噪過度或者分辨率不夠把高斯核從 5x5 改回 3x3 試試。一個我經(jīng)常用的補(bǔ)丁手段是形態(tài)學(xué)閉運(yùn)算。閉運(yùn)算先膨脹再腐蝕能把相距幾個像素的斷裂口接起來kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) edges_closed cv2.morphologyEx(canny_edges, cv2.MORPH_CLOSE, kernel)注意閉運(yùn)算只適用于邊緣斷裂距離很小的情況核太大反而會把鄰近對象連接成一片。如果斷裂口經(jīng)常發(fā)生在直線段上還可以直接跳過形態(tài)學(xué)用 HoughLinesP 的maxLineGap參數(shù)容忍斷點(diǎn)這個思路在后端任務(wù)里更干凈。4.2 偽邊緣太多噪聲去哪兒了邊緣多不一定是好事。紙面紋理、地磚花紋、金屬表面反光都會制造大量梯度響應(yīng)。這類問題不在閾值上而在預(yù)處理上。我的一個經(jīng)驗是當(dāng) Canny 輸出里出現(xiàn)大量細(xì)碎短線時先不要動閾值回頭加大高斯模糊的核或者改用雙邊濾波。雙邊濾波能在保持主邊緣清晰的同時壓掉平坦區(qū)域的紋理噪聲對文檔掃描這類場景特別適用filtered cv2.bilateralFilter(gray, d9, sigmaColor75, sigmaSpace75) edges cv2.Canny(filtered, 50, 150)還有一種情況是閾值太低導(dǎo)致噪聲梯度被當(dāng)成邊緣。如果整張圖都是密密麻麻的細(xì)點(diǎn)說明低閾值已經(jīng)低到把噪聲都放進(jìn)來了。把threshold1往上抬或者用中值濾波先把孤立噪點(diǎn)去掉再看結(jié)果。原理上要明白灰度圖的邊緣只是亮度變化它分不清“物體的真正邊界”和“表面的花紋邊界”。邊緣檢測本身沒有語義要過濾掉那些不該出現(xiàn)的邊緣靠的是預(yù)處理策略和應(yīng)用層的后處理。4.3 邊緣太粗拿什么做細(xì)化和勾邊Sobel 這類梯度算子的輸出天生就有多條響應(yīng)帶看起來邊緣很粗。Canny 經(jīng)過非極大值抑制之后通常是一條像素寬的細(xì)線但如果項目里需要嚴(yán)格細(xì)化比如測量筆畫寬度或者生成激光雕刻路徑就還需要額外處理。OpenCV 的擴(kuò)展模塊里有一個專門的細(xì)化函數(shù)屬于ximgproctry: import cv2.ximgproc thin cv2.ximgproc.thinning(canny_edges) except ImportError: print(需要安裝 opencv-contrib-python)這個函數(shù)會把二值邊緣縮成最細(xì)的骨架但要注意它不是提高精度只是改變表達(dá)形式。如果原始邊緣就不準(zhǔn)細(xì)化只會讓錯誤結(jié)論更精確。另外一個容易踩的坑是有些朋友把sobel_mag當(dāng)作最終邊緣圖來觀察發(fā)現(xiàn)邊緣“又粗又虛”然后回頭調(diào)各種參數(shù)。梯度幅值圖本來就是多灰度級的響應(yīng)帶必須經(jīng)過閾值化和后續(xù)處理才能當(dāng)邊緣用。判斷邊緣粗細(xì)之前先確認(rèn)你看的是哪個函數(shù)的結(jié)果。4.4 性能問題實時視頻里怎么做邊緣檢測有人覺得 Canny 很重其實它在普通 CPU 上的表現(xiàn)相當(dāng)不錯。以 512x512 的灰度圖為例Sobel 和 Laplacian 都是毫秒級操作Canny 通常也只需要幾十毫秒以內(nèi)實時視頻流里瓶頸往往不在 Canny而在圖像尺寸、格式轉(zhuǎn)換和 Hough 這類后續(xù)操作。實時場景里我第一個動作是把處理分辨率降下來。1080p 的幀直接往下采樣到 640 寬邊緣檢測的效果在多數(shù)任務(wù)里損失不大但速度能提升好幾倍。其次是固定曝光環(huán)境下中位數(shù)啟發(fā)式閾值不需要每幀都重新算取一次初值之后只做手工微調(diào)就夠了。另一個細(xì)節(jié)是OpenCV 的 UMat 在部分機(jī)器上能借助 OpenCL 加速但個人電腦上效果不一定明顯。真到了性能瓶頸先profile再決定要不要上 GPU。大多數(shù)實時任務(wù)把分辨率和預(yù)處理優(yōu)化好之后CPU 管線已經(jīng)完全夠用。4.5 常見問題速查表現(xiàn)象可能原因處理方向邊緣斷裂高閾值過高 / 弱邊緣未連接降低 threshold2 或 threshold1形態(tài)學(xué)閉運(yùn)算偽邊緣大量噪聲或紋理干擾加大高斯核、中值濾波、雙邊濾波邊緣太粗使用了梯度幅值而非閾值化結(jié)果確認(rèn)輸出來源必要時用 ximgproc.thinning邊緣整體偏移降噪過度 / 核尺寸過大縮小高斯核或 ksize實時處理卡頓分辨率過高 / 中間轉(zhuǎn)換太多降采樣避免逐幀計算全局統(tǒng)計量減少不必要拷貝5. 從邊緣檢測往前走輪廓與直線5.1 用 findContours 把邊緣連通成輪廓邊緣檢測結(jié)果再干凈在多數(shù)應(yīng)用里也只是中間產(chǎn)物。真正要拿來做邏輯判斷的是輪廓。把 Canny 輸出交給findContours是最典型的連接方式contours, hierarchy cv2.findContours( canny_edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) result cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR) cv2.drawContours(result, contours, -1, (0, 255, 0), 2)RETR_EXTERNAL表示只取最外層輪廓適合想找物體外邊界的情況RETR_TREE會保留嵌套結(jié)構(gòu)適合有孔洞、包含關(guān)系的工件。CHAIN_APPROX_SIMPLE會壓縮直線段到端點(diǎn)內(nèi)存少處理快如果你需要輪廓上每個點(diǎn)的精確坐標(biāo)就用CHAIN_APPROX_NONE。這里有個容易踩的坑findContours期望輸入是單通道二值圖背景必須是黑色前景邊緣是白色。如果你把 Canny 結(jié)果反色了或者直接喂了一張灰度圖結(jié)果往往會完全不對。拿到輪廓之后就可以做很多事了算面積篩選目標(biāo)、算外接矩形做定位、算凸包判斷形狀這些都是后話。5.2 用 HoughLinesP 從邊緣里找直線如果目標(biāo)不是閉合輪廓而是一堆直線段比如文檔邊緣、工件棱邊、車道線HoughLinesP 是更直接的下一步lines cv2.HoughLinesP( canny_edges, rho1, thetanp.pi / 180, threshold80, minLineLength50, maxLineGap10 )rho1和thetapi/180是常見的投票精度threshold表示至少要有多少個點(diǎn)支持這條直線才會被接受。minLineLength過濾掉太短的線段maxLineGap用于把斷裂的小段拼接成同一條直線。這個組合用來做文檔傾斜校正非常順手。先用 Canny 找邊緣再用 Hough 找長直線聚類出主角度最后做旋轉(zhuǎn)校正。整個過程沒有復(fù)雜算法但非常實用。最后說一點(diǎn)我自己的體會。邊緣檢測看起來就是幾個 API 的事真正花時間的永遠(yuǎn)是“邊緣在你這張圖里到底意味著什么”。工業(yè)零件上的劃痕、文檔里的印刷線、自然場景里的物體邊界它們對梯度的表現(xiàn)完全不同適合的算子和閾值也不可能一套吃遍天。剛上手時不用迷戀復(fù)雜算法先把 Sobel 和 Canny 在你自己數(shù)據(jù)上的行為摸清楚把預(yù)處理手段試一遍很多所謂的神秘問題其實都是噪聲和參數(shù)在搗亂。后面再遇到更偏的任務(wù)比如亞像素邊緣、深度學(xué)習(xí)邊緣分割你會發(fā)現(xiàn)自己已經(jīng)在正確的思路上走了一大半。