先上效果圖
一種基于凹陷檢測重疊輪廓分割的方法
這兩個星期壓力大的一批,心臟都給干得亂跳了,現在高血壓+心率不齊+貧血。兄弟們保重身體啊。
簡單說下邏輯:
- 前處理:的噼里啪啦我就不說了,根據樣品來(灰度,濾波,二值化,形態學...)
- 提取輪廓并計算凸包
- 填充凸包后的輪廓 減去 原始輪廓得到凹陷區域
- 計算凹陷區域的面積,利用最大的兩個凹陷區域計算出兩個凹陷區域的最近兩點
- 兩點一條線作為分割線
步驟說起來很簡單,做起來準備上天。
要想做到好的效果就需要有過濾參數,我做了基本的四個參數
- 最小輪廓長度:用來過濾小顆粒
- 深度閾值:凸包凹陷的閾值 用來過濾凹陷淺的輪廓
- 迭代次數:我們上面只是拿了單個輪廓兩個最大凹陷區域進行分割,只適合兩個規則形狀的重疊,那三個呢 四個呢 無數個呢(管他那么多就是干)
- 線寬:分割線的寬度 最好要為2,別問 問就是坑
下面看看不同參數的效果:
迭代次數:可以簡單理解我要分為幾個顆粒?
深度閾值:越小 小凹陷就越多
效果就這樣了,這是基于形狀的,但是都說基于距離變換+分水嶺的好。先貼上代碼吧白嫖兄弟們
!!!這是我封裝的方法不一定適合你 參數自己傳進來就行。最好別用,不一定好用。打開思路很重要..........
比如:根據長軸作為分界線 對應點是不是就可以不用那么多迭代???更好的?骨架作為分界線???迭代次數應該寫在輪廓遍歷里面???當然是的,但是我腦子不想思考了才來寫個文章。
public ProcessingResult ProcessImage(Mat src, Dictionary<string, object> parameters, Mat? originalMat = null)
{if (!src.IsValidMat()) return new ProcessingResult();try{int depthThreshold = parameters.GetValueOrDefault("depthThreshold", 100)?.ToString()?.ToInt(100) ?? 100;int lineThickness = parameters.GetValueOrDefault("lineThickness", 2)?.ToString()?.ToInt(2) ?? 2;int miniArcLength = parameters.GetValueOrDefault("miniArcLength", 20)?.ToString()?.ToInt(20) ?? 20;int iterations = parameters.GetValueOrDefault("iterations", 1)?.ToString()?.ToInt(1) ?? 1;Mat binary = new Mat();if (src.Channels() > 1){var gray = src.CvtColor(ColorConversionCodes.BGR2GRAY);double otsuThresh = Cv2.Threshold(src, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);}else{binary = src;}Mat resultImage = binary.CvtColor(ColorConversionCodes.GRAY2BGR); // 彩色圖方便顯示for (int iteration = 0; iteration < iterations; iteration++){var contours = Cv2.FindContoursAsArray(binary, RetrievalModes.External, ContourApproximationModes.ApproxSimple);foreach (var contour in contours){try{if (contour.Length < 5) continue;double arcLen = Cv2.ArcLength(contour, true);if (arcLen < miniArcLength) continue;int[] hullIndices = Cv2.ConvexHullIndices(contour, true);if (hullIndices.Length < 3) continue;var defects = Cv2.ConvexityDefects(contour, hullIndices);if (defects == null || defects.Length == 0) continue;if (!defects.Any(a => a.Item3 / 256f > depthThreshold)) continue;Point[] hull = new Point[hullIndices.Length];for (int i = 0; i < hullIndices.Length; i++){hull[i] = contour[hullIndices[i]];}var rect = Cv2.BoundingRect(contour);using Mat mask = new Mat(rect.Height, rect.Width, MatType.CV_8UC1, Scalar.Black);Point[] TranslateContour(Point[] pts, Point offset){return pts.Select(p => new Point(p.X - offset.X, p.Y - offset.Y)).ToArray();}var hullLocal = TranslateContour(hull, rect.TopLeft);var contourLocal = TranslateContour(contour, rect.TopLeft);Cv2.DrawContours(mask, new List<Point[]>() { hullLocal }, 0, Scalar.White, -1);Cv2.DrawContours(mask, new List<Point[]>() { contourLocal }, 0, Scalar.Black, -1);var maskContours = Cv2.FindContoursAsArray(mask, RetrievalModes.External, ContourApproximationModes.ApproxSimple);if (maskContours.Length < 2) continue;var maxAreaContours = GetTop2MaxAreaContours(maskContours).Take(2).ToArray();var minDistancePoints = GetMinDistancePoint(maxAreaContours[0], maxAreaContours[1]);var distance = Math.Sqrt((minDistancePoints[0].X - minDistancePoints[1].X) * (minDistancePoints[0].X - minDistancePoints[1].X) + (minDistancePoints[0].Y - minDistancePoints[1].Y) * (minDistancePoints[0].Y - minDistancePoints[1].Y));if (distance > rect.Width / 2) continue;// 注意minDistancePoints中是mask局部坐標,轉回resultImage全局坐標:Cv2.Line(binary, minDistancePoints[0] + rect.TopLeft, minDistancePoints[1] + rect.TopLeft, Scalar.Black, lineThickness);Cv2.Line(resultImage, minDistancePoints[0] + rect.TopLeft, minDistancePoints[1] + rect.TopLeft, Scalar.OrangeRed, lineThickness);}catch (Exception){continue;}}}return new ProcessingResult(resultImage);}catch (Exception ex){Console.WriteLine(ex);return new ProcessingResult();}
}private Point[][] GetTop2MaxAreaContours(Point[][] contours)
{if (contours == null || contours.Length == 0)return new Point[0][];// 按輪廓面積降序排序,取前2個var top2 = contours.OrderByDescending(contour => Cv2.ContourArea(contour)).ToArray();return top2;
}
private Point[] GetMinDistancePoint(Point[] contour, Point[] contour1)
{if (contour == null || contour1 == null || contour.Length == 0 || contour1.Length == 0)return new Point[0];Point minP1 = new Point();Point minP2 = new Point();double minDist = double.MaxValue;foreach (var p1 in contour){foreach (var p2 in contour1){double dist = Math.Sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y));if (dist < minDist){minDist = dist;minP1 = p1;minP2 = p2;}}}return new Point[] { minP1, minP2 };
}
上面是基于凸包凹陷的,那都說分水嶺+距離變換好!!!但是,好是有前提的 對一同樣大小顆粒的分割效果是很好的。但是對大小不一效果不太行,但是我也做了優化 下面給出大體邏輯。
?一種基于距離變幻+分水嶺 檢測重疊輪廓分割的方法
- 前處理:的噼里啪啦我就不說了,根據樣品來(灰度,濾波,二值化,形態學...)
- 進行距離變換?DistanceTransform+Normalize
- 獲取前景標記??Cv2.Threshold(distTrans8u, distTrans8u, foregroundThreshold * 255, 255, ThresholdTypes.Binary);
- 創建標記圖像? Mat markers = Mat.Zeros(binary.Size(), MatType.CV_32S);
- 標記背景?? ?using var sureBg = new Mat();
Cv2.Dilate(binary, sureBg, new Mat(), iterations: 3);- 應用分水嶺算法?Cv2.Watershed(originalMat, markers);
- 前面幾步都是爛大街的 隨便一搜都有的代碼 不清楚的直接搜?距離變換+分水嶺
- 得到輪廓并繪制在一張與原圖大小相等的黑圖上并把邊界涂色
result = Mat.Zeros(originalMat.Size(), originalMat.Type());var boundaries = new Mat();Cv2.Compare(markers, new Scalar(-1), boundaries, CmpType.EQ);result.SetTo(new Scalar(255, 255, 255), boundaries);result.Row(0).SetTo(new Scalar(0, 0, 0));result.Row(result.Rows - 1).SetTo(new Scalar(0, 0, 0));result.Col(0).SetTo(new Scalar(0, 0, 0));result.Col(result.Cols - 1).SetTo(new Scalar(0, 0, 0));boundaries.Dispose();
?上一步得到了所謂分水嶺的輪廓,但是很有可能把你的小輪廓給干掉了 或者說正常的輪廓,那么我們需要合并:
這是分水嶺前后的圖片,會發現少了很多。其實也不多就那幾個。? ? ? ? ??
1.原圖減去分水嶺得到的二值圖 再做形態學處理得到 圖4? ? ? ?
2.合并分水嶺二值圖與圖四至于效果我覺得一般 邏輯嘛也覺得一般 我就是菜雞。
打個總結,牛馬不如騾子。?