提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、Blob的基本原理是什么?

Blob分析是对图像中相同像素的连通阈进行分析。其过程其实就是将图像进行二值化,分割得到前景和背景,然后进行联通区域检测,从而得到Blob块的过程。也可以讲Blob则指图像中具有相似灰度、颜色或纹理特征的连通像素区域。Blob分析则通过对这些区域进行检测、分割、特征提取与筛选,实现目标定位的功能。

二、Blob分析的基本流程

采集图像—>预处理—>分割图像(用于区分前景先像素和背景像素)—>特征提取(面积、圆度、角度等。),需要注意在实际的工程应用中Blob分割会很复杂,需要处理更多的步骤,其原因有很多种,比如杂乱和或不均匀的照明,或者图像背景复杂等。
在这里插入图片描述

1.Halcon实现

* 
* ==========================================
* 基本流程,Blob 分析流程:预处理 → 二值化 → 连通区域 → 特征提取 → 筛选 → 可视化
*定位芯片(Die)区域->提取焊线与球键合区域->通过形态学处理分离球键合->筛选、排序球键合并测量直径
* ==========================================


dev_update_window ('off')//初始化环境:关闭窗口自动更新,提高执行速度;作用:关闭窗口的自动刷新,避免后续每步操作都重绘窗口,提升处理效率
dev_close_window ()//关闭当前可能存在的所有窗口,避免干扰
dev_open_window (0, 0, 728, 512, 'black', WindowID)//打开一个新窗口,(0,0):窗口左上角坐标;728,512:窗口宽高(与图像尺寸匹配);'black':窗口背景色;WindowID:输出参数,窗口句柄,后续操作窗口需用到
read_image (Bond, 'die/die_03')//读取并显示原始图像
dev_display (Bond)//在当前窗口显示原始图像
set_display_font (WindowID, 14, 'mono', 'true', 'false')// 设置显示字体
disp_continue_message (WindowID, 'black', 'true')//在窗口显示“按 F5 继续”的提示文字
stop ()//暂停程序执行
*第一步,定位芯片区域
threshold (Bond, Bright, 100, 255)//全局阈值二值化,提取“亮区域”(芯片本身比背景亮)
shape_trans (Bright, Die, 'rectangle2')//将亮区域转换为“最小外接斜矩形”(rectangle2),精确定位芯片轮廓
*可视化芯片定位结果
dev_set_color ('green')//设置后续绘制的颜色为绿色
dev_set_line_width (3)//设置绘制线条宽度为3
dev_set_draw ('margin')//设置绘制模式为“边缘绘制”(不填充区域,只画轮廓)
dev_display (Die)//在原图上叠加显示芯片的绿色矩形轮廓
disp_continue_message (WindowID, 'black', 'true')
stop ()
*第二步,裁剪芯片内部区域,提取焊线与球键合
reduce_domain (Bond, Die, DieGrey)//抠图,用芯片矩形区域(Die)裁剪原图(Bond),只保留芯片内部的灰度图
threshold (DieGrey, Wires, 0, 50)//在芯片内部二值化,提取“暗区域”(焊线和球键合比芯片背景暗)
fill_up_shape (Wires, WiresFilled, 'area', 1, 100)//按“面积”填充二值区域内的小孔洞(焊线或球内部可能有小空洞)
dev_display (Bond)//重新显示原图(清除之前的绿色矩形)
dev_set_draw ('fill')//设置绘制模式为“填充”
dev_set_color ('red')//设置绘制颜色为红色
dev_display (WiresFilled)//在原图上叠加显示填充后的红色焊线与球区域
disp_continue_message (WindowID, 'black', 'true')
stop ()
* 第三步:形态学开运算,分离球键合(去掉焊线)
opening_circle (WiresFilled, Balls, 15.5)//用“圆形结构元素”做开运算(先腐蚀后膨胀)
dev_set_color ('green')
dev_display (Balls)// 用绿色显示分离后的球键合
disp_continue_message (WindowID, 'black', 'true')
stop ()
*第四步,筛选和排序
connection (Balls, SingleBalls)//连通区域标记,将连在一起的球区域拆分为独立的单个球区域
select_shape (SingleBalls, IntermediateBalls, 'circularity', 'and', 0.85, 1.0)//按“圆度”筛选区域,只保留接近圆形的球(排除可能的杂质或未分离干净的焊线)
sort_region (IntermediateBalls, FinalBalls, 'first_point', 'true', 'column')//对筛选后的球按“列坐标”排序(从左到右排列)
dev_display (Bond)
dev_set_colored (12)
dev_display (FinalBalls)//显示排序后的彩色球区域
disp_continue_message (WindowID, 'black', 'true')
stop ()
* 第五步:测量球键合的直径并显示结果
smallest_circle (FinalBalls, Row, Column, Radius)//计算每个球的“最小外接圆”,得到圆心坐标和半径
NumBalls := |Radius|//统计球的数量
Diameter := 2 * Radius//计算每个球的直径(直径=2*半径,数组运算)
meanDiameter := mean(Diameter)//计算所有球的平均直径
minDiameter := min(Diameter)//计算所有球的最小直径(可用于质量判断,如直径过小则不合格)
dev_display (Bond)
disp_circle (WindowID, Row, Column, Radius)
dev_set_color ('white')//设置文字颜色为白色
disp_message (WindowID, 'D: ' + Diameter$'.4', 'image', Row - 2 * Radius, Column, 'white', 'false') //在每个球的上方标注直径
dev_update_window ('on')//恢复窗口的自动更新

2.Opencv实现

using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Linq;

namespace BallBondingInspection
{
    class Program
    {
        static void Main(string[] args)
        {
            // ==========================================
            // 1. 读取图像(请替换为实际图像路径,原Halcon示例为'die/die_03')
            // ==========================================
            string imagePath = "die_03.png"; 
            Mat bond = Cv2.ImRead(imagePath, ImreadModes.Grayscale);
            if (bond.Empty())
            {
                Console.WriteLine("图像读取失败,请检查路径!");
                return;
            }

            // 转彩色图用于后续绘制(OpenCV默认灰度图无法画彩色)
            Mat bondColor = new Mat();
            Cv2.CvtColor(bond, bondColor, ColorConversionCodes.GRAY2BGR);

            // 显示原始图像(按任意键继续)
            Cv2.ImShow("1. 原始图像", bondColor);
            Cv2.WaitKey(0);


            // ==========================================
            // 2. 定位芯片(Die)区域(对应Halcon: threshold + shape_trans)
            // ==========================================
            // 2.1 全局阈值二值化:提取亮区域(像素值100-255)
            Mat bright = new Mat();
            Cv2.Threshold(bond, bright, 99, 255, ThresholdTypes.Binary); // >99的设为255

            // 2.2 找轮廓并按面积降序排序(取最大轮廓作为芯片)
            Cv2.FindContours(bright, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
            var sortedContours = contours.OrderByDescending(c => Cv2.ContourArea(c)).ToList();

            // 2.3 计算最小外接斜矩形(对应Halcon 'rectangle2')
            RotatedRect dieRect = Cv2.MinAreaRect(sortedContours[0]);

            // 2.4 可视化芯片定位(绿色3px宽矩形)
            Mat dieDisplay = bondColor.Clone();
            Point2f[] diePoints = dieRect.Points();
            for (int i = 0; i < 4; i++)
            {
                Cv2.Line(dieDisplay, (Point)diePoints[i], (Point)diePoints[(i + 1) % 4], Scalar.Green, 3);
            }
            Cv2.ImShow("2. 芯片定位", dieDisplay);
            Cv2.WaitKey(0);


            // ==========================================
            // 3. 裁剪芯片内部区域(对应Halcon: reduce_domain)
            // ==========================================
            // 3.1 创建芯片区域的掩码(白色为有效区域)
            Mat dieMask = Mat.Zeros(bond.Size(), MatType.CV_8UC1);
            Point[] diePointsInt = Array.ConvertAll(diePoints, p => (Point)p);
            Cv2.FillConvexPoly(dieMask, diePointsInt, Scalar.White);

            // 3.2 用掩码裁剪原图
            Mat dieGrey = new Mat();
            bond.CopyTo(dieGrey, dieMask);


            // ==========================================
            // 4. 提取暗区域(焊线+球,对应Halcon: threshold 0-50)
            // ==========================================
            Mat wires = new Mat();
            Cv2.Threshold(dieGrey, wires, 50, 255, ThresholdTypes.BinaryInv); // ≤50的设为255(暗区域)


            // ==========================================
            // 5. 填充小面积孔洞(对应Halcon: fill_up_shape 'area' 1-100)
            // ==========================================
            Mat wiresFilled = FillSmallHoles(wires, 1, 100);

            // 5.1 可视化焊线与球(红色填充)
            Mat wiresDisplay = bondColor.Clone();
            Cv2.FindContours(wiresFilled, out Point[][] wiresContours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
            Cv2.DrawContours(wiresDisplay, wiresContours, -1, Scalar.Red, -1);
            Cv2.ImShow("3. 焊线与球提取", wiresDisplay);
            Cv2.WaitKey(0);


            // ==========================================
            // 6. 形态学开运算分离球键合(对应Halcon: opening_circle 15.5)
            // ==========================================
            Mat balls = new Mat();
            int radius = 16; // 15.5取整,结构元素大小为33x33
            Mat element = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(2 * radius + 1, 2 * radius + 1));
            Cv2.MorphologyEx(wiresFilled, balls, MorphTypes.Open, element);

            // 6.1 可视化分离后的球(绿色填充)
            Mat ballsDisplay = bondColor.Clone();
            Cv2.FindContours(balls, out Point[][] ballsContours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
            Cv2.DrawContours(ballsDisplay, ballsContours, -1, Scalar.Green, -1);
            Cv2.ImShow("4. 球键合分离", ballsDisplay);
            Cv2.WaitKey(0);


            // ==========================================
            // 7. 筛选圆度+排序(对应Halcon: connection + select_shape + sort_region)
            // ==========================================
            // 7.1 找球的轮廓
            Cv2.FindContours(balls, out Point[][] singleBallsContours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);

            // 7.2 筛选圆度0.85-1.0的轮廓(圆度公式:4π*Area/Perimeter²)
            List<Point[]> validBalls = new List<Point[]>();
            foreach (var contour in singleBallsContours)
            {
                double area = Cv2.ContourArea(contour);
                double perimeter = Cv2.ArcLength(contour, true);
                if (perimeter == 0) continue;

                double circularity = (4 * Math.PI * area) / (perimeter * perimeter);
                if (circularity >= 0.85 && circularity <= 1.0)
                {
                    validBalls.Add(contour);
                }
            }

            // 7.3 按质心X坐标升序排序(对应Halcon 'column'排序)
            List<Tuple<Point[], double>> ballsWithCentroidX = validBalls.Select(contour =>
            {
                Moments m = Cv2.Moments(contour);
                double cx = m.M10 / m.M00; // 质心X坐标
                return Tuple.Create(contour, cx);
            }).OrderBy(t => t.Item2).ToList();

            List<Point[]> finalBalls = ballsWithCentroidX.Select(t => t.Item1).ToList();

            // 7.4 可视化筛选排序后的球(12种彩色循环)
            Mat finalDisplay = bondColor.Clone();
            Scalar[] colors = { Scalar.Red, Scalar.Green, Scalar.Blue, Scalar.Yellow, Scalar.Cyan, Scalar.Magenta, Scalar.Orange, Scalar.Purple, Scalar.Pink, Scalar.Lime, Scalar.Teal, Scalar.Indigo };
            for (int i = 0; i < finalBalls.Count; i++)
            {
                Cv2.DrawContours(finalDisplay, new Point[][] { finalBalls[i] }, -1, colors[i % colors.Length], -1);
            }
            Cv2.ImShow("5. 最终球键合检测", finalDisplay);
            Cv2.WaitKey(0);


            // ==========================================
            // 8. 测量直径并显示结果(对应Halcon: smallest_circle + disp_circle)
            // ==========================================
            Mat resultDisplay = bondColor.Clone();
            List<float> diameters = new List<float>();

            for (int i = 0; i < finalBalls.Count; i++)
            {
                // 计算最小外接圆
                Cv2.MinEnclosingCircle(finalBalls[i], out Point2f center, out float ballRadius);
                float diameter = 2 * ballRadius;
                diameters.Add(diameter);

                // 画圆(红色2px宽)
                Cv2.Circle(resultDisplay, (Point)center, (int)ballRadius, Scalar.Red, 2);

                // 标注直径(白色文字,保留4位小数)
                string text = $"D: {diameter:F4}";
                int textY = (int)(center.Y - 2 * ballRadius); // 球上方2倍半径处
                Cv2.PutText(resultDisplay, text, new Point((int)center.X - 30, textY), HersheyFonts.HersheySimplex, 0.5, Scalar.White, 1);
            }

            // 输出统计信息
            if (diameters.Count > 0)
            {
                Console.WriteLine($"检测到 {diameters.Count} 个球键合");
                Console.WriteLine($"平均直径: {diameters.Average():F4}");
                Console.WriteLine($"最小直径: {diameters.Min():F4}");
            }

            // 显示最终结果
            Cv2.ImShow("6. 直径测量结果", resultDisplay);
            Cv2.WaitKey(0);


            // 释放资源
            Cv2.DestroyAllWindows();
        }


        // ==========================================
        // 辅助函数:填充小面积孔洞
        // ==========================================
        /// <summary>
        /// 填充二值图像中面积在[minArea, maxArea]之间的孔洞
        /// </summary>
        static Mat FillSmallHoles(Mat src, int minArea, int maxArea)
        {
            Mat dst = src.Clone();

            // 找所有轮廓(包含内部孔洞,用RetrievalModes.CComp)
            Cv2.FindContours(dst, out Point[][] contours, out HierarchyIndex[] hierarchy, RetrievalModes.CComp, ContourApproximationModes.ApproxSimple);

            for (int i = 0; i < contours.Length; i++)
            {
                // 内部孔洞的特征:hierarchy[i].Parent != -1(有父轮廓)
                if (hierarchy[i].Parent != -1)
                {
                    double area = Cv2.ContourArea(contours[i]);
                    if (area >= minArea && area <= maxArea)
                    {
                        // 填充孔洞为白色
                        Cv2.DrawContours(dst, contours, i, Scalar.White, -1);
                    }
                }
            }

            return dst;
        }
    }
}

在这里插入图片描述

总结

以上就是今天要讲的内容,本文仅仅简单介绍了blob的基本原理和使用,包括了通过halcon的方式以及用opencv的方式实现相同的效果。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐