[转]basler多相机拍照,通过opencv(3.4.1) svm 实时颜色分类(基于C++)
一、背景及实现效果介绍:
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,打开对应的相机。
流程图如下:
由于整个文件工程包含的东西比较多,而且相机的功能实现已经封装分布在工程的不同的类中,没办法完全给贴上来,所以只把核心代码给筛检之后贴了出来,如果有什么问题可以交流,下面是实现这些上述功能的核心代码:
1 #include <pylon/PylonIncludes.h> 2 // Namespace for using pylon objects. 3 using namespace Pylon; 4 5 // Namespace for using cout. 6 //using namespace std; 7 8 //这一句必须要 9 PylonInitialize(); 10 11 //获得设备 12 CTlFactory& tlFactory = CTlFactory::GetInstance(); 13 14 15 //声明设备信息对象,并设置信息 16 //参数是绑定MAC地址信息 17 CDeviceInfo Device_info_siasun_A,Device_info_siasun_B,Device_info_ROKAE; 18 Device_info_siasun_A.SetFullName("Basler acA1300-60gc#0030532699C7#192.168.2.202:3956"); 19 Device_info_siasun_B.SetFullName("Basler acA1300-60gc#0030532699C6#192.168.2.144:3956"); 20 Device_info_ROKAE.SetFullName("Basler acA1300-60gc#0030532699C8#192.168.2.203:3956"); 21 22 23 //把信息添加到filter 24 DeviceInfoList_t Device_filter_siasun_A, Device_filter_siasun_B, Device_filter_ROKAE; 25 Device_filter_siasun_A.push_back(Device_info_siasun_A); 26 Device_filter_siasun_B.push_back(Device_info_siasun_B); 27 Device_filter_ROKAE.push_back(Device_info_ROKAE); 28 29 //创建相机对象 30 CInstantCamera Camera_siasun_A,Camera_siasun_B,Camera_ROKAE; 31 32 33 //注意此处容易出现异常,打开相机异常 34 //信息匹配,如果匹配成功,打开相机 35 //连接并打开相机 36 DeviceInfoList_t device_temp; 37 if (tlFactory.EnumerateDevices(device_temp, Device_filter_siasun_A) > 0) 38 { 39 Camera_siasun_A.Attach(tlFactory.CreateDevice(device_temp[0])); 40 Camera_siasun_A.Open(); 41 } 42 if (tlFactory.EnumerateDevices(device_temp, Device_filter_siasun_B) > 0) 43 { 44 Camera_siasun_B.Attach(tlFactory.CreateDevice(device_temp[0])); 45 Camera_siasun_B.Open(); 46 } 47 if (tlFactory.EnumerateDevices(device_temp, Device_filter_ROKAE) > 0) 48 { 49 Camera_ROKAE.Attach(tlFactory.CreateDevice(device_temp[0])); 50 Camera_ROKAE.Open(); 51 } 52 53 54 //结果指针 55 //相机拍完照片之后会先把数据存入内存中,这里是放入了CGrabResultPtr指针对象中 56 CGrabResultPtr PtrGrabResult_siasun_A,PtrGrabResult_siasun_B,PtrGrabResult_ROKAE; 57 58 //开始抓拍 59 /*Camera_siasun_A.StartGrabbing(1); 60 Camera_siasun_B.StartGrabbing(1); 61 Camera_ROKAE.StartGrabbing(1); 62 //等待并检测,100ms超时 63 Camera_siasun_A.RetrieveResult( 100, PtrGrabResult_siasun_A, TimeoutHandling_ThrowException); 64 Camera_siasun_B.RetrieveResult( 100, PtrGrabResult_siasun_B, TimeoutHandling_ThrowException); 65 Camera_ROKAE.RetrieveResult( 100, PtrGrabResult_ROKAE, TimeoutHandling_ThrowException); 66 */ 67 //1000ms超时 68 //抓取一张图片 69 Camera_siasun_A.GrabOne(1000,PtrGrabResult_siasun_A, TimeoutHandling_ThrowException); 70 Camera_siasun_B.GrabOne(1000,PtrGrabResult_siasun_B, TimeoutHandling_ThrowException); 71 Camera_siasun_ROKAE.GrabOne(1000,PtrGrabResult_siasun_ROKAE, TimeoutHandling_ThrowException); 72 73 //****************接下来转换图片格式 74 //创建格式转换对象 75 CImageFormatConverter Format_converter_siasun_A,Format_converter_siasun_B,Format_converter_ROKAE; 76 CPylonImage PylonImage_Temp_siasun_A,PylonImage_Temp_siasun_B,PylonImage_Temp_ROKAE; 77 78 //设定转换格式 79 Format_converter_siasun_A.OutputPixelFormat = PixelType_BGR8packed; 80 Format_converter_siasun_A.OutputBitAlignment = OutputBitAlignment_MsbAligned; 81 82 Format_converter_siasun_B.OutputPixelFormat = PixelType_BGR8packed; 83 Format_converter_siasun_B.OutputBitAlignment = OutputBitAlignment_MsbAligned; 84 85 86 Format_converter_ROKAE.OutputPixelFormat = PixelType_BGR8packed; 87 Format_converter_ROKAE.OutputBitAlignment = OutputBitAlignment_MsbAligned; 88 89 90 Format_converter_siasun_A.Convert(PylonImage_Temp_siasun_A, PtrGrabResult_siasun_A); 91 Format_converter_siasun_B.Convert(PylonImage_Temp_siasun_B, PtrGrabResult_siasun_B); 92 Format_converter_ROKAE.Convert(PylonImage_Temp_ROKAE, PtrGrabResult_ROKAE); 93 94 好了,相机打开的问题解决了,接下来该处理图像了。(做个工程真的是要经历九九八十一难呀,好多莫名的bug) 95 96 对,还有一个问题就是格式转换,我拍完照片之后是pylon的图片,需要转换成opencv需要处理的格式也就是Mat,最开始以为挺麻烦的,想着实在不行就先保存到磁盘上,然后再用opencv读取过来,后来发现这个一行代码就搞定了: 97 98 //CPylonImage类图片 99 CPylonImage PylonImage_Temp; 100 //把指针指向的缓存数据转换为CPylonImage类 101 Format_converter.Convert(PylonImage_Temp, PtrGrabResult); 102 //把CPylonImage类转换为Mat类,其实图片读取到内容中就是一堆数据(三通道为三维数组),转换的时候只需要把buffer都去过来就好了 103 Mat temp_grab = cv::Mat(PtrGrabResult->GetHeight(), PtrGrabResult->GetWidth(), CV_8UC3, (uint8_t *)PylonImage_Temp.GetBuffer()); 104 105 106 三、opencv颜色分类 107 终于到重点了,这就是整个工程最核心的部分--颜色分类。其实颜色识别分类的方法有挺多的,我这里用了svm进行分类,主要也想了解一下机器学习的一些东西,为以后打点基础。 108 109 其实opencv已经把机器学习的框架都做好了,我们只需要添加数据,训练模型,只不过中间可能需要再调一下参数就好。 110 111 3.1添加数据及标签 112 113 之前一直不知道怎么添加标签,耽误了很多时间,后来发现其实特别简单,就是把读取图片的数据(矩阵)直接添加成训练集就好了,同时要再加上对应的标签。 114 115 需要注意的是svm训练数据的格式是CV_32FC1,而标签是CV_32SC1,所以在训练之前需要对数据进行处理一下,同时在predict的时候也需要处理。 116 117 添加标签以红色和黄色为例: 118 119 120 //------------------红色训练数据--------------------------// 121 Mat red_roi_uf = imread("C:/Users/ncutl/Desktop/red.png"); 122 //原图是CV_8UC3,例如255*255的像素,矩阵是255*255*3的矩阵,现在需要转换成,(255*255)*3的矩阵,格式为CV_32FC1 123 Mat red_roi_convert; 124 red_roi_uf.convertTo(red_roi_convert, CV_32FC1); 125 Mat red_roi_data(red_roi_convert.rows*red_roi_convert.cols, 3, CV_32FC1, red_roi_convert.data); 126 //生成对应的标签,红色标签为1 127 Mat red_label = Mat(red_roi_convert.rows*red_roi_convert.cols, 1, CV_32SC1, Scalar::all(1)); 128 129 //-------------------黄色训练数据--------------------// 130 Mat yellow_roi_uf = imread("C:/Users/ncutl/Desktop/yellow.png"); 131 Mat yellow_roi_convert; 132 //转换 133 yellow_roi_uf.convertTo(yellow_roi_convert, CV_32FC1); 134 Mat yellow_roi_data(yellow_roi_convert.rows*yellow_roi_convert.cols, 3, CV_32FC1, yellow_roi_convert.data); 135 //黄色标签为2 136 Mat yellow_label = Mat(yellow_roi_convert.rows*yellow_roi_convert.cols, 1, CV_32SC1, Scalar::all(2)); 137 //imshow("黄色", yellow_roi); 138 139 //-----------------合并所有的样本点,作为训练数据----------------------// 140 Mat train_data, train_label; 141 vconcat(red_roi_data, yellow_roi_data, train_data); 142 vconcat(red_label, yellow_label, train_label); 143 3.2训练模型并调整参数 144 145 训练模型其实就几行代码,需要做的就是改参数,使得分类最优。 146 147 本例是使用多分类,参数及优化可以参考这篇文章:OpenCV中的SVM参数优化 148 149 下边的参数是我已经调完之后的: 150 151 152 153 // 设置参数 154 155 Ptr<SVM> svm = SVM::create(); 156 svm->setType(SVM::C_SVC); 157 svm->setKernel(SVM::POLY); 158 //svm->setNu(0.5); 159 svm->setGamma(100); 160 //svm->setC(100); 161 svm->setDegree(0.08); 162 svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 100, 1e-6)); 163 164 // 训练分类器 165 Ptr<TrainData> tData = TrainData::create(train_data, ROW_SAMPLE, train_label); 166 167 svm->train(tData); 168 169 170 171 cout << "训练完成" << endl << endl; 172 173 174 3.3predict 175 176 这部分代码适用于分类,遍历像素提取rgb的值进行格式转换,然后predict,根据分类结果把该点像素点替换成理想的红黄蓝绿和背景黑色。 177 178 179 180 //设置颜色 181 Vec3b green(0, 255, 0), blue(255, 0, 0), red(0, 0, 255), yellow(0, 255, 255), black(0, 0, 0); 182 //分类颜色计数器 183 long red_numb = 0, yellow_numb = 0, green_numb = 0, blue_numb = 0, back_numb = 0; 184 Mat test_img=imread("C:\\Users\\ncutl\\Desktop\\samp.png"); 185 //一个一个像素predict,这个缺点是太慢了 186 for (int i = 0; i < test_img.rows; i++) 187 for (int j = 0; j < test_img.cols; j++) 188 { 189 //部分用于格式转关,在上一步已经说过这个问题 190 Vec3b pixel = test_img.at<Vec3b>(i, j); 191 float a_t = pixel[0]; 192 float b_t = pixel[1]; 193 float c_t = pixel[2]; 194 //cout << a_t << endl << b_t << endl << c_t << endl; 195 Mat sampleMat = (Mat_<float>(1, 3) << a_t, b_t, c_t); 196 int response = svm->predict(sampleMat); 197 198 if (response == 1) 199 { 200 test_img.at<Vec3b>(i, j) = red; red_numb++; 201 } 202 if (response == 2) 203 { 204 test_img.at<Vec3b>(i, j) = yellow; 205 yellow_numb++; 206 } 207 208 if (response == 3) 209 { 210 test_img.at<Vec3b>(i, j) = green; 211 green_numb++; 212 } 213 if (response == 4) 214 { 215 test_img.at<Vec3b>(i, j) = blue; 216 blue_numb++; 217 } 218 if (response == 5)//背景颜色 219 { 220 test_img.at<Vec3b>(i, j) = black; 221 back_numb++; 222 } 223 224 }
最后执行完之后得到的结果如下所示:
3.4速度优化
前面说过,分类是单个像素分类,这样的缺点是速度太慢,而且拍摄的照片也是高像素的图片,所以提高速度非常必要。最后识别用的方法是先对原图进行采样,再设置ROI区域,这样的话速度会提高不少。
有许多其他方法可以检测颜色,速度会比较快,用svm的话应该有其他训练模型的方法,可以快速分类。
采样前后的时间对比如下(上边原图,下边采样之后):
时间缩短了有10倍之多,这一部分资源我上传了,文件包括分类cpp文件和测试图片,有需要可以下载:svm颜色分类
四、网络通讯
单网络通讯这一部分网上资源挺多的,也很简单。重要的是在程序运行时候需要单独开线程,防止阻塞,这一部分跟TCP的通讯方式有关。
4.1开辟新线程
网络通信是会有阻塞的,为了防止通讯占用主线程资源,需要开辟新线程处理通讯的程序,开辟新线程主要包括三部分,首先声明线程函数和指针,定义线程函数,最后启动新线程。下边以相机线程为例。
1 //声明线程函数和指针 2 CWinThread* pRecvThread_Connect_Camera = NULL; 3 UINT RecvThread_Connect_Camera(LPVOID pParam); 4 //这一句启动新线程 5 pRecvThread_Connect_Camera = AfxBeginThread(RecvThread_Connect_Camera, this); 6 7 //这一部分是线程函数 8 UINT RecvThread_Connect_Camera(LPVOID pParam) 9 { 10 //传递对话框的this指针 11 C视觉Dlg* pThis = (C视觉Dlg*)pParam; 12 /* 13 这里填写代码 14 */ 15 return 0; 16 17 }
4.2网络通讯
这一部分是tcp通讯,打开服务等待连接,如果有连接判断接收数据,然后根据接收数据执行相应代码就好。
1 #include <stdio.h> 2 #include <winsock2.h> 3 4 #pragma comment(lib,"ws2_32.lib") 5 6 int main() 7 { 8 //初始化WSA 9 WORD sockVersion = MAKEWORD(2, 2); 10 WSADATA wsaData; 11 if (WSAStartup(sockVersion, &wsaData) != 0) 12 { 13 return 0; 14 } 15 16 17 //创建套接字 18 SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 19 if (slisten == INVALID_SOCKET) 20 { 21 printf("socket error !"); 22 return 0; 23 } 24 25 //绑定IP和端口 26 sockaddr_in sin; 27 sin.sin_family = AF_INET; 28 sin.sin_port = htons(4680); 29 sin.sin_addr.S_un.S_addr = inet_addr("192.168.2.211");// htonl(INADDR_ANY); //inet_addr("172.20.10.8"); 30 if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) 31 { 32 printf("bind error !"); 33 } 34 35 //开始监听 36 if (listen(slisten, 5) == SOCKET_ERROR) 37 { 38 printf("listen error !"); 39 return 0; 40 } 41 42 //循环接收数据 43 SOCKET sClient; 44 sockaddr_in remoteAddr; 45 int nAddrlen = sizeof(remoteAddr); 46 char revData[255]; 47 long linenum = 0; 48 while (true) 49 { 50 //printf("等待连接...\n"); 51 sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen); 52 if (sClient == INVALID_SOCKET) 53 { 54 printf("accept error !"); 55 continue; 56 } 57 58 59 //printf("第%d次:\n", linenum); 60 //linenum++; 61 62 //printf("接受到一个连接:%s :\r\n", inet_ntoa(remoteAddr.sin_addr)); 63 //printf("/t/t"); 64 //接收数据 65 int ret = recv(sClient, revData, 255, 0); 66 if (ret > 0) 67 { 68 revData[ret] = 0x00; 69 printf(revData); 70 } 71 printf("\n"); 72 //发送数据 73 //char send[4] = { 1, 1, 1, 1 }; 74 const char * sendData=""; 75 if (revData[1] == '1') 76 { 77 sendData = "9999"; 78 79 } 80 81 send(sClient, sendData, strlen(sendData), 0); 82 printf("发送结果:%s\n\n", sendData); 83 Sleep(100); 84 closesocket(sClient); 85 } 86 87 closesocket(slisten); 88 WSACleanup(); 89 return 0; 90 91 92 }
这个版本代码是网上的例程,但只能连接一个client,并且不能连续发送数据,可以适当修改使得可以连接多个client且连续发送数据。
目前核心的功能已经全部实现。