第一次写博客还是挺激动的,哈哈哈,先来一波干货吧,没写过博客,也不知道什么格式合适,大家先凑合看吧(笔芯)
1.1背景简介
该示例基于工业4.0的项目,具体产线技术,流程这里就不多说了,主要说一下我负责的视觉那一块。视觉主要识别乐高积木,识别是否有积木,是什么颜色?(说到这里,估计有的人应该知道了我们这个工业4.0的东西了。)视觉这一部分主要工作是接收上位机给我的拍照命令,然后控制相机拍照并把识别结果返回给上位机,通讯采用c/s模式,其中相机有三个,收到拍照命令拍照,拍照时间随机。
1.2实现效果
要识别的积木原图如下(只选了两张作为代表,实际有多种情况):
然后上一张识别结果图吧,先看一下效果(另一张没保存,懒得再运行程序了):
这部分算是整个识别软件的核心,主要采用svm进行多分类,然后根据分类结果重新设置了像素的RGB,所以显示才想上图那样,目的是更易于观察结果。其中积木凸点反光点经过处理之后也基本能识别成要的结果,反光的部分就无能为力了,黄色积木和蓝色积木的反光区就被识别成了背景黑色,但总体来说这个分类结果还是比较满意的,已经能够很容易的得到要的结果了。
再来看看软件的界面吧
但是识别分类只是其中的一部分,要实现预期目标还要做很多东西,比如怎么同时打开三个相机(Basler GigE),而且保证准确拍照(我没记错的话opencv只能打开一个相机吧?所以这里也算是一个技术点了),然后是通讯,界面等等一系列问题。
第一部分主要是简介,所以先上一张程序运行截图吧
说一下这个界面,左侧是程序的界面(嗯,好像是有点卡通啊,萌萌哒,哈哈哈哈),界面上的文本是对应的三个相机。右侧打开的文本是生成的工作日志(作用我就不说了吧),文本文档随着视觉的软件启动而自动打开。如果相机拍照,会把照片显示在界面上,写这篇文章的时候不在现场,所以没有相机,没有拍照图片,但大致是介个样子滴:
(呃~~,没错,这个是我p上去的!)
然后说一下整个开发的过程吧,首先从哪里入手呢?是不是很懵?其实当时我也是很懵,因为有网路,有相机,有图像处理分类还有软件界面等等,所以我就思考了一下整个流程,然后大致写了一下要实现的功能,上图吧(初稿):
这个是当时的原稿,后来才发现这个东西用处很大,做事之前最好先有个这样的“草稿”,有一个规划,这样不仅让自己心里有谱,而且还能防止自己懵圈,比如做到一半了突然蒙圈了,不知道自己在干嘛了(不开玩笑,这是真的),然后看一下这个流程也能让自己做一个定位,知道自己在干嘛,或者下一步干嘛。我有时候也会犯迷糊,写着写着不知道自己在干嘛了,然后看看流程就知道我要干什么,或者下一步要干什么,然后为了实现这一步的功能,去想办法,如果这个方法不行,那能不能换个方法实现?
现在回过头写文档,再把这个流程图给画一下吧:
首先初始化的东西有,界面,相机,svm训练,网络通讯,日志等等一系列东西;
接着是拍照,处理图片,再进行分类,中间有很多需要优化的东西,比如说一张图片保存下来10M+的大小,这无码高清大图不经过处理,那计算机得跑到什么时候才能给分完类呀?
好差不多流程弄完了,那接下来开始着手吧,理论上首先是解决网络的问题,测试通讯是否可行,这一块之前做过项目,能保证技术上可行,所以接下来开始先考虑相机吧。
背景已经介绍过了,这里再重新说一下相机这部分要实现的目标:三个相机(basler ,GigE接口),需要一直处于连接状态并保证独立随时拍照。
最开始的打算是用opencv打开相机:VideoCapture,但是后来发现这个没办法同时打开三个相机,只能打开一个相机,默认打开第一个先连接的相机,这里不多介绍了,具体想了解的话可以查资料。
后来继续查资料,发现basler有自己的SDK,可以自己开发相机,于是就装了pylon,接着就是一顿安装,配置环境(vs2013),然后看官网的sample,安装配置教程这里就不表述了,网上资料一查一大堆。
经过研究最后确定了一种可行的方案:通过匹配相机的mac,打开对应的相机。
流程图如下:
由于整个文件工程包含的东西比较多,而且相机的功能实现已经封装分布在工程的不同的类中,没办法完全给贴上来,所以只把核心代码给筛检之后贴了出来,如果有什么问题可以交流,下面是实现这些上述功能的核心代码:
#include
// Namespace for using pylon objects.
using namespace Pylon;
// Namespace for using cout.
//using namespace std;
//这一句必须要
PylonInitialize();
//获得设备
CTlFactory& tlFactory = CTlFactory::GetInstance();
//声明设备信息对象,并设置信息
//参数是绑定MAC地址信息
CDeviceInfo Device_info_siasun_A,Device_info_siasun_B,Device_info_ROKAE;
Device_info_siasun_A.SetFullName("Basler acA1300-60gc#0030532699C7#192.168.2.202:3956");
Device_info_siasun_B.SetFullName("Basler acA1300-60gc#0030532699C6#192.168.2.144:3956");
Device_info_ROKAE.SetFullName("Basler acA1300-60gc#0030532699C8#192.168.2.203:3956");
//把信息添加到filter
DeviceInfoList_t Device_filter_siasun_A, Device_filter_siasun_B, Device_filter_ROKAE;
Device_filter_siasun_A.push_back(Device_info_siasun_A);
Device_filter_siasun_B.push_back(Device_info_siasun_B);
Device_filter_ROKAE.push_back(Device_info_ROKAE);
//创建相机对象
CInstantCamera Camera_siasun_A,Camera_siasun_B,Camera_ROKAE;
//注意此处容易出现异常,打开相机异常
//信息匹配,如果匹配成功,打开相机
//连接并打开相机
DeviceInfoList_t device_temp;
if (tlFactory.EnumerateDevices(device_temp, Device_filter_siasun_A) > 0)
{
Camera_siasun_A.Attach(tlFactory.CreateDevice(device_temp[0]));
Camera_siasun_A.Open();
}
if (tlFactory.EnumerateDevices(device_temp, Device_filter_siasun_B) > 0)
{
Camera_siasun_B.Attach(tlFactory.CreateDevice(device_temp[0]));
Camera_siasun_B.Open();
}
if (tlFactory.EnumerateDevices(device_temp, Device_filter_ROKAE) > 0)
{
Camera_ROKAE.Attach(tlFactory.CreateDevice(device_temp[0]));
Camera_ROKAE.Open();
}
//结果指针
//相机拍完照片之后会先把数据存入内存中,这里是放入了CGrabResultPtr指针对象中
CGrabResultPtr PtrGrabResult_siasun_A,PtrGrabResult_siasun_B,PtrGrabResult_ROKAE;
//开始抓拍
/*Camera_siasun_A.StartGrabbing(1);
Camera_siasun_B.StartGrabbing(1);
Camera_ROKAE.StartGrabbing(1);
//等待并检测,100ms超时
Camera_siasun_A.RetrieveResult( 100, PtrGrabResult_siasun_A, TimeoutHandling_ThrowException);
Camera_siasun_B.RetrieveResult( 100, PtrGrabResult_siasun_B, TimeoutHandling_ThrowException);
Camera_ROKAE.RetrieveResult( 100, PtrGrabResult_ROKAE, TimeoutHandling_ThrowException);
*/
//1000ms超时
//抓取一张图片
Camera_siasun_A.GrabOne(1000,PtrGrabResult_siasun_A, TimeoutHandling_ThrowException);
Camera_siasun_B.GrabOne(1000,PtrGrabResult_siasun_B, TimeoutHandling_ThrowException);
Camera_siasun_ROKAE.GrabOne(1000,PtrGrabResult_siasun_ROKAE, TimeoutHandling_ThrowException);
//****************接下来转换图片格式
//创建格式转换对象
CImageFormatConverter Format_converter_siasun_A,Format_converter_siasun_B,Format_converter_ROKAE;
CPylonImage PylonImage_Temp_siasun_A,PylonImage_Temp_siasun_B,PylonImage_Temp_ROKAE;
//设定转换格式
Format_converter_siasun_A.OutputPixelFormat = PixelType_BGR8packed;
Format_converter_siasun_A.OutputBitAlignment = OutputBitAlignment_MsbAligned;
Format_converter_siasun_B.OutputPixelFormat = PixelType_BGR8packed;
Format_converter_siasun_B.OutputBitAlignment = OutputBitAlignment_MsbAligned;
Format_converter_ROKAE.OutputPixelFormat = PixelType_BGR8packed;
Format_converter_ROKAE.OutputBitAlignment = OutputBitAlignment_MsbAligned;
Format_converter_siasun_A.Convert(PylonImage_Temp_siasun_A, PtrGrabResult_siasun_A);
Format_converter_siasun_B.Convert(PylonImage_Temp_siasun_B, PtrGrabResult_siasun_B);
Format_converter_ROKAE.Convert(PylonImage_Temp_ROKAE, PtrGrabResult_ROKAE);
好了,相机打开的问题解决了,接下来该处理图像了。(做个工程真的是要经历九九八十一难呀,好多莫名的bug)
对,还有一个问题就是格式转换,我拍完照片之后是pylon的图片,需要转换成opencv需要处理的格式也就是Mat,最开始以为挺麻烦的,想着实在不行就先保存到磁盘上,然后再用opencv读取过来,后来发现这个一行代码就搞定了:
//CPylonImage类图片
CPylonImage PylonImage_Temp;
//把指针指向的缓存数据转换为CPylonImage类
Format_converter.Convert(PylonImage_Temp, PtrGrabResult);
//把CPylonImage类转换为Mat类,其实图片读取到内容中就是一堆数据(三通道为三维数组),转换的时候只需要把buffer都去过来就好了
Mat temp_grab = cv::Mat(PtrGrabResult->GetHeight(), PtrGrabResult->GetWidth(), CV_8UC3, (uint8_t *)PylonImage_Temp.GetBuffer());
终于到重点了,这就是整个工程最核心的部分--颜色分类。其实颜色识别分类的方法有挺多的,我这里用了svm进行分类,主要也想了解一下机器学习的一些东西,为以后打点基础。
其实opencv已经把机器学习的框架都做好了,我们只需要添加数据,训练模型,只不过中间可能需要再调一下参数就好。
3.1添加数据及标签
之前一直不知道怎么添加标签,耽误了很多时间,后来发现其实特别简单,就是把读取图片的数据(矩阵)直接添加成训练集就好了,同时要再加上对应的标签。
需要注意的是svm训练数据的格式是CV_32FC1,而标签是CV_32SC1,所以在训练之前需要对数据进行处理一下,同时在predict的时候也需要处理。
添加标签以红色和黄色为例:
//------------------红色训练数据--------------------------//
Mat red_roi_uf = imread("C:/Users/ncutl/Desktop/red.png");
//原图是CV_8UC3,例如255*255的像素,矩阵是255*255*3的矩阵,现在需要转换成,(255*255)*3的矩阵,格式为CV_32FC1
Mat red_roi_convert;
red_roi_uf.convertTo(red_roi_convert, CV_32FC1);
Mat red_roi_data(red_roi_convert.rows*red_roi_convert.cols, 3, CV_32FC1, red_roi_convert.data);
//生成对应的标签,红色标签为1
Mat red_label = Mat(red_roi_convert.rows*red_roi_convert.cols, 1, CV_32SC1, Scalar::all(1));
//-------------------黄色训练数据--------------------//
Mat yellow_roi_uf = imread("C:/Users/ncutl/Desktop/yellow.png");
Mat yellow_roi_convert;
//转换
yellow_roi_uf.convertTo(yellow_roi_convert, CV_32FC1);
Mat yellow_roi_data(yellow_roi_convert.rows*yellow_roi_convert.cols, 3, CV_32FC1, yellow_roi_convert.data);
//黄色标签为2
Mat yellow_label = Mat(yellow_roi_convert.rows*yellow_roi_convert.cols, 1, CV_32SC1, Scalar::all(2));
//imshow("黄色", yellow_roi);
//-----------------合并所有的样本点,作为训练数据----------------------//
Mat train_data, train_label;
vconcat(red_roi_data, yellow_roi_data, train_data);
vconcat(red_label, yellow_label, train_label);
3.2训练模型并调整参数
训练模型其实就几行代码,需要做的就是改参数,使得分类最优。
本例是使用多分类,参数及优化可以参考这篇文章:OpenCV中的SVM参数优化
下边的参数是我已经调完之后的:
// 设置参数
Ptr svm = SVM::create();
svm->setType(SVM::C_SVC);
svm->setKernel(SVM::POLY);
//svm->setNu(0.5);
svm->setGamma(100);
//svm->setC(100);
svm->setDegree(0.08);
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 100, 1e-6));
// 训练分类器
Ptr tData = TrainData::create(train_data, ROW_SAMPLE, train_label);
svm->train(tData);
cout << "训练完成" << endl << endl;
3.3predict
这部分代码适用于分类,遍历像素提取rgb的值进行格式转换,然后predict,根据分类结果把该点像素点替换成理想的红黄蓝绿和背景黑色。
//设置颜色
Vec3b green(0, 255, 0), blue(255, 0, 0), red(0, 0, 255), yellow(0, 255, 255), black(0, 0, 0);
//分类颜色计数器
long red_numb = 0, yellow_numb = 0, green_numb = 0, blue_numb = 0, back_numb = 0;
Mat test_img=imread("C:\\Users\\ncutl\\Desktop\\samp.png");
//一个一个像素predict,这个缺点是太慢了
for (int i = 0; i < test_img.rows; i++)
for (int j = 0; j < test_img.cols; j++)
{
//部分用于格式转关,在上一步已经说过这个问题
Vec3b pixel = test_img.at(i, j);
float a_t = pixel[0];
float b_t = pixel[1];
float c_t = pixel[2];
//cout << a_t << endl << b_t << endl << c_t << endl;
Mat sampleMat = (Mat_(1, 3) << a_t, b_t, c_t);
int response = svm->predict(sampleMat);
if (response == 1)
{
test_img.at(i, j) = red; red_numb++;
}
if (response == 2)
{
test_img.at(i, j) = yellow;
yellow_numb++;
}
if (response == 3)
{
test_img.at(i, j) = green;
green_numb++;
}
if (response == 4)
{
test_img.at(i, j) = blue;
blue_numb++;
}
if (response == 5)//背景颜色
{
test_img.at(i, j) = black;
back_numb++;
}
}
最后执行完之后得到的结果如下所示:
3.4速度优化
前面说过,分类是单个像素分类,这样的缺点是速度太慢,而且拍摄的照片也是高像素的图片,所以提高速度非常必要。最后识别用的方法是先对原图进行采样,再设置ROI区域,这样的话速度会提高不少。
有许多其他方法可以检测颜色,速度会比较快,用svm的话应该有其他训练模型的方法,可以快速分类。
采样前后的时间对比如下(上边原图,下边采样之后):
时间缩短了有10倍之多,这一部分资源我上传了,文件包括分类cpp文件和测试图片,有需要可以下载:svm颜色分类
单网络通讯这一部分网上资源挺多的,也很简单。重要的是在程序运行时候需要单独开线程,防止阻塞,这一部分跟TCP的通讯方式有关。
4.1开辟新线程
网络通信是会有阻塞的,为了防止通讯占用主线程资源,需要开辟新线程处理通讯的程序,开辟新线程主要包括三部分,首先声明线程函数和指针,定义线程函数,最后启动新线程。下边以相机线程为例。
//声明线程函数和指针
CWinThread* pRecvThread_Connect_Camera = NULL;
UINT RecvThread_Connect_Camera(LPVOID pParam);
//这一句启动新线程
pRecvThread_Connect_Camera = AfxBeginThread(RecvThread_Connect_Camera, this);
//这一部分是线程函数
UINT RecvThread_Connect_Camera(LPVOID pParam)
{
//传递对话框的this指针
C视觉Dlg* pThis = (C视觉Dlg*)pParam;
/*
这里填写代码
*/
return 0;
}
4.2网络通讯
这一部分是tcp通讯,打开服务等待连接,如果有连接判断接收数据,然后根据接收数据执行相应代码就好。
#include
#include
#pragma comment(lib,"ws2_32.lib")
int main()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
//创建套接字
SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (slisten == INVALID_SOCKET)
{
printf("socket error !");
return 0;
}
//绑定IP和端口
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4680);
sin.sin_addr.S_un.S_addr = inet_addr("192.168.2.211");// htonl(INADDR_ANY); //inet_addr("172.20.10.8");
if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("bind error !");
}
//开始监听
if (listen(slisten, 5) == SOCKET_ERROR)
{
printf("listen error !");
return 0;
}
//循环接收数据
SOCKET sClient;
sockaddr_in remoteAddr;
int nAddrlen = sizeof(remoteAddr);
char revData[255];
long linenum = 0;
while (true)
{
//printf("等待连接...\n");
sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
if (sClient == INVALID_SOCKET)
{
printf("accept error !");
continue;
}
//printf("第%d次:\n", linenum);
//linenum++;
//printf("接受到一个连接:%s :\r\n", inet_ntoa(remoteAddr.sin_addr));
//printf("/t/t");
//接收数据
int ret = recv(sClient, revData, 255, 0);
if (ret > 0)
{
revData[ret] = 0x00;
printf(revData);
}
printf("\n");
//发送数据
//char send[4] = { 1, 1, 1, 1 };
const char * sendData="";
if (revData[1] == '1')
{
sendData = "9999";
}
send(sClient, sendData, strlen(sendData), 0);
printf("发送结果:%s\n\n", sendData);
Sleep(100);
closesocket(sClient);
}
closesocket(slisten);
WSACleanup();
return 0;
}
这个版本代码是网上的例程,但只能连接一个client,并且不能连续发送数据,可以适当修改使得可以连接多个client且连续发送数据。
目前核心的功能已经全部实现,先写到这里吧,后期再更新吧。