Qt5&OpenCV3.2 Canny边缘检测+Hough变换识别未成熟柚子

简介: 省电赛需求,要在单片机平台上使用摄像头模组采集图像,通过串口通信或者其他通信方式传输到上位机,由上位机来识别并发送指令给下位机。识别目标是绿色的未成熟柚子,如下图。

省电赛需求,要在单片机平台上使用摄像头模组采集图像,通过串口通信或者其他通信方式传输到上位机,由上位机来识别并发送指令给下位机。识别目标是绿色的未成熟柚子,如下图。通过颜色的识别是不太现实的了,但幸好柚子形状近似圆形,所以想到通过使用Hough变换检测圆,从而检测柚子。


img_f73301b2c0eb269056afadd7a1211707.png
用来模拟在树上的未成熟柚子

1.硬件平台

  • STM32F103ZET6单片机
  • OV7670带FIFO摄像头模组,最高支持320*240分辨率
  • 一台有USB接口的电脑

2.上位机软件

我使用Qt进行开发。这个我个人还是比较有经验的。这里主要是RGB565转RGB888编码算法和串口通信的编写。
RGB565是一种颜色编码方式,相对于一般的RGB888(红绿蓝各8位,三个字节)的存储方式更节省空间,一般用在存储空间比较少的场合,比如嵌入式系统。她可以存储256*256=65536种颜色,图片质量相比而言不会受到太大影响。


img_ab947813ca3c57a214dd868cb3b921d3.jpe
RGB565
  • RGB565转RGB888的主要思路是截取有效的5/6/5位,然后通过左移和右移使其实际只有8位(高8位为0),最后拼接到代表RGB888编码的uint类型变量。
/************颜色编码转换*************/
#define RGB888_RED      0x00ff0000
#define RGB888_GREEN    0x0000ff00
#define RGB888_BLUE     0x000000ff

#define RGB565_RED      0xf800
#define RGB565_GREEN    0x07e0
#define RGB565_BLUE     0x001f


//编码转换
unsigned short RGB888ToRGB565(unsigned int n888Color)
{
    unsigned short n565Color = 0;

    // 获取RGB单色,并截取高位
    unsigned char cRed   = (n888Color & RGB888_RED)   >> 19;
    unsigned char cGreen = (n888Color & RGB888_GREEN) >> 10;
    unsigned char cBlue  = (n888Color & RGB888_BLUE)  >> 3;

    // 连接
    n565Color = (cRed << 11) + (cGreen << 5) + (cBlue << 0);
    return n565Color;
}

unsigned int RGB565ToRGB888(unsigned short n565Color)
{
    unsigned int n888Color = 0;

    // 获取RGB单色,并填充低位
    unsigned char cRed   = (n565Color & RGB565_RED)    >> 8;
    unsigned char cGreen = (n565Color & RGB565_GREEN)  >> 3;
    unsigned char cBlue  = (n565Color & RGB565_BLUE)   << 3;

    // 连接
    n888Color = (cRed << 16) + (cGreen << 8) + (cBlue << 0);
    return n888Color;
}
  • 在MainWindow种初始化一些参数
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{

    ui->setupUi(this);
    //初始化时获取可用的串口
    initSerialPort();
    //创建一个串口对象
    tc = QTextCodec::codecForName("GBK");   //支持汉字编码
    serial = new QSerialPort;
    //当串口准备好读数据时,调用读取数据的函数
    connect(serial,SIGNAL(readyRead()),this,SLOT(readSerialData()));
    //接收数据框设置为只读模式
  //  ui->textEdit->setReadOnly(true);

    //初始化波特率,默认921600
    baudBox=new QComboBox;
    QStringList baudItems;
    baudItems<<"1382400"<<"921600"<<"46080"<<"256000"<<"230400"
             <<"128000"<<"115200"<<"76800"<<"57600"<<"43000"<<"38400"<<"19200"
             <<"14400"<<"9600"<<"4800"<<"2400"<<"1200";
    ui->baudBox->addItems(baudItems);
    ui->baudBox->setCurrentIndex(1);

    ui->sendButton->setEnabled(false);//开启串口之前限制发送按钮
}
  • 不断刷新串口列表
//鼠标点击刷新当前存在的串口
void MainWindow::mousePressEvent(QMouseEvent *e)
{
    serial->clear();     //清掉所有串口,重新检测当前串口
    ui->portBox->clear();//删除当前所以已经存在的串口号
    initSerialPort();
}

void MainWindow::initSerialPort()
{
    QList<QSerialPortInfo>  infos = QSerialPortInfo::availablePorts();
    if(infos.isEmpty())
    {
      //  QMessageBox::information(this,"提示信息","当前没有可用的串口");
        return;
    }
    //将搜索串口号添加到下拉列表中
    foreach (QSerialPortInfo info, infos)
    {
        ui->portBox->addItem(info.portName());
    }
}
  • 打开串口时根据下拉框的参数进行初始化
//打开或者关闭串口
void MainWindow::on_openSerialButton_toggled(bool checked)
{
    if(ui->connectPushButton->text() == "断开")
    {
        QMessageBox::information(this,"提示信息","请关闭WIFI连接");
        return;
    }
    //这里可以输出一些串口出错的信息
    if(serial->error()==serial->DeviceNotFoundError)
    {
        QMessageBox::information(this,"提示信息","串口打开失败");
        return;
    }
    if(checked)
    {
        ui->openSerialButton->setText(tr("关闭串口"));
        //设置串口名
        serial->setPortName(ui->portBox->currentText());
        //打开串口
        serial->open(QIODevice::ReadWrite);
        //设置波特率
        serial->setBaudRate(ui->baudBox->currentText().toInt());
        //设置数据位
        serial->setDataBits(QSerialPort::Data8);
        //设置奇偶校验
        serial->setParity(QSerialPort::NoParity);
         //设置停止位
        serial->setStopBits(QSerialPort::OneAndHalfStop);

        //关闭串口设置按钮,使能数据发送按钮
        ui->portBox->setEnabled(false);
        ui->baudBox->setEnabled(false);
        ui->sendButton->setEnabled(true);
    }

    else
    {
        ui->openSerialButton->setText(tr("打开串口"));
        //关闭串口
        serial->clear();
        serial->close();
        //使能串口设置按钮,关闭数据发送按钮
        ui->portBox->setEnabled(true);
        ui->baudBox->setEnabled(true);
        ui->sendButton->setEnabled(false);

    }
}
  • 读取和清除
//串口读取数据
void MainWindow::readSerialData()
{
//接收的时候要将QByteArray转换成String才能够方便的显示在textEdit中
    QByteArray buf;
    buf=serial->readAll();
    if(buf!=NULL)
    {
        data+=(tc->toUnicode(buf));
    }
    buf.clear();
}

//清除数据接收区的数据
void MainWindow::on_clearDataButton_clicked()
{
    data.clear();
    random_color();
    ui->leftLabel->setText("柚子呢?");
    ui->rightLabel->setText("柚子呢?");
    update();
   // ui->textEdit->clear();
}

3.图像识别

由于不需要颜色信息,我在OV7670模式选择中使用黑白模式,这样噪点会比较少


img_766538f19c0411aec6107496dc3b7258.png
原始图像
  • 有时依旧会有不少噪点,先来一发高斯模糊再说
//读取图像并定义处理过程中使用的Mat
Mat originalImageL =imread("/original_left.png");
Mat cannyImageL;
Mat closeImageL;
Mat contoursImageL = Mat::zeros(originalImageL.size(),CV_8U);;
vector<vector<Point>> contoursL;
Mat outputImageL = originalImageL.clone();

//滤波去除噪点
GaussianBlur(originalImageL,originalImageL,cv::Size(BLUR_SIZE,BLUR_SIZE),1.5);

cv::Size(BLUR_SIZE,BLUR_SIZE)是卷积核的大小,最后一个参数是高斯函数的σ。效果如下,稍微有点糊但无伤大雅。

img_08be720829ae559b173b72b0674b64cb.png
高斯模糊去噪

  • Canny边缘检测。参数要多次尝试
//Canny边缘检测
Canny(originalImageL,cannyImageL,CANNY_VAR/2,CANNY_VAR);

Canny算子通常基于Soble算子。通过一个高阈值的Sobel和低阈值的Sobel综合考量可以得到比较令人满意的结果。柚子的大概轮廓出来了,虽然周围的树叶的轮廓会对识别有干扰,但没关系。


img_d382a3d5f86d18c225921ed11b21691a.png
Canny
  • 闭运算去除细小纹理
//结构元素
Mat element5(M_R_SIZE,M_R_SIZE,CV_8U,cv::Scalar(1));
morphologyEx(cannyImageL,closeImageL,MORPH_CLOSE,element5);

闭运算的定义是对图像先膨胀后腐蚀。比结构元素小的空隙和间隙会被闭合滤波器消除。可以看到柚子周围的小轮廓基本消除了。


img_b3a7afa759f7201100f1487d024ec2f3.png
Close

闭运算的定义是对图像先膨胀后腐蚀。比结构元素小的空隙和间隙会被闭合滤波器消除。

  • 提取连续区域中的轮廓(非必需)
findContours(closeImageL,contoursL, CV_RETR_LIST,CV_CHAIN_APPROX_NONE);
drawContours(contoursImageL, contoursL, -1, Scalar(255, 0, 255));

第三个参数使用CV_RETR_LIST 指提取所有轮廓。其他的值可以提取其中的层次结构,这里不介绍。经实验发现这一步可以略过,但加上的话更加直观。

img_6a3fe2c67485a8557839da2069cab92a.png
findContours
  • Hough变换检测圆形
std::vector<cv::Vec3f> circlesL;
HoughCircles(contoursImageL,circlesL,HOUGH_GRADIENT,2,CIRCLES_BAR,
             CANNY_VAR,HOUGH_STD,R_MIN,R_MAX);
std::vector<cv::Vec3f>::const_iterator itcL = circlesL.begin();
while(itcL!=circlesL.end())
{
   cv::circle(outputImageL,cv::Point((*itcL)[0],(*itcL)[1]),(*itcL)[2],
              cv::Scalar(188),4);
        cout << cv::Point((*itcL)[0],(*itcL)[1]) << (*itcL)[2] <<endl;
        ++itcL;
}

HoughCircles这个函数比较有意思,有必要看看函数原型。

void HoughCircles( InputArray image, OutputArray circles,
                   int method, double dp, double minDist,
                   double param1 = 100, double param2 = 100,
                   int minRadius = 0, int maxRadius = 0 );

源码中参数解释的翻译大概如下


§ method 使用的检测方法,一般使用霍夫梯度法,它的标识符为CV_HOUGH_GRADIENT
§ dp 用来检测圆心的累加器图像的分辨率于输入图像之比的倒数
§ minDist 圆之间最小的距离
§ param1 第三个参数 method设置的检测方法的对应的第一个参数,对于CV_HOUGH_GRADIENT为传递给canny边缘检测算子的高阈值
§ param2 第三个参数 method设置的检测方法的对应的第二个参数,表示在检测阶段圆心的累加器阈值,越低检测出的圆越多
§ minRadius 圆最小半径
§ minRadius 圆最大半径


参数method设为 CV_HOUGH_GRADIENTdp 设为2 即可。在原图像上使用迭代器画出所有圆形,效果如下

img_337a43e5c0b832eab0d6a788ce283bee.png
输出结果

mission completed!

4.经验总结

  • 在背景过于复杂的情况下,可以将图片转为HSI色域并且使用掩膜的方式将绿色的柚子和树叶分割出来。这里没有使用。
  • 一系列操作都是一些比较基础的算法,但需要一些经验去选择和去调参。
  • 要考虑摄像头和灯光等硬件因素
目录
相关文章
|
2月前
|
算法 计算机视觉
基于qt的opencv实时图像处理框架FastCvLearn实战
本文介绍了一个基于Qt的OpenCV实时图像处理框架FastCvLearn,通过手撕代码的方式详细讲解了如何实现实时人脸马赛克等功能,并提供了结果展示和基础知识回顾。
104 7
基于qt的opencv实时图像处理框架FastCvLearn实战
|
2月前
|
文字识别 计算机视觉 开发者
基于QT的OCR和opencv融合框架FastOCRLearn实战
本文介绍了在Qt环境下结合OpenCV库构建OCR识别系统的实战方法,通过FastOCRLearn项目,读者可以学习Tesseract OCR的编译配置和在Windows平台下的实践步骤,文章提供了技术资源链接,帮助开发者理解并实现OCR技术。
128 9
基于QT的OCR和opencv融合框架FastOCRLearn实战
|
2月前
|
计算机视觉
基于QT的opencv插件框架qtCvFrameLearn实战
这篇文章详细介绍了如何基于Qt框架开发一个名为qtCvFrameLearn的OpenCV插件,包括项目配置、插件加载、Qt与OpenCV图像转换,以及通过各个插件学习OpenCV函数的使用,如仿射变换、卡通效果、腐蚀、旋转和锐化等。
44 10
|
2月前
|
机器学习/深度学习 Java 计算机视觉
opencv4.5.5+qt5.15.2+vtk9.1+mingw81_64编译记录
本文记录了使用mingw81_64编译OpenCV 4.5.5、Qt 5.15.2、VTK 9.1的详细过程,包括编译结果截图、编译步骤、遇到的问题及其解决方案,以及相关参考链接。文中还提到了如何编译boost源码为静态库,并提供了测试代码示例。
opencv4.5.5+qt5.15.2+vtk9.1+mingw81_64编译记录
|
3月前
|
计算机视觉
使用QT显示OpenCV读取的图片
使用QT显示OpenCV读取的图片
70 1
|
4月前
|
机器学习/深度学习 人工智能 计算机视觉
好的资源-----打卡机+Arm+Qt+OpenCV嵌入式项目-基于人脸识别的考勤系统-----B站神经网络与深度学习,商城
好的资源-----打卡机+Arm+Qt+OpenCV嵌入式项目-基于人脸识别的考勤系统-----B站神经网络与深度学习,商城
|
1月前
|
计算机视觉
Opencv学习笔记(三):图像二值化函数cv2.threshold函数详解
这篇文章详细介绍了OpenCV库中的图像二值化函数`cv2.threshold`,包括二值化的概念、常见的阈值类型、函数的参数说明以及通过代码实例展示了如何应用该函数进行图像二值化处理,并展示了运行结果。
330 0
Opencv学习笔记(三):图像二值化函数cv2.threshold函数详解
|
2月前
|
算法 计算机视觉
opencv图像形态学
图像形态学是一种基于数学形态学的图像处理技术,它主要用于分析和修改图像的形状和结构。
49 4
|
2月前
|
存储 计算机视觉
Opencv的基本操作(一)图像的读取显示存储及几何图形的绘制
本文介绍了使用OpenCV进行图像读取、显示和存储的基本操作,以及如何绘制直线、圆形、矩形和文本等几何图形的方法。
Opencv的基本操作(一)图像的读取显示存储及几何图形的绘制
|
3月前
|
算法 计算机视觉 Python
python利用opencv进行相机标定获取参数,并根据畸变参数修正图像附有全部代码(流畅无痛版)
该文章详细介绍了使用Python和OpenCV进行相机标定以获取畸变参数,并提供了修正图像畸变的全部代码,包括生成棋盘图、拍摄标定图像、标定过程和畸变矫正等步骤。
python利用opencv进行相机标定获取参数,并根据畸变参数修正图像附有全部代码(流畅无痛版)