【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();