[转]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("等待连接...
");
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次:
", linenum);
60         //linenum++;
61  
62         //printf("接受到一个连接:%s :
", 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("
");
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

", sendData);
83         Sleep(100);
84         closesocket(sClient);
85     }
86  
87     closesocket(slisten);
88     WSACleanup();
89     return 0;
90  
91  
92 }
这个版本代码是网上的例程,但只能连接一个client,并且不能连续发送数据,可以适当修改使得可以连接多个client且连续发送数据。

目前核心的功能已经全部实现。

原文地址:https://www.cnblogs.com/Henry-ZHAO/p/12725218.html