zoukankan      html  css  js  c++  java
  • [转]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且连续发送数据。

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

  • 相关阅读:
    页面get请求 中文参数方法乱码问题
    java版ftp简易客户端(可以获取文件的名称及文件大小)
    文件下载
    kafka:一个分布式消息系统
    Executor的线程代码
    验证码的生成
    二维码的简单实现
    rsync实现大致流程描述
    C++中模板生成时机
    gcc虚函数表生成时机
  • 原文地址:https://www.cnblogs.com/Henry-ZHAO/p/12725218.html
Copyright © 2011-2022 走看看