Qt5&OpenCV3.2 Canny邊緣檢測+Hough變換

at_1發表於2021-09-09

省電賽需求,要在微控制器平臺上使用攝像頭模組採集影像,透過串列埠通訊或者其他通訊方式傳輸到上位機,由上位機來識別併傳送指令給下位機。識別目標是綠色的未成熟柚子,如下圖。透過顏色的識別是不太現實的了,但幸好柚子形狀近似圓形,所以想到透過使用Hough變換檢測圓,從而檢測柚子。


圖片描述

用來模擬在樹上的未成熟柚子

1.硬體平臺

  • STM32F103ZET6微控制器

  • OV7670帶FIFO攝像頭模組,最高支援320*240解析度

  • 一臺有USB介面的電腦

2.上位機軟體

我使用Qt進行開發。這個我個人還是比較有經驗的。這裡主要是RGB565轉RGB888編碼演算法和串列埠通訊的編寫。
RGB565是一種顏色編碼方式,相對於一般的RGB888(紅綠藍各8位,三個位元組)的儲存方式更節省空間,一般用在儲存空間比較少的場合,比如嵌入式系統。她可以儲存256*256=65536種顏色,圖片質量相比而言不會受到太大影響。


圖片描述

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模式選擇中使用黑白模式,這樣噪點會比較少


圖片描述

原始影像

  • 有時依舊會有不少噪點,先來一發高斯模糊再說

//讀取影像並定義處理過程中使用的MatMat 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)是卷積核的大小,最後一個引數是高斯函式的σ。效果如下,稍微有點糊但無傷大雅。

圖片描述

高斯模糊去噪


  • Canny邊緣檢測。引數要多次嘗試

//Canny邊緣檢測Canny(originalImageL,cannyImageL,CANNY_VAR/2,CANNY_VAR);

Canny運算元通常基於Soble運算元。透過一個高閾值的Sobel和低閾值的Sobel綜合考量可以得到比較令人滿意的結果。柚子的大概輪廓出來了,雖然周圍的樹葉的輪廓會對識別有干擾,但沒關係。


圖片描述

Canny

  • 閉運算去除細小紋理

//結構元素Mat element5(M_R_SIZE,M_R_SIZE,CV_8U,cv::Scalar(1));
morphologyEx(cannyImageL,closeImageL,MORPH_CLOSE,element5);

閉運算的定義是對影像先膨脹後腐蝕。比結構元素小的空隙和間隙會被閉合濾波器消除。可以看到柚子周圍的小輪廓基本消除了。


圖片描述

Close

閉運算的定義是對影像先膨脹後腐蝕。比結構元素小的空隙和間隙會被閉合濾波器消除。

  • 提取連續區域中的輪廓(非必需)

findContours(closeImageL,contoursL, CV_RETR_LIST,CV_CHAIN_APPROX_NONE);
drawContours(contoursImageL, contoursL, -1, Scalar(255, 0, 255));

第三個引數使用CV_RETR_LIST 指提取所有輪廓。其他的值可以提取其中的層次結構,這裡不介紹。經實驗發現這一步可以略過,但加上的話更加直觀。

圖片描述

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 圓最大半徑



作者:Jacob楊幫幫
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1817/viewspace-2819352/,如需轉載,請註明出處,否則將追究法律責任。

相關文章