背景:deepstream检测到的结果:框位置和目标类型、概率值需要进一步传递到ros节点中分析使用,本例是采用TCP/IP通信协议将这些数据发送出去

来源:使用darknet框架,利用yolov3-tiny模型在nvidia jetson nano上进行目标检测推理的时候,帧率较低,约6ps,不能满足实际任务需求。庆幸的是Nvidia提供了很多加速工具,典型的如tensorRT和deepstream。

文章目录

  • 1.deepstream使用过程
    • 1.1 安装
      • 1.1.1 安装依赖:
      • 1.1.2 安装librdkafka
      • 1.1.3 安装DeepStream SDK
    • 1.2 例子-加速官方模型进行推理
        • 1.2.1.2 config_infer_primary_yoloV3_tiny.txt文件
      • 1.2.2 加速yolov3并推理:
    • 1.3 实践-加速自己的模型完成推理
      • 1.3.1 准备必要的文件
      • 1.3.2 修改deepstream_app_config_yoloV3_tiny.txt文件
        • 1.3.2.1 修改图像来源
        • 1.3.2.2 修改图像大小
      • 1.3.3 修改config_infer_primary_yoloV3_tiny.txt文件
        • 1.3.3.1 指定模型配置文件
        • 1.3.3.2 指定模型文件
        • 1.3.3.3 指定图片集
        • 1.3.3.4 修改类型个数
  • 2.与ROS进行集成
    • 2.1 修改程序:nvdsinfer_custom_impl_Yolo.cpp
      • 2.1.1 添加头文件
      • 2.1.2 函数decodeYoloV3Tensor()函数
      • 2.1.3 位置3:添加自定义函数int_to_string()
      • 2.1.4 位置4:添加自定义函数socket_write()
    • 2.2 创建ROS节点:detection_server

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 255struct Pose_3D
{float x;float y;float z;
} pose_3d;Pose_3D position_to_3D(int U, int V)
{// //单目相机标定结果,f及光心坐标cMatrix<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;  //初始化为0Pos_XS << Z*U,Z*V,Z*1;//计算相机坐标系的三维坐标:Matrix<float, 3, 1> Pos_TX3D;//在图像坐标系下坐标Pos_TX3D << 0,0,0;//初始化为0Pos_TX3D = Nei_Can.inverse() * Pos_XS;//计算在机械臂基坐标系坐标:Matrix<float, 4, 1> Pos_TX3D_QC;//在图像坐标系下坐标,本行是为了转为齐次形式,便于矩阵相乘Pos_TX3D_QC << 0,0,0,0;//初始化为0Pos_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;//初始化为0Pos_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是本机的IPstruct 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()函数,让套接字处于被动监听状态。所谓被动监听,是指套接字一直处于“睡眠”中,直到客户端发起请求才会被“唤醒”//参数二:进程上限,一般小于30listen(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.关闭sockfdclose(fd);memset(buffer, 0, BUF_SIZE);}close(socket_fd);return 0;
}

使用deepstream对自己模型进行加速推理以及与ROS通信相关推荐

  1. 量化感知训练实践:实现精度无损的模型压缩和推理加速

    简介:本文以近期流行的YOLOX[8]目标检测模型为例,介绍量化感知训练的原理流程,讨论如何实现精度无损的实践经验,并展示了量化后的模型能够做到精度不低于原始浮点模型,模型压缩4X.推理加速最高2.3 ...

  2. 如何给深度学习加速——模型压缩、推理加速

    深度学习模型往往受到端计算力的限制,无法很好的部署在移动端或无法降低端的计算成本.例如自动驾驶的模型就过于巨大,而且往往是很多模型并行,所以一般会用一些加速的方法来降低推算的计算力要求. 加速方法有多 ...

  3. Intel N100工控机使用核显加速推理yolov5模型

    Intel N100工控机使用核显加速推理yolov5模型 前言 安装openvino环境 核显加速运行yolov5 进一步加速 再进一步量化压榨 前言 今年3月初开始,某平台开始陆续上货基于英特尔A ...

  4. 1、pth转onnx模型、onnx转tensorrt模型、python中使用tensorrt进行加速推理(全网最全,不信你打我)

    本文向所有亲们介绍在python当中配置tensorrt环境.使用tensorrt环境进行推理的教程,主要分为两大部分,第一部分环境配置,第二部分前向推理. 第一部分 环境配置 第一步:检查你的系统类 ...

  5. float32精度_混合精度对模型训练和推理的影响

    单精度/双精度/半精度/混合精度 计算机使用0/1来标识信息,每个0或每个1代表一个bit.信息一般会以下面的三种形式表示: 1 字符串 字符串的最小单元是char,每个char占8个bit,也就是1 ...

  6. 北京 | 免费高效训练及OpenVINO™加速推理深度学习实战,送Intel神经计算棒二代...

    当今人工智能时代,深度学习极大得促进了计算机视觉技术的快速应用和成熟,也是算法工程师们必须掌握的一项技能,然而,不同环境的依赖部署,高算力的需求,海量数据量需求及算法应用高硬件成本也让深度学习陷入了规 ...

  7. yolov3-tiny原始weights模型转onnx模型并进行推理

    时隔不知道多少天,我记起来我还有部分博客没写完(偷懒),所以不能偷懒把它完成!! 这篇博客的主要内容 将yolov3-tiny.weights模型转换到.onnx模型: 使用onnnxruntime- ...

  8. Java / Tensorflow - API 调用 pb 模型使用 GPU 推理

    目录 一.引言 二.Java / Tensorflow 代码配置 1.代码配置 2.Maven 配置 三.环境检测 1.显卡检测 2.显卡监控 四.推理踩坑 1.异常现象 2.异常日志 五.安装 cu ...

  9. onnx标准 onnxRuntime加速推理引擎

    onnx标准 & onnxRuntime加速推理引擎 文章目录 onnx标准 & onnxRuntime加速推理引擎 一.onnx简介 二.pytorch转onnx 三.tf1.0 / ...

最新文章

  1. 有bug!PyTorch在AMD CPU的计算机上卡死了
  2. golang []byte和string相互转换
  3. 用Android自带的signapk.jar + .x509.pem + .pk8签名应用程序
  4. C语言链表的转置算法,c语言编程集 数据结构 顺序表 点链表 数制转换 矩阵转置.doc...
  5. firefox2.0的拖放式搜索怎么不行了?是设置问题吗?
  6. JDK 12:实际中的切换语句/表达式
  7. stopwatch_在Java中衡量执行时间– Spring StopWatch示例
  8. pytorch 入门学习 MSE
  9. OpenCV之响应鼠标(一):利用鼠标获取坐标
  10. C/C++底层实现指定磁盘只读
  11. android软件安全权威指南 pdf_目录公众号内的所有资源软件!
  12. 数据库第四次作业:数据备份与还原
  13. csv文件行数超过软件上限解决方案
  14. 【保姆级教程,100%成功】MAC OS打开ntfs格式U盘
  15. HTML5 CSS3做的一个静态的苹果官网首页
  16. MySQL:连接错误
  17. RuiJi Scraper 分页抽取
  18. 魔兽怀旧服务器位置,《魔兽世界》怀旧服稀有狼位置坐标大全
  19. 腾讯云CPU处理器Intel Ice Lake主频2.7GHz睿频3.3GHz)
  20. 谁为企业数字化转型“保驾护航”?

热门文章

  1. 解剖常见电子元器件,了解其内部结构
  2. 成都奔驰电动折叠后视镜改装电耳 蔚一名车汇
  3. 深度技术 GHOSTXP V6.0 快速装机个人版 (NTFS格式)
  4. RK3368 8.1 HDMI声音调节只有最大和最小两个等级
  5. 【计算机毕业设计】Java基于微信小程序的智慧旅游平台
  6. ARouter 源码分析
  7. linux php 压缩中文乱码,linux下zip文件解压乱码问题的解决办法分享
  8. ET7.0 AssetBundle
  9. Haskll Lesson:Huffman编码实现文本压缩
  10. 前向欧拉法、后向欧拉法简介