
1. 从图像连通域到业务落地为什么要认真学连通性检测先说我自己的结论连通性检测是图像处理里最容易被低估的一个基础操作。很多人一开始接触 OpenCVSharp注意力全放在边缘检测、模板匹配、人脸识别这些听起来高大上的功能上连通域反倒成了顺带一提的小知识点。但实际做项目做到后面你会发现连通性检测几乎是无处不在的——去噪、目标计数、区域分析、字符分割、缺陷检测哪哪都有它的影子。所谓连通性检测简单说就是判断图像里哪些像素是“连在一起”的把连在一起的像素归为同一个对象再把彼此独立的对象分开。这个过程在 OpenCVSharp 里对应着 ConnectedComponents 和 ConnectedComponentsWithStats 这两个核心方法。前者返回每个像素属于第几个连通域后者在它的基础上额外返回每个连通域的外接矩形、面积、中心点等统计信息——这些统计信息才是真正干活时用得最多的部分。这篇文章我打算从一个完整项目的角度来拆连通性检测先讲清楚原理层面的两种连通关系4连通和8连通再对比三种不同的实现方式——OpenCVSharp 原生方法、自写泛洪填充、以及轮廓分析看完你应该能明白为什么实际项目里我更推荐用 ConnectedComponentsWithStats。后面我会给出一段可运行的完整代码配合一个实际的场景——统计一张图里有多少个药片、并排除噪点和粘连对象目标是让读完的人真的能照着把代码跑起来而不是停留在看懂的阶段。适合谁来读如果你是刚接触 OpenCVSharp、想系统搞明白连通域怎么用的人这篇文章可以作为你从原理到落地的完整参考如果你已经在用轮廓分析做目标提取读完之后可以对比一下两条技术路线的优劣以后选型心里更有底。2. 先搞清楚底层逻辑4连通、8连通和泛洪填充2.1 4连通和8连通的差异决定了对象怎么“连”连通性检测这件事本质上是回答一个问题两个相邻的像素算不算同一个对象这里的关键是“相邻”的定义。OpenCV 里有两种标准4连通只考虑上下左右四个方向。一个像素只跟它正上方、正下方、正左边、正右边的像素发生关系。8连通在4连通的基础上再加上左上、右上、左下、右下四个斜对角方向。也就是周围一圈8个像素全部视为邻居。用一个很直观的例子说明。想象一张图里有两个方块斜着挨在一起比如X . . X如果按4连通判定这两个方格不连通因为没有任何一个方格在另一个方格的正上下左右方向。如果按8连通判定这两个方格在对角线方向上是相邻的所以会被认为属于同一个连通域。这个差异在实践里影响非常大。举个我实际踩过的例子检测印刷电路板上的短线缺陷时导线之间常常出现很细微的“搭桥”。这种搭桥可能就是斜向的、一两个像素宽。如果你用8连通去提取连通域搭桥处会把两条本应独立的走线连成一个对象缺陷检测直接漏报。反过来如果检测的是颗粒状对象——比如药片、细胞、钢材表面的夹杂物这些对象本身形状不规则边缘经常存在斜向接触的像素用4连通反而会把同一个对象拆成好几块导致计数偏多。所以我的选择原则很简单场景推荐连通方式原因细线、电路、文字笔画4连通避免斜向“桥接”造成对象合并颗粒、块状对象8连通对象边缘斜向接触常见避免破碎拆分通用目标计数8连通提取结果更符合人眼对“一个物体”的感知2.2 泛洪填充连通性检测的“祖宗算法”理解了连通关系再看算法本身。OpenCVSharp 的 ConnectedComponents 底层用的其实是泛洪填充Flood Fill思路从某个未访问的种子像素出发沿着连通关系一层层向外扩展把所有能到达的像素标记为同一个编号撞到背景或者已经标记过的像素就停下然后换下一个未访问的种子继续。这就像往一个迷宫里倒水水会顺着所有能走的通道流到所有能到的地方直到被墙挡住。每一摊水就是一个连通域。明白这个底层逻辑有实际意义。最大的意义在于——你能预测结果出了问题能排查。比如你发现某个连通域莫名其妙包含了本不该属于它的区域脑子里第一个反应应该是是不是该像素和它之间确实存在一条连通的像素路径而你自己没看出来这时候用像素值可视化工具把中间路径逐层放大往往能发现是1像素宽的噪声点构成了“桥梁”。关于泛洪填充有一个 OpenCVSharp 使用细节很容易被忽略ConnectedComponents 函数有一个 connectivity 参数默认值其实是 8而不是很多人以为的 4。如果你需要4连通必须显式传入4。这个细节我在 3.3 节会结合代码再强调一次。2.3 基于轮廓分析提取连通域另一种思路OpenCV 里还有一种和连通性检测目标高度一致的方法——FindContours。它是先找图像里对象的边界轮廓再通过轮廓的层级关系hierarchy判断哪些边界围成了独立的对象区域。轮廓分析本身不是连通性检测但经常被拿来干同样的事分离独立目标、计算目标数量、提取目标外接矩形。它和 ConnectedComponents 的区别在于两个方面输出对象不同。连通域给出的是像素级的分组结果轮廓给出的是边界点序列。对内部空洞的处理方式不同。连通域会把整个区域连同空洞一起算作一个对象轮廓则需要按层级关系区分外轮廓和内轮廓。实际项目中我更多的做法是用 ConnectedComponentsWithStats 做快速筛选和计数用 FindContours 做需要精确边界的后续操作比如计算周长、拟合形状、绘制多边形。两者不是互相替代的关系而是上下游的关系。3. 正式进入 OpenCVSharp函数原型与参数详解3.1 两个核心函数的完整签名先看 OpenCVSharp 里两个函数的完整定义我基于比较常用的版本来说明。public static int ConnectedComponents( InputArray image, OutputArray labels, int connectivity 8, OutputArray? stats null, OutputArray? centroids null ) public static int ConnectedComponentsWithStats( InputArray image, OutputArray labels, OutputArray stats, OutputArray centroids, int connectivity 8, PixelTypes ltype PixelTypes.Labels32S )其中image输入图像必须是单通道二值图类型是 8 位也就是 byte 类型取值只有0和255。labels输出图像和输入同尺寸每个像素位置存放它所属的连通域编号。connectivity4 或 8默认8。stats输出统计信息类型是 Nx5 的矩阵每行对应一个连通域五列分别是左边界x、上边界y、宽度width、高度height、面积area。centroids每个连通域的中心点坐标类型是 Nx2 的浮点矩阵。ltypelabel 图的像素深度。默认是 Labels32S也就是32位有符号整数。实际中我基本上一直用默认值。有一点必须先说清楚返回的连通域数量是包括背景的。也就是说它返回的 N 里面编号0一定是背景真正的前景对象编号从1开始。假设你调用函数后拿到返回值 6那么实际前景对象数量是5个。忘掉这一点统计计数时被1坑过的人绝对不止我一个。3.2 预处理从彩色图到合格的二值图连通性检测对输入图像的要求很严格单通道、二值化、前景是白色、背景是黑色。所以正式调用 ConnectedComponents 之前预处理链路几乎是固定的// 1. 读取彩色图像 Mat src Cv2.ImRead(input.jpg, ImreadModes.Color); // 2. 转灰度 Mat gray new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // 3. 二值化 Mat binary new Mat(); Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);这里的二值化方式选择值得展开说。多数自然图像的光照不均匀问题用固定阈值很难一次搞定所以我默认优先用 Otsu 自动阈值——它的思想是最大化前景和背景两类像素的类间方差不依赖人工调参对大多数灰度分布呈双峰的图像效果都不错。但 Otsu 也不是万能的。如果目标区域和背景的灰度分布高度重叠比如白色药片放在浅色桌面上Otsu 很可能把一部分背景也归为前景。这种情况下我一般会调整策略先试试 Otsu 有没有明显的问题不行就转用自适应阈值 AdaptiveThreshold它是按局部邻域窗口动态计算阈值的能应对光照渐变再不行就考虑只针对特定颜色范围做筛选比如用 HSV 色彩空间提取指定色相的像素。预处理质量直接决定后续连通域分析的效果这个环节值得花时间调。3.3 第一次运行四行代码实现连通域可视化先跑一个最小示例建立直观感受。以下代码读入一张二值图调用 ConnectedComponentsWithStats然后把每个连通域用不同颜色画出来。using OpenCvSharp; Mat binary Cv2.ImRead(shapes.png, ImreadModes.Grayscale); Mat binaryMask new Mat(); Cv2.Threshold(binary, binaryMask, 128, 255, ThresholdTypes.Binary); Mat labels new Mat(); Mat stats new Mat(); Mat centroids new Mat(); int count Cv2.ConnectedComponentsWithStats( binaryMask, labels, stats, centroids, connectivity: 8 ); // 为每个连通域生成一个随机颜色 Mat result Mat.Zeros(binaryMask.Size(), MatType.CV_8UC3); for (int i 1; i count; i) { // 读取第i个连通域的统计信息 int x stats.Atint(i, 0); int y stats.Atint(i, 1); int w stats.Atint(i, 2); int h stats.Atint(i, 3); int area stats.Atint(i, 4); if (area 50) continue; // 过滤掉小的连通域 Scalar color new Scalar( (i * 37) % 255, (i * 97) % 255, (i * 157) % 255 ); // 在原图上画矩形框 Cv2.Rectangle(result, new Rect(x, y, w, h), color, 2); } using (new Window(labeled, result)) { Cv2.WaitKey(0); }这段代码里我故意做了两件实践中比较关键的事跳过编号0。因为编号0是背景无论是统计还是绘制都应该直接从1开始。按面积过滤。图像里难免有噪声点它们是面积很小的连通域直接滤掉能省掉后面很多麻烦。第一次跑通这个例子后你对连通域的印象就不再是抽象概念而是“界面里一个个带框的彩色块”。接下来就可以进入真正的实战。4. 项目实战药片图像中的目标计数与杂质剔除4.1 业务问题的明确化为了讲清楚连通性检测的实际用法我设计了一个模拟项目统计一张摆满药片的图片里一共有多少个药片。同时图片里存在两类干扰细微噪点比如桌面纹理、灰尘颗粒它们面积很小。相互粘连的药片有些药片紧挨在一起甚至轻微重叠。这个场景在制药行业的视觉质检里非常常见产线上的药板、瓶装药片计数、胶囊排列检测核心逻辑都类似。不夸张地说连通性检测加面积筛选就能解决这个需求的80%。正式开发前我把问题拆成四步图像预处理转灰度、二值化让药片变成白色前景。连通域提取调用 ConnectedComponentsWithStats 获得所有连通域的统计信息。噪点过滤利用面积字段把大小不合理的连通域剔除。粘连处理对于面积离群的大连通域根据标准单粒药片的面积估算其包含的药片数量。4.2 用面积判断“这个连通域是什么”拿到 stats 矩阵以后怎么用它来区分药片、噪点和粘连块思路非常直接。先设定一个参考面积——单粒药片在图像中占的像素数。这个值怎么来两个办法先跑一次全图连通域统计把面积数据分布打印出来观察几个平台区间或者手工框选一个干净的单粒药片计算它占据的像素面积。有了参考面积 A判断逻辑就是面积远小于 A 的区域比如小于 0.3 × A判定为噪点丢弃。面积接近 A 的区域在 0.3 × A 到 1.5 × A 之间判定为单个药片计数加1。面积显著大于 A 的区域大于 1.5 × A判定为粘连区域记入待处理队列用后续的分离算法应对。阈值取 0.3 和 1.5 是我反复测试后比较稳的一个区间。实际项目里肯定要根据图像分辨率和对象形状重新标定但思路是一样的。下面给出完整的实现。为了代码可直接复现注释我会写得比较细。using OpenCvSharp; class PillCounter { public static void Run(string imagePath) { // 1. 读取图像并预处理 using Mat src Cv2.ImRead(imagePath, ImreadModes.Color); using Mat gray new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // Otsu自动阈值二值化 using Mat binary new Mat(); Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu); // 2. 连通域分析 Mat labels new Mat(); Mat stats new Mat(); Mat centroids new Mat(); int count Cv2.ConnectedComponentsWithStats( binary, labels, stats, centroids, connectivity: 8 ); // 单粒药片参考面积在实际项目中由标定得到 // 这里假设为 2500 像素 int refArea 2500; int singleCount 0; int mergedCount 0; for (int i 1; i count; i) { int area stats.Atint(i, 4); if (area refArea * 0.3) { continue; // 噪点 } else if (area refArea * 1.5) { singleCount; // 单粒药片 } else { mergedCount; // 粘连区域需要进一步处理 } } Console.WriteLine($单个药片数量: {singleCount}); Console.WriteLine($粘连区域数量: {mergedCount}); } }这段代码是目前为止的核心骨架。接下来的工作是处理粘连区域这也是实际项目里真正的难点。4.3 粘连区域的分离分水岭算法与形态学腐蚀粘连怎么处理我先说两个思路再说我推荐的顺序。第一个思路是形态学腐蚀。既然粘连药片会在接触处形成“狭窄走廊”那用腐蚀操作反复收缩前景区域直到粘连断开。每断开一次就多出一个独立连通域。这个方法实现简单但副作用也很明显腐蚀会让每个药片的面积变小形状失真腐蚀过头甚至会把小药片整个消掉。所以腐蚀一般只用于重度粘连且允许面积误差的场景。第二个思路是分水岭算法。它是这问题的经典解法。思路是把图像看成一张高低起伏的地形图灰度值高的区域看成山峰灰度值低的看成山谷。药片的中心区域因为内部比较明亮相当于山峰药片之间的边界、背景区域相当于山谷。然后从每个局部最低点开始“注水”水面慢慢上涨不同洼地即将汇合之处就筑起分水岭把两个对象分开。OpenCVSharp 里基于距离变换的分水岭流程大致是public static Mat SeparateMergedPills(Mat binaryImg) { // 1. 计算每个前景像素到最近背景的距离 Mat dist new Mat(); Cv2.DistanceTransform(binaryImg, dist, DistanceTypes.L2, DistanceTransformMasks.Mask3); // 2. 对距离图做阈值得到“确定属于药片内部”的种子区域 Mat seeds new Mat(); Cv2.Threshold(dist, seeds, 0.5 * dist.MaxVal(), 255, ThresholdTypes.Binary); // 3. 找种子的连通域给每个种子一个唯一编号 Mat markers new Mat(); Cv2.ConnectedComponents(seeds, markers, connectivity: 8); // 4. 用分水岭算法在标记图上完成分割 Mat result new Mat(); Cv2.Watershed(new Mat(), result); // 简化示意实际需要三通道彩色图像 return result; }这里我必须诚实地说明分水岭算法不是三行代码就能完美跑通的实际调试时经常需要配合形态学开闭运算来控制种子区域的数量和位置。但如果粘连场景在项目里反复出现花时间把分水岭调通是值得的。我个人实践经验是先分两阶段走。第一阶段用面积阈值筛选出粘连区域第二阶段对这些区域单独做分水岭而不是对全图做分水岭。原因很简单全图分水岭会把大量正常药片也卷入计算效率低且容易误分割。只在嫌疑区域上做精细化处理既稳又快。4.4 效果评估我怎么判断“结果是对的”做图像处理的都知道没有量化的评估“效果不错”就是空话。在这个药片计数任务里我常用的评估方式有三种人工目视验证生成标记图把每个连通域用不同颜色绘制出来肉眼确认有没有错分、漏分。交叉验证把程序计数结果和人工计数结果对比误差率控制在 1% 以内才算及格。像素级验证手工标注一张“标准答案图”每个药片区域一种编号然后比较算法编号和标准答案编号的一致性。这个最严谨但制作成本也最高。实际项目中我建议至少做到前两步。很多人跑完代码看一眼没报错就完事了这个习惯在视觉项目里很危险——算法不报错不代表结果正确。5. 三种连通域提取方式对比ConnectedComponents、泛洪填充与轮廓分析5.1 三种方式的核心取舍我在文章开头提到了三种实现路径这里把它们的对比表格化方便快速参考。维度ConnectedComponentsWithStats自写泛洪填充FindContours 轮廓分析输出像素标签 区域统计自定义标签图边界坐标 层级关系性能高底层C实现取决于写法通常较慢高统计信息自带面积、外接矩形、中心点需要自己统计需要自己计算面积空洞处理自动作为区域整体需要额外编码通过层级区分适合场景计数、筛选、区域测量学习原理、研究算法边界精确提取、形状分析有件事值得单独强调自写泛洪填充虽然性能通常不如 OpenCV 原生方法但如果你是做算法学习或者特殊领域比如自定义像素连通规则、处理非矩形邻域它依然是不可替代的手段。我的建议是业务代码用 ConnectedComponentsWithStats学习研究场合可以自己实现一遍泛洪填充两者不冲突。5.2 泛洪填充的代码级实现示例为了帮大家彻底理解连通域的原理这里给一个非常精简的泛洪填充实现。它按4连通或8连通遍历二值图给每个连通域打标签。public static int FloodFillLabels( Mat binary, out Mat labels, int connectivity 8 ) { int rows binary.Rows; int cols binary.Cols; labels Mat.Zeros(binary.Size(), MatType.CV_32SC1); // 四连通或八连通的邻域偏移 int[] dx { -1, 0, 1, 0, -1, -1, 1, 1 }; int[] dy { 0, -1, 0, 1, -1, 1, -1, 1 }; int neighborCount connectivity 8 ? 8 : 4; int currentLabel 0; var stack new List(int r, int c)(); for (int r 0; r rows; r) { for (int c 0; c cols; c) { if (binary.Atbyte(r, c) 0 || labels.Atint(r, c) ! 0) continue; currentLabel; stack.Clear(); stack.Add((r, c)); labels.Set(r, c, currentLabel); while (stack.Count 0) { var (curR, curC) stack[stack.Count - 1]; stack.RemoveAt(stack.Count - 1); for (int i 0; i neighborCount; i) { int nr curR dx[i]; int nc curC dy[i]; if (nr 0 || nr rows || nc 0 || nc cols) continue; if (labels.Atint(nr, nc) ! 0) continue; if (binary.Atbyte(nr, nc) 0) continue; labels.Set(nr, nc, currentLabel); stack.Add((nr, nc)); } } } } return currentLabel; }这段代码虽然和 OpenCV 底层实现相比效率差很多但它把“连通”的定义落到了具体代码上。以后你再看到 OpenCV 的 connectivity 参数就能直观地知道它在改什么——改的就是邻域数组里的那几个偏移量。5.3 为什么最终选型我会用 ConnectedComponentsWithStats回到现实中的技术选型问题。我见过不止一个项目在一开始选择了 FindContours 来做目标计数后期发现还得核对层级关系、计算外接矩形代码越写越绕。其实这活儿用 ConnectedComponentsWithStats 一行统计就够。反过来如果项目需要精确的外围边界去做形状分析连通域给出的粗略外接矩形又不够用这时候必须上 FindContours。选型的关键是明确你的核心目标目标是“有没有”和“有几个”优先用 ConnectedComponentsWithStats。目标是“边缘长什么样”优先用 FindContours。目标是“按自定义规则蔓延像素”自己写泛洪填充。至于性能在一般工业场景下比如200万像素的图像ConnectedComponentsWithStats 耗时通常在几十毫秒以内完全不需要担心。真正耗性能的反而是前面的高斯模糊、边缘检测这类操作。6. 实战中几乎必踩的坑边界、类型与参数误区6.1 输入图像类型不对导致没有结果这是新手最常见的问题。ConnectedComponentsWithStats 要求输入必须是8位单通道二值图。如果你直接拿三通道的彩色图传进去或者拿一副灰度分布范围正常的图直接传进去结果往往是一堆莫名其妙的连通域甚至函数直接报错。二值化后的图像素值只有0和255两种。我看代码时会专门检查这一点如果输入图像不是二值图先做二值化千万别偷懒。6.2 统计结果从编号1开始背景是0这个坑我在前面已经提过但因为它太重要值得再单独展开说。OpenCVSharp 中返回的连通域编号0永远是背景区域。所以当你遍历连通域时必须从 i 1 开始而不是从 0 开始。如果你从 0 开始遍历那么你统计的第一个“对象”实际上是整张图的背景。一旦背景里有多个不相连的区域——这在二值图中极为常见——背景被算作一个大对象面积可能占到全图面积的80%你的所有阈值判断都会崩。我在实际代码调试中看到输出数量整体偏大时第一反应就是检查遍历起点。6.3 面积过滤阈值怎么定才科学面积阈值不是一个拍脑袋的固定值。它在不同分辨率、不同镜头距离下差异巨大。我的做法分三步先用完整统计结果打印出所有面积人工观察区间分布。框选一个理想对象比如一个干净药片得到一个参考值。根据参考值上下浮动设定过滤区间然后跑小样本验证调整区间。永远不要写死一个绝对面积阈值到生产代码里。正确的做法是在初始化阶段动态标定参考面积或者把阈值放到配置文件中。6.4 图像边界处的连通域需要特殊考虑还有一个容易忽略的细节边缘部分的目标对象可能被图像边界截断。如果一个药片只有一半出现在画面里它的面积可能只有完整药片的一半这时它会因为面积过小而误判为噪点。处理方案有两种如果你只关心完整对象直接忽略边缘区域比如只统计外接矩形完全落在图像内部的对象。如果你需要统计边缘的不完整对象需要单独建立“截断对象”的判断规则。这一步虽然看场景需求但在业务项目里最好提前想清楚否则上线后第一个月就会因为边缘情况提一堆 bug。7. 优化与扩展从单张图片到视频流和大批量处理7.1 视频流里的实时计数连通域检测并不局限于静态图片。把上面的代码嵌入视频帧循环就是一个简单的实时计数系统。VideoCapture capture new VideoCapture(0); Mat frame new Mat(); Mat gray new Mat(); Mat binary new Mat(); while (true) { capture.Read(frame); if (frame.Empty()) break; Cv2.CvtColor(frame, gray, ColorConversionCodes.BGR2GRAY); Cv2.Threshold(gray, binary, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu); Mat labels new Mat(); Mat stats new Mat(); Mat centroids new Mat(); int count Cv2.ConnectedComponentsWithStats( binary, labels, stats, centroids, connectivity: 8 ); // 在这里做业务处理…… Cv2.ImShow(frame, frame); if (Cv2.WaitKey(30) 27) break; // Esc退出 }视频流处理有一个额外问题目标运动带来的闪烁和抖动会影响计数稳定性。比如药片刚从画面外移入时面积从0增长到正常值这个过程会导致它在一段时间内被当成噪点。比较成熟的解决思路是加“跟踪状态机”对每个目标维护一个状态连续 N 帧面积稳定才确认它是个有效目标连续 M 帧消失才从列表中移除。这个思路其实已经超出连通域本身的范围涉及到简单的目标跟踪了。但如果你要做实时计数这部分是无法回避的。7.2 性能优化ROI 裁剪与并行如果图像尺寸很大比如5000万像素的航拍图直接全图跑连通域可能比较吃内存。优化手段有两种第一种先做粗略的兴趣区域ROI裁剪。如果目标集中在图像某个局部区域只对该区域执行连通域检测即可。这在工业产线上非常常见因为相机视野固定目标出现的区域基本是固定的。第二种分块并行。把大图切分成多个小块分别执行连通域检测再合并结果。但这个方案有个麻烦跨块的连通域会被切断。合并时需要用标签偏移量和跨块边界匹配去恢复实现复杂度会上升不少。建议只有在性能确实不够用时才考虑。7.3 与机器学习结合的可能性连通性检测还有一个常常被忽略的价值——它可以把像素级信息压缩成对象级信息为后续的机器学习分类提供输入。比如你有一个药片碎粒检测需求先用连通域把每个目标切出来统一缩放到固定尺寸再丢给分类器判断是好药片还是碎粒。这个流程里连通域起到的就是“目标检测分割前置”的作用。再比如工业质检里的表面划痕检测连通域统计能快速给出划痕区域的位置和面积分布作为异常特征送入分类模型和传统视觉特征互补。8. 把连通性检测应用到更多场景中去学了连通性检测之后能做的事情其实远超“数药片”这一件事。一句话总结我自己的感悟连通性检测的本质是把一张像素图抽象成一张“对象图”。像素只是原始材料连通域才是语义层面的最小单元。谁先掌握这种抽象能力谁就能在图像处理项目中省下一半的力。举几个我做过或见过的实际场景作为参考文档扫描件的段落分离把文字行连成连通域行间距大于字间距时文字行会成为独立的连通域从而实现对文本行的切分。医学影像中的细胞计数二值化后的细胞图像连通域数量和面积分布直接对应细胞数量和尺寸。工业外观检测螺丝、螺母、表面划痕、气泡都能用连通域定位后做尺寸测量。遥感图像中的水体提取经过阈值分割的水域连通域可用于湖泊面积统计。每个场景的核心套路大同小异预处理 → 连通域提取 → 按统计信息筛选 → 结果可视化。框架一旦搭好换换参数就能应对不同图像。所以这篇分享真正想传达的并不是某一个函数怎么调用而是希望你能形成这种“以对象为单位思考图像”的思维方式。下一次遇到“这堆像素代表着什么”的问题时你能第一时间想到先跑一遍连通域看看这里面到底藏着多少个独立的东西。