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

实例目录

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


前言

饮料工厂,箱喷码上除了日期外,额外打印了序列号,分两道(下文就叫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 List
            System.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 device
                if (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 Mode
                m_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-operation
            int nRet = NecessaryOperBeforeGrab2();
            if (CErrorDefine.MV_OK != nRet)
            {
                return;
            }
            SetCamera();
            // ch:标志位置位true | en:Set position bit true
            m_bGrabbing = true;

            m_hReceiveThreadA = new Thread(ReceiveThreadProcessA);
            m_hReceiveThreadA.Start();
            m_hReceiveThreadB = new Thread(ReceiveThreadProcessB);
            m_hReceiveThreadB.Start();

            // ch:开始采集 | en:Start Grabbing
            nRet = 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 Operation
            SetCtrlWhenStartGrab();
            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 Data
                        try
                        {
                            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 command
                        int 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 = Jhlib.common.ToGray(crop(PImage, pic_Ax, pic_Ay, pic_Aw, pic_Ah));

                    PImage2 = Jhlib.common.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 "";
            }
        }

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值