OpenCV開發筆記(七十三):紅胖子8分鐘帶你使用opencv+dnn+yolov3識別物體

紅胖子(紅模仿)發表於2020-10-27
 

前言

  級聯分類器的效果並不是很好,準確度相對深度學習較低,上一章節使用了dnn中的tensorflow,本章使用yolov3模型,識別出具體的分類。

 

Demo

  320x320,置信度0.6
在這裡插入圖片描述
  608x608,置信度0.6(.cfg裡面是608)
在這裡插入圖片描述

 

yolov3模型下載

在這裡插入圖片描述

OpenCV深度識別基本流程

  opencv3.4.x支援了各種模型。

支援的模型

  opencv3.4.x支援一下深度學習的模型:
- caffe:.caffemodel
  官網:http://caffe.berkeleyvision.org
- tensorflow:.pb
  官網:https://www.tensorflow.org
- torch:.t7 | .net
  官網:http://torch.ch
- darknet:.weights
  官網:https://pjreddie.com/darknet
- DLDT:.bin
  官網:https://software.intel.com/openvino-toolkit

操作步驟:yolov3

  不同深度學習框架產生的模型,在操作上和資料輸出上有一些區別。梳理下opencv使用tensorflow訓練好的模型的使用步驟。

步驟一:讀取分類檔案

  模型檔案對應了不同的分類檔案,分類檔案是以行為標識,所在的行數(0開始),就是最終識別出的分類號的第幾個分類。

std::string classesFile = "E:/qtProject/openCVDemo/dnnData/" \
                    "yolov3/coco.names";
// 讀入分類名稱,存入快取
std::ifstream ifs(classesFile);
std::vector<std::string> classes;
std::string classLine;
while(std::getline(ifs, classLine))
{
    classes.push_back(classLine);
}

步驟二:載入模型和配置檔案,建立神經網路。

  根據不同的模型,使用cv::dnn::readNetFromXXX系列函式進行讀取,opencv3.4.x系列支援的dnn模型(支援模型往上看)。
  yolov3模型如下:

std::string modelWeights = "E:/qtProject/openCVDemo/dnnData/" \
                       "yolov3/yolov3.weights";
std::string modelCfg = "E:/qtProject/openCVDemo/dnnData/" \
                       "yolov3/yolov3.cfg";
// 載入yolov3模型
cv::dnn::Net net = cv::dnn::readNetFromDarknet(modelCfg, modelWeights);
if(net.empty())
{
    qDebug() << __FILE__ << __LINE__ << "net is empty!!!";
    return;
}

步驟三:將要預測的圖片加入到神經網路中

  加入之後,需要識別圖片,那麼需要把圖片輸入到神經網路當中去,使用yolov3模型特別注意,要先進行歸一化,然後變成指定大小的圖片,如下:

// 讀取圖片識別
mat = cv::imread("E:/testFile/15.jpg");
if(!mat.data)
{
    qDebug() << __FILE__ << __LINE__ << "Failed to read image!!!";
    return;
}
//    cv::dnn::blobFromImage(mat, blob);
//  必須要設定,否則會跑飛
cv::dnn::blobFromImage(mat,
                     blob,
                     1.0f/255,
                     cv::Size(320, 320),
                     cv::Scalar(0, 0, 0),
                     true,
                     false);
net.setInput(blob);

  寬度高度增加可以提升檢測的準確度,最好是根據cfg檔案進行修改,本Demo是320x320,實際.cfg檔案中的是608x608,並且經過測試,這個是識別效果最好的畫素,大於608則會跑飛。
  在這裡插入圖片描述

步驟四:分類預測,獲取識別的結果

  輸入之後,就進行識別,識別是向前預測(分類預測),並且拿到結果,對於yolov3模型,規定了有3個輸出層,所以需要先獲取3個輸出層,然後預測的時候就需要指定預測這3個輸出層,否則會跑飛。

// 獲取輸出的層
std::vector<cv::String> outPutNames;
std::vector<int> outLayers = net.getUnconnectedOutLayers();
for(int index = 0; index < outLayers.size(); index++)
{
    outPutNames.push_back(layerNames[outLayers[index] - 1]);
    qDebug() << __FILE__ << __LINE__
    << QString(layerNames[outLayers[index] - 1].c_str());
} 
// 推理預測:可以輸入預測的圖層名稱
std::vector<cv::Mat> probs;
net.forward(probs, outPutNames);

  對於預測的結果,存於std::vectorcv::Mat型別的probs,每一個元素指定為cv::Mat型別的prob,每一行代表一個檢測到的分類,具體列資訊如下表:
  在這裡插入圖片描述
  (注意:具體的使用,請參照“步驟五”)

步驟五:對達到置信度的可以通過輸出的mat進行分類和框選

  關鍵的輸出結果步驟,不同的識別有區別,yolov3如下圖:

// 置信度預製,大於執行度的將其使用rect框出來
for(int index = 0; index < probs.size(); index++)
{
    for (int row = 0; row < probs[index].rows; row++)
    {
        // 獲取probs中一個元素裡面匹配對的所有物件中得分最高的
        cv::Mat scores = probs[index].row(row).colRange(5, probs[index].cols);
        cv::Point classIdPoint;
        double confidence;
        // Get the value and location of the maximum score
        cv::minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
        if(confidence > 0.6)
        {
            qDebug() << __FILE__ << __LINE__ << confidence << classIdPoint.x;
            int centerX = (int)(probs.at(index).at<float>(row, 0) * mat.cols);
            int centerY = (int)(probs.at(index).at<float>(row, 1) * mat.rows);
            int width   = (int)(probs.at(index).at<float>(row, 2) * mat.cols);
            int height  = (int)(probs.at(index).at<float>(row, 3) * mat.rows);
            int left = centerX - width / 2;
            int top = centerY - height / 2;
            cv::Rect objectRect(left, top, width, height);
            cv::rectangle(mat, objectRect, cv::Scalar(255, 0, 0), 2);
            cv::String label = cv::format("%s:%.4f",
                                          classes[classIdPoint.x].data(),
                                          confidence);
            cv::putText(mat,
                        label,
                        cv::Point(left, top - 10),
                        cv::FONT_HERSHEY_SIMPLEX,
                        0.4,
                        cv::Scalar(0, 0, 255));
            qDebug() << __FILE__ << __LINE__
                    << centerX << centerY << width << height;
        }
    }
}
 

函式原型

讀取yolov3模型與配置檔案函式原型

Net readNetFromDarknet(const String &cfgFile,
                      const String &darknetModel = String());

  從檔案中讀取。

  • 引數一:帶有網路體系結構文字描述的.cfg檔案的路徑;
  • 引數二:已學習網路的.weights檔案的路徑;

讀取圖片(需要識別的)函式原型

void blobFromImage(InputArray image,
                  OutputArray blob,
                  double scalefactor=1.0,
                  const Size& size = Size(),
                  const Scalar& mean = Scalar(),
                  bool swapRB=false,
                  bool crop=false,
                  int ddepth=CV_32F);.

  從影像建立區域。可選擇從中心調整和裁剪影像。

  • 引數一:影像輸入影像(1、3或4通道);
  • 引數二:輸出的影像空間;
  • 引數三:影像值的縮放因子乘數;
  • 引數四:大小輸出影像的空間大小;
  • 引數五:從通道中減去平均值的平均標量。價值是有意的,如果image有BGR順序,swapRB為真,則按(mean-R,mean-G,mean-B)順序排列;
  • 引數六:swapRB標誌,指示交換第一個和最後一個通道,在三通道影像是必要的;
  • 引數七:裁剪標誌,指示調整大小後是否裁剪影像;
  • 引數八:輸出blob的深度,選擇CV_32F或CV_8U;

設定神經網路輸入函式原型

void cv::dnn::Net::setInput(InputArray blob,
                      const String& name = "",
                      double scalefactor = 1.0,
                      const Scalar& mean = Scalar());

  設定網路的新輸入值。

  • 引數一:一個新的blob。應具有CV_32F或CV_8U深度。
  • 引數二:輸入層的名稱。
  • 引數三:可選的標準化刻度。
  • 引數四:可選的平均減去值。

返回所有層的名稱(按照本身的索引循序排列)

std::vector<String> getLayerNames() const;

返回具有未連線輸出的層的索引。

std::vector<int> getUnconnectedOutLayers() const;

深度檢測識別(向前預測)函式原型

void cv::dnn::Net::Mat forward(const String& outputName = String());

  向前預測,返回指定層的第一個輸出的blob,一般是返回最後一層,可使用cv::Net::getLayarNames()獲取所有的層名稱。

  • 引數一:outputName需要獲取輸出的層的名稱
 

Demo

void OpenCVManager::testYoloV3()
{
    std::string classesFile = "E:/qtProject/openCVDemo/dnnData/" \
                              "yolov3/coco.names";
    std::string modelWeights = "E:/qtProject/openCVDemo/dnnData/" \
                          "yolov3/yolov3.weights";
    std::string modelCfg = "E:/qtProject/openCVDemo/dnnData/" \
                           "yolov3/yolov3.cfg";

    // 讀入分類名稱,存入快取
    std::ifstream ifs(classesFile);
    std::vector<std::string> classes;
    std::string classLine;
    while(std::getline(ifs, classLine))
    {
        classes.push_back(classLine);
    }

    // 載入yolov3模型
    cv::dnn::Net net = cv::dnn::readNetFromDarknet(modelCfg, modelWeights);
    if(net.empty())
    {
        qDebug() << __FILE__ << __LINE__ << "net is empty!!!";
        return;
    }

    cv::Mat mat;
    cv::Mat blob;

    // 獲得所有層的名稱和索引
    std::vector<cv::String> layerNames = net.getLayerNames();
    int lastLayerId = net.getLayerId(layerNames[layerNames.size() - 1]);
    cv::Ptr<cv::dnn::Layer> lastLayer = net.getLayer(cv::dnn::DictValue(lastLayerId));
    qDebug() << __FILE__ << __LINE__
             << QString(lastLayer->type.c_str())
             << QString(lastLayer->getDefaultName().c_str())
             << QString(layerNames[layerNames.size()-1].c_str());

    // 獲取輸出的層
    std::vector<cv::String> outPutNames;
    std::vector<int> outLayers = net.getUnconnectedOutLayers();
    for(int index = 0; index < outLayers.size(); index++)
    {
        outPutNames.push_back(layerNames[outLayers[index] - 1]);
        qDebug() << __FILE__ << __LINE__ 
                 << QString(layerNames[outLayers[index] - 1].c_str());
    }

    while(true)
    {
        // 讀取圖片識別
        mat = cv::imread("E:/testFile/15.jpg");
        if(!mat.data)
        {
            qDebug() << __FILE__ << __LINE__ << "Failed to read image!!!";
            return;
        }

//        cv::dnn::blobFromImage(mat, blob);
        // 必須要設定,否則會跑飛
        cv::dnn::blobFromImage(mat,
                               blob,
                               1.0f/255,
                               cv::Size(320, 320),
                               cv::Scalar(0, 0, 0),
                               true,
                               false);
        net.setInput(blob);
        // 推理預測:可以輸入預測的圖層名稱
        std::vector<cv::Mat> probs;
        net.forward(probs, outPutNames);

        // 顯示識別花費的時間
        std::vector<double> layersTimes;
        double freq = cv::getTickFrequency() / 1000;
        double t = net.getPerfProfile(layersTimes) / freq;
        std::string label = cv::format("Inference time: %.2f ms", t);
        cv::putText(mat,
                  label,
                  cv::Point(0, 15),
                  cv::FONT_HERSHEY_SIMPLEX,
                  0.5,
                  cv::Scalar(255, 0, 0));
        // 置信度預製,大於執行度的將其使用rect框出來
        for(int index = 0; index < probs.size(); index++)
        {
            for (int row = 0; row < probs[index].rows; row++)
            {
                // 獲取probs中一個元素裡面匹配對的所有物件中得分最高的
                cv::Mat scores = probs[index].row(row).colRange(5, probs[index].cols);
                cv::Point classIdPoint;
                double confidence;
                // Get the value and location of the maximum score
                cv::minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
                if(confidence > 0.6)
                {
                    qDebug() << __FILE__ << __LINE__ << confidence << classIdPoint.x;
                    int centerX = (int)(probs.at(index).at<float>(row, 0) * mat.cols);
                    int centerY = (int)(probs.at(index).at<float>(row, 1) * mat.rows);
                    int width   = (int)(probs.at(index).at<float>(row, 2) * mat.cols);
                    int height  = (int)(probs.at(index).at<float>(row, 3) * mat.rows);
                    int left = centerX - width / 2;
                    int top = centerY - height / 2;
                    cv::Rect objectRect(left, top, width, height);
                    cv::rectangle(mat, objectRect, cv::Scalar(255, 0, 0), 2);
                    cv::String label = cv::format("%s:%.4f",
                                                  classes[classIdPoint.x].data(),
                                                  confidence);
                    cv::putText(mat,
                                label,
                                cv::Point(left, top - 10),
                                cv::FONT_HERSHEY_SIMPLEX,
                                0.4,
                                cv::Scalar(0, 0, 255));
                    qDebug() << __FILE__ << __LINE__
                            << centerX << centerY << width << height;
                }
            }
        }

        cv::imshow(_windowTitle.toStdString(), mat);
        cv::waitKey(0);
    }
}
 

對應工程模板v1.65.0

  openCVDemo_v1.65.0_基礎模板_yolov3分類檢測.rar。

 

入坑

入坑一:載入模型時候錯誤

錯誤
  在這裡插入圖片描述

原因
  模型檔案載入錯誤。
解決
  檢查檔案是否存在,路徑是否正確,模型檔案是否能對應上。

入坑二:輸入blob時錯誤

錯誤
  在這裡插入圖片描述

原因
  預測的時候未輸入引數,需要輸入引數(注意:tensorflow未輸入沒有問題)。
解決
  在這裡插入圖片描述

 

相關文章