使用deepstream對自己模型進行加速推理以及與ROS通信

背景:deepstream檢測到的結果:框位置和目標類型、概率值需要進一步傳遞到ros節點中分析使用,本例是採用TCP/IP通信協議將這些數據發送出去

來源:使用darknet框架,利用yolov3-tiny模型在nvidia jetson nano上進行目標檢測推理的時候,幀率較低,約6ps,不能滿足實際任務需求。慶幸的是Nvidia提供了很多加速工具,典型的如tensorRT和deepstream。

1.deepstream使用過程

1.1 安裝

如果是使用nvidia的設備如nano ,如在燒錄系統時不自定義安裝的話,deepstream會一併被安裝上。
  如果沒有安裝,可按照如下方式進行安裝:

1.1.1 安裝依賴:

$ sudo apt install \
    libssl1.0.0 \
    libgstreamer1.0-0 \
    gstreamer1.0-tools \
    gstreamer1.0-plugins-good \
    gstreamer1.0-plugins-bad \
    gstreamer1.0-plugins-ugly \
    gstreamer1.0-libav \
    libgstrtspserver-1.0-0 \
    libjansson4=2.11-1

1.1.2 安裝librdkafka

sudo apt-get install librdkafka1=0.11.3-1build1

1.1.3 安裝DeepStream SDK

這裏下載,然後通過如下執行解壓縮:

$ tar -xpvf deepstream_sdk_v4.0.2_jetson.tbz2

之後依次執行如下指令進行安裝:

$ cd deepstream_sdk_v4.0.2_jetson
$ sudo tar -xvpf binaries.tbz2 -C /
$ sudo ./install.sh
$ sudo ldconfig

1.2 例子-加速官方模型進行推理

可以首先運行這個例子,來嚐鮮,之後來解釋其實際實現過程:
### 1.2.1 加速yolov3-tiny模型並推理:

cd /home/inano/deepstream/sources/objectDetector_Yolo
deepstream-app -c deepstream_app_config_yoloV3_tiny.txt

如果一切運行正常,且是第一次運行(目錄中沒有deepstream生成的加速模型),deepstream會下載yoloV3_tiny的權重文件,配置參數等文件,然後接着會生成加速模型,這個可能會需要一段時間,之後便會顯示實時的圖像及檢測效果(含檢測框)。若非第一次運行,即deepstream檢測到目錄中已經有加速模型,則直接開始進行推理,耗時也會短很多。

可以看到deepstream使用過程很簡單,通過使用deepstream-app來加載配置文件就可以,所以一個關鍵的點就是這個配置文件:

#### 1.2.1.1 deepstream_app_config_yoloV3_tiny.txt文件
文件的有效部分如下,一般需要修改的參數如註釋所示:

[application]
enable-perf-measurement=1
perf-measurement-interval-sec=5
#gie-kitti-output-dir=streamscl

[tiled-display]
enable=1
rows=1
columns=1
width=1280            # 在推理中,要使用的圖像大小 
height=720
gpu-id=0
#(0): nvbuf-mem-default - Default memory allocated, specific to particular platform
#(1): nvbuf-mem-cuda-pinned - Allocate Pinned/Host cuda memory, applicable for Tesla
#(2): nvbuf-mem-cuda-device - Allocate Device cuda memory, applicable for Tesla
#(3): nvbuf-mem-cuda-unified - Allocate Unified cuda memory, applicable for Tesla
#(4): nvbuf-mem-surface-array - Allocate Surface Array memory, applicable for Jetson
nvbuf-memory-type=0

[source0]
enable=1
#Type - 1=Camera V4L2 2=URI 3=MultiURI
type=3                        # 如果要用攝像頭進行檢測,就將type設置爲0
uri=file://../../samples/streams/sample_1080p_h264.mp4
num-sources=1
gpu-id=0
# (0): memtype_device   - Memory type Device
# (1): memtype_pinned   - Memory type Host Pinned
# (2): memtype_unified  - Memory type Unified
cudadec-memtype=0

[sink0]
enable=1
#Type - 1=FakeSink 2=EglSink 3=File
type=2
sync=0
source-id=0
gpu-id=0
nvbuf-memory-type=0

[osd]
enable=1
gpu-id=0
border-width=1
text-size=15
text-color=1;1;1;1;
text-bg-color=0.3;0.3;0.3;1
font=Serif
show-clock=0
clock-x-offset=800
clock-y-offset=820
clock-text-size=12
clock-color=1;0;0;0
nvbuf-memory-type=0

[streammux]
gpu-id=0
##Boolean property to inform muxer that sources are live
live-source=0
batch-size=1
##time out in usec, to wait after the first buffer is available
##to push the batch even if the complete batch is not formed
batched-push-timeout=40000
## Set muxer output width and height
width=1920
height=1080
##Enable to maintain aspect ratio wrt source, and allow black borders, works
##along with width, height properties
enable-padding=0
nvbuf-memory-type=0

# config-file property is mandatory for any gie section.
# Other properties are optional and if set will override the properties set in
# the infer config file.
[primary-gie]
enable=1
gpu-id=0
#model-engine-file=model_b1_fp32.engine
labelfile-path=labels.txt
batch-size=1
#Required by the app for OSD, not a plugin property
bbox-border-color0=1;0;0;1
bbox-border-color1=0;1;1;1
bbox-border-color2=0;0;1;1
bbox-border-color3=0;1;0;1
gie-unique-id=1
nvbuf-memory-type=0
config-file=config_infer_primary_yoloV3_tiny.txt     #加載的配置文件(這個裏面要設置我們想要加速的模型,以及模型相關的配置文件)

[tests]
file-loop=0

1.2.1.2 config_infer_primary_yoloV3_tiny.txt文件

該文件中要指定的有效信息很多,若模型文件,模型配置文件等。

[property]
gpu-id=0
net-scale-factor=1
#0=RGB, 1=BGR
model-color-format=0
custom-network-config=yolov3-tiny.cfg   # 模型配置文件
model-file=yolov3-tiny.weights      #  模型權重文件

model-engine-file=model_b1_fp32.engine  #  這裏可以直接給出engine模型,就不用每次浪費大量時間來轉換模型了,v3尤其浪費時間。每次改動需要把其屏蔽,執行時打開
labelfile-path=labels.txt          # 標籤文件,即模型對應的要檢測的圖片列表
## 0=FP32, 1=INT8, 2=FP16 mode
network-mode=0                # 網絡計算要使用的浮點數類型,這個要看你GPU設備是否支持以及你的算力要求,默認爲FP32
num-detected-classes=80          # 模型分類的個數
gie-unique-id=1
is-classifier=0
maintain-aspect-ratio=1
parse-bbox-func-name=NvDsInferParseCustomYoloV3Tiny  
# 下面加載的是用於檢測後處理的一個動態庫,是deepstream前端檢測和後端後處理之間的一個接口該動態庫是將nvdsinfer_custom_impl_Yolo目錄下的文件編譯得到的,nvdsinfer_custom_impl_Yolo主要是面向後處理的,如檢測完後,矩形框繪製,檢測類型顯示,閾值設置等。如果需要修改這些參數,則修改完後需要重新編譯。 
custom-lib-path=nvdsinfer_custom_impl_Yolo/libnvdsinfer_custom_impl_Yolo.so

1.2.2 加速yolov3並推理:

安裝同樣的運行方式可以查看yolov3加速推理的效果。

$ cd /home/inano/deepstream/sources/objectDetector_Yolo
$ deepstream-app -c deepstream_app_config_yoloV3.txt

1.3 實踐-加速自己的模型完成推理

根據上述配置文件的描述,我們想加速我們自己訓練好的yolov3-tiny模型並使用攝像頭實時推理,需要進行如下幾步驟:

1.3.1 準備必要的文件

需要將我們自己的模型文件xf_yolov3-tiny.weights,xf_yolov3-tiny.cfg,imagelist.txt拷貝到/home/inano/deepstream/sources/objectDetector_Yolo目錄中,這些是加速必須的文件。

1.3.2 修改deepstream_app_config_yoloV3_tiny.txt文件

1.3.2.1 修改圖像來源

我們圖像來源於攝像頭,即將deepstream_app_config_yoloV3_tiny.txt文件中,type=3 修改爲  type=1;

1.3.2.2 修改圖像大小

由於nano算力有限約0.47t,爲了想使推理加速後有高的幀率,將圖像大小修改,即將即將deepstream_app_config_yoloV3_tiny.txt文件中;width=1280 height=720,修改爲width=640 height=360

1.3.3 修改config_infer_primary_yoloV3_tiny.txt文件

1.3.3.1 指定模型配置文件

由於文件就在本文件夾中,所以不需要使用什麼絕對路徑和相對路徑,直接修改,即:
將文件中  custom-network-config=yolov3-tiny.cfg   修改爲   custom-network-config=xf_yolov3-tiny.cfg

1.3.3.2 指定模型文件

由於文件就在本文件夾中,所以不需要使用什麼絕對路徑和相對路徑,直接修改,即:
將文件中  model-file=yolov3-tiny.weights   修改爲   model-file=xf_yolov3-tiny.weights

1.3.3.3 指定圖片集

將labelfile-path=labels.txt 修改爲   labelfile-path=imagelist.txt

1.3.3.4 修改類型個數

將num-detected-classes=80   修改爲  num-detected-classes=12 (模型對應的imagelist對應12種物品)

2.與ROS進行集成

如果想要使用檢測完的結果,如根據識別結果進行其他的操作。就需要我們將裏面的檢測框位置、類型、置信度等信息提取出來給其他節點使用。我們這裏的需求是將其結果傳遞給機器人動作執行節點,即需要將檢測結果發送到ros網絡中。在這裏我們通過socket通信作爲傳輸方式。編寫一箇中間節點,一邊通過socket接收數據,另一邊通過ros的話題和服務發佈出去。

而通過分析瞭解到 後處理功能全部在nvdsinfer_custom_impl_Yolo文件夾中,其中我們需要的數據都在nvdsparsebbox_Yolo.cpp文件中,所以我們修改這個文件就可以了。操作步驟如下:

2.1 修改程序:nvdsinfer_custom_impl_Yolo.cpp

2.1.1 添加頭文件

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <sstream>

2.1.2 函數decodeYoloV3Tensor()函數

末尾 "return binfo;“上方添加以下代碼:

std::cout<<"There are "<<binfo.size()<<" kinds of object" <<endl;
if(binfo.size()>0)
{
    string send_result = "AA";
    send_result = send_result + int_to_string(binfo.size());
    for(int i=0;i<binfo.size();i++)
    {
        std::cout <<"class::"<<binfo[i].classId <<" location:"<<" x:"<<binfo[i].left<<" y:"<<binfo[i].top<<" w:"<<binfo[i].width<<" h:"<<binfo[i].height <<" confidence:"<<binfo[i].detectionConfidence<<std::endl;
        send_result = send_result +"BB"+int_to_string(binfo[i].classId)+int_to_string(binfo[i].left)+int_to_string(binfo[i].top)+int_to_string(binfo[i].width)+int_to_string(binfo[i].height)+"CC";
    }
    send_result = send_result + "DD";
    socket_write(send_result);
}

2.1.3 位置3:添加自定義函數int_to_string()

在decodeYoloV3Tensor()函數上方空白處添加自定義函數:int_to_string(int)

string int_to_string(int a)
{
    stringstream ss;
    string str;
    ss << a;
    ss >> str;
    if(str.size()<4)
    {
        string s3(4-str.size(),'0');
        str = s3+str;
    }
    return str;
}

2.1.4 位置4:添加自定義函數socket_write()

在decodeYoloV3Tensor()函數上方空白處添加自定義函數:socket_write(std::string s)

bool socket_write(std::string s)
{
    int socket_fd = socket(AF_INET,SOCK_STREAM,0);
    if(socket_fd == -1)
    {
        cout<<"socket create failed"<<endl;
        exit(-1);
    }
    struct sockaddr_in_addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int res = connect(socket_fd, (struct sockaddr *)&addr, sizeof(addr));
    if (res == -1)
    {
        cout << "bind 鏈接失敗:" << endl;
        exit(-1);
    }
    const char *p = s.c_str();  
    write(socket_fd,p, s.size());
    close(socket_fd);
}

2.2 創建ROS節點:detection_server

本節點作爲一個過渡節點,一方面作爲上述TCP通信的服務端,實時接收客戶端發送過來的流數據,然後讀取並解析;另一方面作爲話題發佈器,將解析到的位置信息和類型信息通過話題的形式發佈出去。程序如下:

/*used for deepstream*/
#include <ros/ros.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <waste_clasify/UarmAction.h>
#include <waste_clasify/Uarm.h>
#include <iostream>
/*注意使用eigen的時候路徑在/usr/include/eigen3下
 也可執行如下指令:
 cp -rf /usr/include/eigen3/Eigen /usr/include/Eigen -R
*/
#include <eigen3/Eigen/Dense>

using namespace std;
using namespace Eigen;

#define BUF_SIZE 255

struct Pose_3D
{
 float x;
 float y;
 float z;
} pose_3d;

Pose_3D position_to_3D(int U, int V)
{
 // //單目相機標定結果,f及光心座標c
 Matrix<float, 3, 3> Nei_Can;
 Nei_Can << 386.157194, 0.000000, 159.067975, 0.000000, 388.063524, 129.470492, 0, 0, 1;
 // //手眼標定結果,旋轉及平移矩陣
 Matrix<float, 4, 4> Wai_Can;
 Wai_Can << -47.2253736, 808.943874, -934.623712, 486.125510, 896.251615, 28.4136536, -178.435138, 36.1651555, -3.05148887, 5.81405247, -1133.56463, 279.001094, 0, 0, 0, 1;

 float Z = 0.1625;

 //像素座標系座標
 Matrix<float, 3, 1> Pos_XS;
 Pos_XS << 0,0,0;  //初始化爲0
 Pos_XS << Z*U,Z*V,Z*1;

 //計算相機座標系的三維座標:
 Matrix<float, 3, 1> Pos_TX3D;//在圖像座標系下座標
 Pos_TX3D << 0,0,0;//初始化爲0
 Pos_TX3D = Nei_Can.inverse() * Pos_XS;


 //計算在機械臂基座標系座標:
 Matrix<float, 4, 1> Pos_TX3D_QC;//在圖像座標系下座標,本行是爲了轉爲齊次形式,便於矩陣相乘
 Pos_TX3D_QC << 0,0,0,0;//初始化爲0
 Pos_TX3D_QC << Pos_TX3D(0,0),Pos_TX3D(1,0),0.214,1;

 Matrix<float, 4, 1> Pos_World;
 Pos_World << 0,0,0,0;//初始化爲0
 Pos_World= Wai_Can*Pos_TX3D_QC;//在機器人基座標系下座標
 Pose_3D pos_world_3d;
 pos_world_3d.x = Pos_World(0, 0);
 pos_world_3d.y = Pos_World(1, 0);
 pos_world_3d.z = 30;
 return pos_world_3d;
}

int string_to_int(string s)
{
 stringstream ss;
 ss << s;

 int i;
 ss >> i;
 return i;
}

int outof0(string str)
{
 int i = 0;
 while (str[i] == '0')
 {
     i += 1;
 }
 str.erase(0, i);
 int num = string_to_int(str);
 return num;
}

int main(int argc, char **argv)
{
 ros::init(argc, argv, "deepstream_server");
 ros::NodeHandle n;
 ros::Publisher chatter_pub = n.advertise<waste_clasify::Uarm>("/waste/detection_result", 1000);

 //1.創建一個socket socket() 函數確定了套接字的各種屬性
 int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
 if (socket_fd == -1)
 {
     cout << "socket 創建失敗: " << endl;
     exit(1);
 }
 //2.準備通訊地址(必須是服務器的)192.168.1.49是本機的IP
 struct sockaddr_in addr;
 addr.sin_family = AF_INET;
 addr.sin_port = htons(8888);                       //將一個無符號短整型的主機數值轉換爲網絡字節順序,即大尾順序(big-endian)
 addr.sin_addr.s_addr = inet_addr("192.168.43.78"); //net_addr方法可以轉化字符串,主要用來將一個十進制的數轉化爲二進制的數,用途多於ipv4的IP轉化。
 //3.bind()綁定
 //參數一:0的返回值(socket_fd)
 //參數二:(struct sockaddr*)&addr 前面結構體,即地址
 //參數三: addr結構體的長度
 //通過 bind() 函數將套接字 serv_sock 與特定的 IP 地址和端口綁定,IP 地址和端口都保存在 sockaddr_in 結構體中
 int res = bind(socket_fd, (struct sockaddr *)&addr, sizeof(addr));
 if (res == -1)
 {
     cout << "bind創建失敗: " << endl;
     exit(-1);
 }
 cout << "bind ok 等待客戶端的連接" << endl;

 //4.監聽客戶端listen()函數,讓套接字處於被動監聽狀態。所謂被動監聽,是指套接字一直處於“睡眠”中,直到客戶端發起請求才會被“喚醒”
 //參數二:進程上限,一般小於30
 listen(socket_fd, 30);
 //5.等待客戶端的連接accept(),返回用於交互的socket描述符
 struct sockaddr_in client;
 socklen_t len = sizeof(client);
 while (1)
 {
     //accept() 函數用來接收客戶端的請求。程序一旦執行到 accept() 就會被阻塞(暫停運行),直到客戶端發起請求。
     int fd = accept(socket_fd, (struct sockaddr *)&client, &len);
     if (fd == -1)
     {
         cout << "accept錯誤\n"
              << endl;
         exit(-1);
     }
     //6.使用第5步返回socket描述符,進行讀寫通信。
     char *ip = inet_ntoa(client.sin_addr);
     cout << "客戶: 【" << ip << "】連接成功" << endl;

     write(fd, "welcome", 7); //必須給客戶端返回一些數據,否則客戶端會認爲沒有完成任務發送,客戶端會阻塞。

     char buffer[BUF_SIZE] = {};
     int size = read(fd, buffer, sizeof(buffer)); //通過fd與客戶端聯繫在一起,返回接收到的字節數
     //第一個參數:accept 返回的文件描述符
     //第二個參數:存放讀取的內容
     //第三個參數:內容的大小

     cout << "接收到字節數爲: " << size << endl;
     cout << "內容: " << buffer << endl;
     string result(buffer);
     waste_clasify::Uarm result_pub;

     int number_detection = outof0(result.substr(2, 4));
     vector<int> possible_indexes;
     vector<float> possible_locations;
     if (number_detection)
     {
         result_pub.data = number_detection;
         string flag = "BB";
         int position = 0;
         int i = 1;
         while ((position = result.find(flag, position)) != string::npos)
         {
             possible_indexes.push_back(position);
             position++;
         }
         cout << possible_indexes.size() << endl;
         for (int i = 0; i < possible_indexes.size(); i++)
         {
             possible_locations.push_back((float)(outof0(result.substr(possible_indexes[i] + 2, 4))));
             //根據返回的矩形框計算質心位置
             int X0 = (int)(outof0(result.substr(possible_indexes[i] + 6, 4)) + outof0(result.substr(possible_indexes[i] + 14, 4)) / 2);
             int Y0 = (int)(outof0(result.substr(possible_indexes[i] + 10, 4)) + outof0(result.substr(possible_indexes[i] + 18, 4)) / 2);
             //計算三維位置
             Pose_3D Pos_Grasp = position_to_3D(X0, Y0);
             possible_locations.push_back(Pos_Grasp.x);
             possible_locations.push_back(Pos_Grasp.y);
             possible_locations.push_back(Pos_Grasp.z);
         }
         result_pub.pose = possible_locations;
         chatter_pub.publish(result_pub);
     }
     //7.關閉sockfd
     close(fd);
     memset(buffer, 0, BUF_SIZE);
 }
 close(socket_fd);
 return 0;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章