实例目录
第一章:需求介绍及软硬件选型
第二章:界面布局
第三章:代码编写
文章目录
- 实例目录
- 前言
- 一、要识别的对象长啥样?
- 二、软件硬件的规划
- 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 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 "";
}
}
其实核心的代码就是上面的,非专业人士,写的自己用的,高手请指导不要喷死我