【1.19 深度学习 AOI:液晶屏幕外观检测】视觉检测实战项目

项目需求

液晶屏幕外观检测

传统视觉无法解决或者很难解决,选择用深度学习 AOI 检测

样品缺陷主要在边缘,包括缺损、裂纹、压伤等缺陷特征

训练过程

收集各类缺陷样品图片,做好标注,训练,得到训练模型;

这是个漫长的过程,需要各方面配合达到更加理想的训练模型;

AOI 检测软件

包括以下功能:离线图片检测、相机实时检测、和PLC通讯;

几个重要的功能模块:相机管理模块、通讯模块、日志模块,前面都介绍过了;

离线模式

实时模式

显示图像,使用了 Image 控件,目前仅仅能显示,未实现缩放、平移等功能;

Image 和重要参数,构成了独立的用户控件,对应到一个相机工位;

界面整体比较简单,几个重要功能键,实现基础功能;

核心 AOI 检测 SDK 取自 SaigeVision,是一家韩国公司。

核心功能代码

离线检测

// 初始化
NoticeEvent("初始化", EnumLoggerType.Debug);
SaigeVisionApp.Init();
// 检测、启动 GPU
NoticeEvent("检测、启动 GPU", EnumLoggerType.Debug);
NvGpuService gpuService = new NvGpuService();
gpuService.DetectConnectedGPUs();
// 计时
Stopwatch stopwatch = new Stopwatch();
// 导入模型
NoticeEvent("导入模型", EnumLoggerType.Debug);
Uri modelUri = new Uri(MyImagePresentVM.ModelName);
using (SegmentationEngine segEngine = EngineLoader.LoadFrom<SegmentationEngine>(modelUri))
    NoticeEvent("设置检测参数", EnumLoggerType.Debug);
    SegInspectionOptions option = segEngine.ReadInspectionOptions();
    option.TimeLimit = default;
    // 得分阈值
    option.ScoreThresholds = new int[] { MyImagePresentVM.ScoreThreshold };
    option.ContourOptions.SaveCroppedPatch = true;
    // 面积阈值
    option.ContourOptions.AreaThresholds = new int[] { MyImagePresentVM.AreaThreshold };
    // 配置 GPU
    NoticeEvent("配置 GPU", EnumLoggerType.Debug);
    stopwatch.Start();
    segEngine.AllocateNetwork(gpuService.CurrentDetectedGPUs[0], option);
    stopwatch.Stop();
    NoticeEvent("配置 GPU 完成,耗时: " + stopwatch.ElapsedMilliseconds + "ms", EnumLoggerType.Debug);
    // 逐个检测
    for (int i = 0; i < ListImageFiles.Count; i++)
        NoticeEvent("当前检测目标:" + (i + 1), EnumLoggerType.Debug);
        // 方式一:图像路径
        SrImage image = new SrImage(ListImageFiles[i].Name);
        // 方式二:图像数据内存
        //byte[] btimage = new byte[bitmap.Width * bitmap.Height * channel];
        //Marshal.Copy(bitmapData.Scan0, btimage, 0, btimage.Length);
        //bitmap.UnlockBits(bitmapData);
        //var images = new SrImage[]
        //   new SrImage(bitmap.Width, bitmap.Height, bitmapData.Stride, channel, btimage),
        // 获取检测结果
        SegmentationReport<SegContourResult> report = segEngine.Inspect<SegContourResult>(image);
        // 释放
        image.Dispose();
        // 列表显示结果
        ListImageFiles[i].DefectNumber = report.Prediction.Count;
        if (ListImageFiles[i].DefectNumber > 0)
            MyImagePresentVM.ShowInspectResult("NG", Brushes.OrangeRed);
            MyImagePresentVM.ShowInspectResult("OK", Brushes.SpringGreen);
        // 显示当前图像
        IntSelectImage = -1;
        IntSelectImage = i;
        string inspectionTime = report.InspectionTime.TotalMilliseconds.ToString();
        NoticeEvent("检测完成,耗时: " + inspectionTime + "ms", EnumLoggerType.Success);
        // 打印检测结果
        for (int j = 0; j < report.Prediction.Count; j++)
            int area = (int)report.Prediction[j].Area;
            System.Drawing.Point point = report.Prediction[j].Center;
            NoticeEvent(string.Format("缺陷 {0} 面积 {1} 位置 {2},{3}", j + 1, area, point.X, point.Y), 

实时检测

从相机获取图像,生成 SrImage(用于检测) 和 Bitmap(用于显示)

区分黑白相机和彩色相机,格外注意彩色相机中的图像格式转换

接收指定 PLC 信号,触发相机拍照,检测,发送结果给 PLC

public static CImageSource GetSrImage(int idx, bool isSaved = false)
    CImageSource cImage = new CImageSource();
    MyCamera.MV_FRAME_OUT frameData = new MyCamera.MV_FRAME_OUT();
    IntPtr pImageBuf = IntPtr.Zero;
    int nImageBufSize = 0;
    IntPtr pTemp = IntPtr.Zero;
    // 获取图像数据
    // 等待超时时间默认为 1000
    int nRet = CcdManager.Instance.HikCamInfos[idx].Camera.MV_CC_GetImageBuffer_NET(ref frameData, 10000);
    if (nRet != MyCamera.MV_OK)
        // 释放内存
        _ = CcdManager.Instance.HikCamInfos[idx].Camera.MV_CC_FreeImageBuffer_NET(ref frameData);
        return null;
    // 图像宽高
    int width = frameData.stFrameInfo.nWidth;
    int height = frameData.stFrameInfo.nHeight;
    int channel = 1;
    // 判断图像是彩图还是灰度图
    if (MvsMethod.GetIsColorPixelFormat(frameData.stFrameInfo.enPixelType))
        // 彩色图通道为 3
        channel = 3;
        if (frameData.stFrameInfo.enPixelType == MyCamera.MvGvspPixelType.PixelType_Gvsp_RGB8_Packed)
            pTemp = frameData.pBufAddr;
            if (IntPtr.Zero == pImageBuf || nImageBufSize < (frameData.stFrameInfo.nWidth * frameData.stFrameInfo.
                if (pImageBuf != IntPtr.Zero)
                    Marshal.FreeHGlobal(pImageBuf);
                pImageBuf = Marshal.AllocHGlobal(frameData.stFrameInfo.nWidth * frameData.stFrameInfo.nHeight * 3);
                nImageBufSize = frameData.stFrameInfo.nWidth * frameData.stFrameInfo.nHeight * 3;
            MyCamera.MV_PIXEL_CONVERT_PARAM stPixelConvertParam = new MyCamera.MV_PIXEL_CONVERT_PARAM
                pSrcData = frameData.pBufAddr,
                nWidth = frameData.stFrameInfo.nWidth,
                nHeight = frameData.stFrameInfo.nHeight,
                enSrcPixelType = frameData.stFrameInfo.enPixelType,
                nSrcDataLen = frameData.stFrameInfo.nFrameLen,
                nDstBufferSize = (uint)nImageBufSize,
                pDstBuffer = pImageBuf,
                enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_RGB8_Packed
            nRet = CcdManager.Instance.HikCamInfos[idx].Camera.MV_CC_ConvertPixelType_NET(ref stPixelConvertParam);
            if (nRet != MyCamera.MV_OK)
                return null;
            pTemp = pImageBuf;
    else if (MvsMethod.GetIsMonoPixelFormat(frameData.stFrameInfo.enPixelType))
        // 灰度图通道为 1
        channel = 1;
        if (frameData.stFrameInfo.enPixelType == MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8)
            pTemp = frameData.pBufAddr;
            if (IntPtr.Zero == pImageBuf || nImageBufSize < (frameData.stFrameInfo.nWidth * frameData.stFrameInfo.
                if (pImageBuf != IntPtr.Zero)
                    Marshal.FreeHGlobal(pImageBuf);
                pImageBuf = Marshal.AllocHGlobal(frameData.stFrameInfo.nWidth * frameData.stFrameInfo.nHeight);
                nImageBufSize = frameData.stFrameInfo.nWidth * frameData.stFrameInfo.nHeight;
            MyCamera.MV_PIXEL_CONVERT_PARAM stPixelConvertParam = new MyCamera.MV_PIXEL_CONVERT_PARAM
                pSrcData = frameData.pBufAddr,
                nWidth = frameData.stFrameInfo.nWidth,
                nHeight = frameData.stFrameInfo.nHeight,
                enSrcPixelType = frameData.stFrameInfo.enPixelType,
                nSrcDataLen = frameData.stFrameInfo.nFrameLen,
                nDstBufferSize = (uint)nImageBufSize,
                pDstBuffer = pImageBuf,
                enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8
            nRet = CcdManager.Instance.HikCamInfos[idx].Camera.MV_CC_ConvertPixelType_NET(ref stPixelConvertParam);
            if (nRet != MyCamera.MV_OK)
                return null;
            pTemp = pImageBuf;
    // 释放内存
    _ = CcdManager.Instance.HikCamInfos[idx].Camera.MV_CC_FreeImageBuffer_NET(ref frameData);
    cImage.SrImage = new SrImage(width, height, width * channel, channel, pTemp);
    cImage.Bitmap = new Bitmap(cImage.SrImage);
    if (isSaved)
        // 存图
        cImage.Bitmap.Save(string.Format("Image\\{0}.png", DateTime.Now.ToString("yyyyMMddHHmmssfff")), ImageFormat.
    // 注意要清空
    if (pImageBuf != IntPtr.Zero)
        Marshal.FreeHGlobal(pImageBuf);
    return cImage;

发生过的问题

图像显示界面卡死:图像刷新会耗费一些时间,刷新过快导致界面卡死

// 图像显示到界面
// 这个过程会耗费一些时间 后面必须 Sleep 长一点时间 否则界面会卡死
_ = DispatcherHelper.Dispatcher.BeginInvoke(new Action(() =>
    if (IsInspecting)
        MyImagePresentTools[idx].MyImage.Source = MyCImageSource[idx].Bitmap.
        // 释放
        MyCImageSource[idx].SrImage.Dispose();
        MyCImageSource[idx].Bitmap.Dispose();

改用两个线程,一个检测,一个显示,同步进行

/// 检测和图像显示同步进行
// 检测
Task task1 = Task.Run(() =>
    //// 获取检测结果
    //SegmentationReport<SegContourResult> report = segEngine.Inspect<SegContourResult>(MySrImages[idx]);
    //// 显示结果
    //if (report.Prediction.Count > 0)
    //    MyImagePresentVMs[idx].ShowInspectResult("NG", Brushes.OrangeRed);
    //else
    //    MyImagePresentVMs[idx].ShowInspectResult("OK", Brushes.SpringGreen);
    //string inspectionTime = report.InspectionTime.TotalMilliseconds.ToString();
    //OnNoticed(string.Format("工位 {0}  ", idx + 1) + "检测完成,耗时: " + inspectionTime + "ms", EnumLoggerType.Success);
    //// 打印检测结果
    //for (int j = 0; j < report.Prediction.Count; j++)
    //    int area = (int)report.Prediction[j].Area;
    //    System.Drawing.Point point = report.Prediction[j].Center;
    //    OnNoticed(string.Format(string.Format("工位 {0}  ", idx + 1) + "缺陷 {0} 面积 {1} 位置 {2},{3}", j + 1, area, point.X, point.Y), EnumLoggerType.Message);
    //// 发送检测结果
    //OnNoticed(string.Format("工位 {0}  ", idx + 1) + "发送检测结果: " + report.Prediction.Count, EnumLoggerType.Debug);
    //_ = McManager.Instance.Write(address + 20, 1);
    //_ = report.Prediction.Count == 0 ? McManager.Instance.Write(address + 21, 1) : McManager.Instance.Write(address + 21, 2);
// 图像显示到界面
Task task2 = Task.Run(() =>
    // 这个过程会耗费一些时间 后面必须 Sleep 长一点时间 否则界面会卡死
    _ = DispatcherHelper.Dispatcher.BeginInvoke(new Action(() =>
        if (IsInspecting)
            MyImagePresentTools[idx].MyImage.Source = MyCImageSource[idx].Bitmap.BitmapToBitmapImage();
            // 释放
            MyCImageSource[idx].SrImage.Dispose();
            MyCImageSource[idx].Bitmap.Dispose();