海康工业相机视觉识别喷码组垛实例

编程入门 行业动态 更新时间:2024-10-10 02:21:24

海康工业相机视觉识别喷码组垛<a href=https://www.elefans.com/category/jswz/34/1771375.html style=实例"/>

海康工业相机视觉识别喷码组垛实例

实例目录

第一章:需求介绍及软硬件选型
第二章:界面布局
第三章:代码编写

文章目录

  • 实例目录
  • 前言
  • 一、要识别的对象长啥样?
  • 二、软件硬件的规划
    • 1.软件
    • 2.硬件
  • 三、界面设计
    • 1.界面采用Sunny UI,美观大方
  • 四、代码编写
    • 1、先安装海康的MVS_STD_4.1.0_230531
    • 2、窗口加载时初始化:先枚举设备并进行相机的初始化,因为我用的是两个相机,所以定义 了LIST来存放
    • 3、启动相机与线程
    • 4、收到信号开始识别


前言

饮料工厂,箱喷码上除了日期外,额外打印了序列号,分两道(下文就叫A、B道)码垛机,要求记录下每个栈板上的A、B道的序列,并打印,最后贴在栈板产品上,以便出货时记录与追踪;现在这个软件已经在用,bug基本修复完成;记录下来,以便有需要的人参考。
大概的示意如下:
1、每层产品进入码垛机后,下一层的包装产品在相机处停留;
2、产品进设备进行自动码垛,当整个栈板码好,设备发出一个满垛信号;
3、收到信号时,相机处的产品即为一下板的第1和第2箱;
4、此时进行抓图识别,将时间、序列进行处理打印、并将相关信息写入数据库;


提示:以下是本篇文章正文内容,下面案例可供参考

一、要识别的对象长啥样?

这就是我们要识别的,主要要取的信息分2段,时间、序列,最后那个S1什么的没什么用,就是AB道的区别而已

其实一看,还是挺简单的,但因为涉及工业环境、产品停留位置、光线、运动等因素,并不是每个图像都能有这截图这么完美的。

二、软件硬件的规划

1.软件

采用C#WINFORM编写,网络与PLC通讯,相机USB通讯
支持历史数据存档,查询、删除、导出EXCEL表;
支持信息的打印

2.硬件

2.1、相机:海康工业相机 MV-CA004-10UC(有钱可以买 更好点的,高像素的,建议用这款MV-CU013-80UC)

2.2、镜头:MVL-HF0828M-6MPE(FA镜头,8mm F2.8 1/1.8’’ C)

2.3、USB线:3米USB线,无需IO线及电源线;

2.4、工控机及显示屏:网口、最好带独显、打印机USB接口、相机*2的USB3.0接口,操作系统WIN10;

2.5、打印机,最好是墨水连供的,减少换打印头;

三、界面设计

1.界面采用Sunny UI,美观大方

界面最终如下,顶上为功能按钮,中间实时显示相机,下边显示信息、结果以及相机的参数

参数设置界面采用TabControl,分两页

数据页采用flexGrid控件

四、代码编写

1、先安装海康的MVS_STD_4.1.0_230531

2、窗口加载时初始化:先枚举设备并进行相机的初始化,因为我用的是两个相机,所以定义 了LIST来存放

  List<CCameraInfo> m_ltDeviceList = new List<CCameraInfo>();List<CCamera> m_pMyCamera = new List<CCamera>();
/// <summary>/// 枚举设备函数/// </summary>private void DeviceListAcq(){// ch:创建设备列表 | en:Create Device ListSystem.GC.Collect();m_ltDeviceList.Clear();int nRet = CSystem.EnumDevices(CSystem.MV_USB_DEVICE, ref m_ltDeviceList);if (0 != nRet){ShowErrorMsg("Enumerate devices fail!", nRet);return;}return;}
/// <summary>/// 相机的初始化/// </summary>private void IntCamare(){int m_nDevNum = m_ltDeviceList.Count;if (m_nDevNum != 2){ShowWarningDialog("识别到不是两台相机,请检查硬件!");}for (int i = 0; i < m_nDevNum; i++){m_pMyCamera.Add(new CCamera());}for (int i = 0, j = 0; j < m_nDevNum; j++){CCameraInfo device = m_ltDeviceList[i];// ch:打开设备 | en:Open deviceif (null == m_pMyCamera[i]){m_pMyCamera[i] = new CCamera();if (null == m_pMyCamera[i]){return;}}int nRet = m_pMyCamera[i].CreateHandle(ref device);if (CErrorDefine.MV_OK != nRet){ShowErrorMsg("Create device Handle fail!", nRet);return;}nRet = m_pMyCamera[i].OpenDevice();if (CErrorDefine.MV_OK != nRet){m_pMyCamera[i].DestroyHandle();ShowErrorMsg("Device open fail!", nRet);return;}InfoText.AppendText("相机" + i + "初始化完成!\r\n");// ch:探测网络最佳包大小(只对GigE相机有效) | en:Detection network optimal package size(It only works for the GigE camera)if (device.nTLayerType == CSystem.MV_GIGE_DEVICE){int nPacketSize = m_pMyCamera[i].GIGE_GetOptimalPacketSize();if (0 < nPacketSize){nRet = m_pMyCamera[i].SetIntValue("GevSCPSPacketSize", (uint)nPacketSize);if (nRet != CErrorDefine.MV_OK){ShowErrorMsg("Set Packet Size failed!", nRet);}}}// ch:设置采集连续模式 | en:Set Continues Aquisition Modem_MyCamera.SetEnumValue("AcquisitionMode", (uint)MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS);i++;}if (m_nDevNum > 1){//设置控件可使用SetControlWhenOpen();//获得相机参数GetParamWhenOpen();}}

上面GetParamWhenOpen,这个,是取得相机的参数,放到窗体的控件上显示,便后后面有需要修改作参考

这个在修改后也要调用,因为有时候设置曝光时,帧率会跟着变

/// <summary>/// 取得相机参数显示到控件/// </summary>private void GetParamWhenOpen(){// 获取曝光参数CFloatValue stParam = new CFloatValue();Int32 nRet = m_pMyCamera[0].GetFloatValue("ExposureTime", ref stParam);if (CErrorDefine.MV_OK == nRet){tbExposure.Text = stParam.CurValue.ToString("F2");tbExposure.Enabled = true;}nRet = m_pMyCamera[0].GetFloatValue("Gain", ref stParam);if (CErrorDefine.MV_OK == nRet){tbGain.Text = stParam.CurValue.ToString("F1");}nRet = m_pMyCamera[0].GetFloatValue("ResultingFrameRate", ref stParam);if (CErrorDefine.MV_OK == nRet){tbFrameRate.Text = stParam.CurValue.ToString("F1");}// 获取曝光参数nRet = m_pMyCamera[1].GetFloatValue("ExposureTime", ref stParam);if (CErrorDefine.MV_OK == nRet){tbExposure2.Text = stParam.CurValue.ToString("F2");tbExposure2.Enabled = true;}nRet = m_pMyCamera[1].GetFloatValue("Gain", ref stParam);if (CErrorDefine.MV_OK == nRet){tbGain2.Text = stParam.CurValue.ToString("F1");}nRet = m_pMyCamera[1].GetFloatValue("ResultingFrameRate", ref stParam);if (CErrorDefine.MV_OK == nRet){tbFrameRate2.Text = stParam.CurValue.ToString("F1");}nRet = m_pMyCamera[1].GetFloatValue("ResultingFrameRate", ref stParam);if (CErrorDefine.MV_OK == nRet){tbFrameRate2.Text = stParam.CurValue.ToString("F1");}}

3、启动相机与线程

启动后,设置了两个线程分别给两个相机,这样,双方的识别与文字处理不用排队
再来个线程判定PLC信号 是否收到:getPlcSignl
收到就会启动识别程序

        /// <summary>/// 启动相机与识别线程/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void btnstart_Click(object sender, EventArgs e){if (m_ltDeviceList.Count != 2){DeviceListAcq();IntCamare();//打开相机if (m_ltDeviceList.Count != 2){ShowWarningDialog("相机数量不对!请检查硬件连接!");return;}}Auto = true;btnstart.Enabled = false;getIniParam();sysStart = true;PlcConnect();// ch:前置配置 | en:pre-operationint nRet = NecessaryOperBeforeGrab2();if (CErrorDefine.MV_OK != nRet){return;}SetCamera();// ch:标志位置位true | en:Set position bit truem_bGrabbing = true;m_hReceiveThreadA = new Thread(ReceiveThreadProcessA);m_hReceiveThreadA.Start();m_hReceiveThreadB = new Thread(ReceiveThreadProcessB);m_hReceiveThreadB.Start();// ch:开始采集 | en:Start GrabbingnRet = m_pMyCamera[0].StartGrabbing();if (CErrorDefine.MV_OK != nRet){m_bGrabbing = false;m_hReceiveThread.Join();ShowErrorMsg("Start Grabbing Fail!", nRet);return;}nRet = m_pMyCamera[1].StartGrabbing();if (CErrorDefine.MV_OK != nRet){m_bGrabbing = false;m_hReceiveThread.Join();ShowErrorMsg("Start Grabbing Fail!", nRet);return;}// ch:控件操作 | en:Control OperationSetCtrlWhenStartGrab();PlcReciveThread = new Thread(getPlcSignl);PlcReciveThread.Start();

里面有个PLC连接的,我用的是ioClient

举例1个相机的启动线程

        /// <summary>相机A线程</summary>public void ReceiveThreadProcessA(){CFrameout pcFrameInfo = new CFrameout();CPixelConvertParam pcConvertParam = new CPixelConvertParam();CDisplayFrameInfo pcDisplayInfo = new CDisplayFrameInfo();int nRet = CErrorDefine.MV_OK;while (m_bGrabbing){nRet = m_pMyCamera[0].GetImageBuffer(ref pcFrameInfo, 1000);if (CErrorDefine.MV_OK == nRet){// 保存图像数据用于保存图像文件lock (BufForDriverLock){m_pcImgForDriverA = pcFrameInfo.Image.Clone() as CImage;m_pcImgSpecInfoA = pcFrameInfo.FrameSpec;pcConvertParam.InImage = pcFrameInfo.Image;if (PixelFormat.Format8bppIndexed == m_pcBitmapA.PixelFormat){pcConvertParam.OutImage.PixelType = MvGvspPixelType.PixelType_Gvsp_Mono8;m_pMyCamera[0].ConvertPixelType(ref pcConvertParam);}else{pcConvertParam.OutImage.PixelType = MvGvspPixelType.PixelType_Gvsp_BGR8_Packed;m_pMyCamera[0].ConvertPixelType(ref pcConvertParam);}BitmapAready = false;// ch:保存Bitmap数据 | en:Save Bitmap Datatry{BitmapData m_pcBitmapData = m_pcBitmapA.LockBits(new Rectangle(0, 0, pcConvertParam.InImage.Width, pcConvertParam.InImage.Height), ImageLockMode.ReadWrite, m_pcBitmapA.PixelFormat);Marshal.Copy(pcConvertParam.OutImage.ImageData, 0, m_pcBitmapData.Scan0, (Int32)pcConvertParam.OutImage.ImageData.Length);m_pcBitmapA.UnlockBits(m_pcBitmapData);GetPicture[0] = (Bitmap)m_pcBitmapA.Clone();BitmapAready = true;}catch (Exception ex){WriteLog(ex.Message);continue;}}// 渲染图像数据pcDisplayInfo.WindowHandle = pictureBox1.Handle;pcDisplayInfo.Image = pcFrameInfo.Image;m_pMyCamera[0].DisplayOneFrame(ref pcDisplayInfo);Huaxian(pictureBox1, pic_Ax, pic_Ay, pic_Aw, pic_Ah);m_pMyCamera[0].FreeImageBuffer(ref pcFrameInfo);                   }else{if (MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_ON == m_enTriggerMode){Thread.Sleep(5);}}}}

这段代码其实也没什么好看的,主要的功能就是持续的渲染,将图像显示到两个PictureBox里,再克隆一份位图写到数组
GetPicture[0] 里,这个图再给后面用

4、收到信号开始识别

这看起来比较简单,代码也都有批注了,不详细介绍了

 /// <summary>/// 这是一个读PLC满垛信号的线程/// </summary>private void getPlcSignl(){while (Auto)//自动状态{if (enableplc)//是否启用PLC连接功能{PLC_signal = client.ReadBoolean(PLCsignal).Value;if (PLC_signal) { uiLight1.State = UILightState.On; } else {uiLight1.State = UILightState.Off;}}if (PLC_signal || Test_signal)//收到PLC信号或测试信号 {if (SoftTrigger){// ch:触发命令 | en:Trigger commandint nRet = m_pMyCamera[0].SetCommandValue("TriggerSoftware");if (CErrorDefine.MV_OK != nRet){ShowErrorMsg("Trigger Software Fail!", nRet);}nRet = m_pMyCamera[1].SetCommandValue("TriggerSoftware");if (CErrorDefine.MV_OK != nRet){ShowErrorMsg("Trigger Software Fail!", nRet);}}OCR_deal();//文字识别}Thread.Sleep(100);}}

OCR_deal:
这段有点长,写了一堆识别后的逻辑处理,写数据库,以及打印的
真正的文字识别是PaddleOCR

/// <summary>/// 文字识别的处理/// </summary>private void OCR_deal(){ShowSuccessTip("收到PLC信号,一秒后开始识别,关注两道是否有产品可识别");AppendText("收到PLC信号,一秒后开始识别,关注两道是否有产品可识别");Thread.Sleep(1000);while (!(BitmapAready&&BitmapBready)){//要等到同时好再往下}if (abSwich){PImage = GetPicture[1];PImage2 = GetPicture[0];}else{PImage = GetPicture[0];PImage2 = GetPicture[1];}//定义一个图片保存开关bool savepicA = false;bool savepicB = false;//AppendText("得到A图");Bitmap Atu = PImage;Bitmap Btu= PImage2;//AppendText("得到B图");//是否对图像进行灰度化,裁剪是都有if (isGray){PImage = Jhlibmon.ToGray(crop(PImage, pic_Ax, pic_Ay, pic_Aw, pic_Ah));PImage2 = Jhlibmon.ToGray(crop(PImage2, pic_Bx, pic_By, pic_Bw, pic_Bh));}else{PImage = crop(PImage, pic_Ax, pic_Ay, pic_Aw, pic_Ah);PImage2 = crop(PImage2, pic_Bx, pic_By, pic_Bw, pic_Bh);}//调用文字识别,得到3个:原始数据,序列,时间Orc_A();Orc_B();//把裁剪后灰度后的图显示到界面上,方便判断图像是否是对的pictureBox3.Image = PImage;pictureBox4.Image = PImage2;//这个针对重开软件时,上一板的数据没有if (A_Pre == 0 && B_Pre == 0){if (A_Serial > xiangshu || B_Serial > xiangshu){A_Pre = A_Serial - xiangshu / 2;B_Pre = B_Serial - xiangshu / 2;}}//如果单边差异数都大于整板,直接按识别错误处理if (A_Pre > 0 && A_Serial - A_Pre > xiangshu - 2){A_Serial = 0;savepicA = true;}if (B_Pre>0&&B_Serial - B_Pre > xiangshu - 2){B_Serial = 0;savepicB = true;}//下面要分好几个情况//1、A\B都没取到,索性两边各加上总箱数一半if (A_Serial == 0 && B_Serial == 0){A_Serial = A_Pre + xiangshu / 2;B_Serial = B_Pre + xiangshu / 2;savepicA = true;savepicB = true;}//2、B有、A没有else if (A_Serial == 0 && B_Serial != 0){savepicA = true;int B_dif = B_Serial - B_Pre;if (B_dif > chayi){A_Serial = A_Pre + xiangshu / 2;}else{A_Serial = A_Pre + xiangshu - B_dif;}}//3、A有、B没有else if (A_Serial != 0 && B_Serial == 0){savepicB = true;int A_dif = A_Serial - A_Pre;if (A_dif > chayi){B_Serial = B_Pre + xiangshu / 2;}else{B_Serial = B_Pre + xiangshu - A_dif;}}int AStart, AEnd, BStart, BEnd;//这个相等,就说明这个板没有A道产品if (A_Serial == A_Pre){AStart = 0;AEnd = 0;}else{AStart = A_Pre;AEnd = (A_Serial - 1);}//这个相等,就说明这个板没有B道产品if (B_Serial == B_Pre){BStart = 0;BEnd = 0;}else{BStart = B_Pre;BEnd = (B_Serial - 1);}banhao = int.Parse(client.ReadInt16(PLCBanhao).Value.ToString());//要开始打印了if (enableprinter){//创建一个名为"Table_New"的空表DataTable dt = new DataTable("Table_New");dt.Clear();//2.创建带列名和类型名的列(两种方式任选其一)dt.Columns.Add("AB道", typeof(String));dt.Columns.Add("喷码时间", typeof(String));dt.Columns.Add("开始序列", typeof(String));dt.Columns.Add("结束序列", typeof(String));dt.Rows.Add("A道", A_Datetime, AStart.ToString(), AEnd.ToString());//Add里面参数的数据顺序要和dt中的列的顺序对应 dt.Rows.Add("B道", B_Datetime, BStart.ToString(), BEnd.ToString());//Add里面参数的数据顺序要和dt中的列的顺序对应 ToPrint print = new ToPrint();print.Print(dt, "板号:" + banhao + "     组垛时间:" + DateTime.Now.ToString());}//这里开始写数据库try{Xulie xulie = new Xulie() { banhao = banhao, intime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),ayuanshi=Ayuanshi,byuanshi=Byuanshi, atime = A_Datetime, btime = B_Datetime, astart = AStart, aend = AEnd, bstart = BStart, bend = BEnd };db.db.Insertable(xulie).ExecuteCommand();}catch{WriteLog("写入数据库失败");}//下面作一些后期的处理//先把信息显示到文本框里AppendText("A原始数据:" + Ayuanshi);                AppendText("B原始数据:" + Byuanshi);AppendText("板数:"+banhao.ToString());AppendText("A道_时间:" + A_Datetime + "_序列号:" + A_Serial);AppendText("B道_时间:" + B_Datetime + "_序列号:" + B_Serial);//控件显示banshu.Text = banhao.ToString();aBegin.Text = A_Pre.ToString();aEnd.Text = (A_Serial - 1).ToString();bBegin.Text = B_Pre.ToString();bEnd.Text = (B_Serial - 1).ToString();Axiang.Text = (A_Serial - A_Pre).ToString();Bxiang.Text = (B_Serial - B_Pre).ToString();heji.Text = (A_Serial - A_Pre + B_Serial - B_Pre).ToString();//写入日志WriteLog2("A原始数据:" + Ayuanshi);WriteLog2("B原始数据:" + Byuanshi);WriteLog2("A道_时间:" + A_Datetime + "_序列号:" + A_Serial);WriteLog2("B道_时间:" + B_Datetime + "_序列号:" + B_Serial);WriteLog2("板号:" + banhao);//都干完了之后,把当前序列当作每板第一箱A_Pre = A_Serial;B_Pre = B_Serial;//要不要把错误的图存下来if (savepicA && saveErrorpic){try{SaveBitmap(BMPhuaxian(Atu, pic_Ax,pic_Ay,pic_Aw,pic_Ah));}catch { }}if (savepicB && saveErrorpic){try{SaveBitmap(BMPhuaxian(Btu,pic_Bx,pic_By,pic_Bw,pic_Bh));}catch { }}           }
 private void Orc_A(){Thread.Sleep(1);Atext = "0";if (PImage != null)//A道如果有照到相片就去识别,然后取文字,没取到就赋值0{Ayuanshi = common.result(PImage);    if ( Ayuanshi.IndexOf("S") == -1 || Ayuanshi.Length < 7){ShowErrorTip("A道识别不到喷码里的S,或识别文本长度不足");A_Serial = 0;//没识别到按0处理}else if(string.IsNullOrEmpty(Ayuanshi)){Ayuanshi = "没有识别或识别错误";ShowErrorTip("A道没有识别或识别错误");}else{Atext = Regex.Replace(Ayuanshi, @"[\u4e00-\u9fa5]", "");//识别文字并去除汉字A_Serial = common.GetNumber(Atext, Num_Serial);}}//下面这个得到序列与日期,序列没得到会赋0,日期时间没得到赋当前时间A_Datetime = common.Get_time(Atext, printyear);}
/// <summary>/// 识别BIMAP图片,返回文字,这里去了汉字/// </summary>/// <param name="bitmap">图片</param>/// <returns>非汉字文本</returns>public static string result(Bitmap bitmap){try{//bitmap = ToGray(bitmap);OCRParameter oCRParameter = new OCRParameter();OCRModelConfig config = null;OCRResult ocrResult = new OCRResult();using (PaddleOCREngine engine = new PaddleOCREngine(config, oCRParameter)){ocrResult = engine.DetectText(bitmap);}if (ocrResult != null){string str1 = GetNumberAlpha(ocrResult.Text);//str1 = Regex.Matches(str1, @"[^A-Za-z0-9]+", "");//str1 = Regex.Replace(str1, @"[\u4e00-\u9fa5]", "");//识别文字并去除汉字return str1;}else{return "";}}catch{return "";}}

其实核心的代码就是上面的,非专业人士,写的自己用的,高手请指导不要喷死我

更多推荐

海康工业相机视觉识别喷码组垛实例

本文发布于:2024-03-12 11:18:37,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1731384.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:实例   视觉   相机   工业   喷码

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!