RoboMaster 2017 HandOn Vision Framework

RoboMaster 2017 HandOn Vision Framework

HandOn系统是基于RoboMasters 2017赛季神符识别的需求而搭建的。采用目标检测领域迭代的 YOLO v2 算法:YOLO9000: Better, Faster, Stronger进行有针对性的手写字符集和七段码字符集的训练和检验。

针对RoboMasters 2017 神符识别训练

请确保高性能主机安装好Opencv和cuda,最好推荐安装成功cudnn以此来加速训练,安装详情请见博主另一篇博文:Ubuntu16.04 配置 CUDA8.0 + cudnn5.1 + Opencv2.4.13
主机配置:
CPU:Intel i7-6700K
GPU:Nvidia GTX 960

1. 在系统文件夹的../cfg目录下新建 rm.data 文件,并添加以下内容:

classes = 18
train = /home/xinhumei/Desktop/HandOn/scripts/rm_train.txt
valid = /home/xinhumei/Desktop/HandOn/rm_test.txt
names = data/rm.names
backup = backup

备注:
classes为训练的目标种类数
train为存放训练集图片的各个完整路径总文件rm_train.txt路径
valid为存放测试集图片的各个完整路径总文件rm_test.txt路径
names为存放训练的目标种类名文件路径
backup为存放训练出来的权重存放路径

2. 在系统文件夹的../cfg目录下复制 tiny-yolo-voc.cfg 并粘贴更名为 rm-voc-tiny-yolo.cfg ,并更新以下内容 ;

(1)配置文件前端。其中更新subdivisions为16,最大迭代次数为120000

[net]
batch=64
subdivisions=16
width=416
height=416
channels=3
momentum=0.9
decay=0.0005
angle=0
saturation = 1.5
exposure = 1.5
hue=.1

learning_rate=0.001
max_batches = 120000
policy=steps
steps=-1,100,20000,30000
scales=.1,10,.1,.1

(2)配置文件后端。其中更新种类个数classes为18,然后更新filters = (bias_match + classes + coords) * num = 115

[convolutional]
size=1
stride=1
pad=1
filters=115
activation=linear

[region]
anchors = 1.08,1.19,  3.42,4.41,  6.63,11.38,  9.42,5.11,  16.62,10.52
bias_match=1
classes=18
coords=4
num=5
softmax=1
jitter=.2
rescore=1

object_scale=5
noobject_scale=1
class_scale=1
coord_scale=1

absolute=1
thresh = .6
random=1

3. 在系统文件夹的../data目录下新建 rm.names 文件,存入训练的种类名。并添加以下内容:

num1
num2
num3
num4
num5
num6
num7
num8
num9
1
2
3
4
5
6
7
8
9

4. 使用 Yolo_mark 进行YOLO v2训练集的制作。

(1) 下载 Yolo_mark: GUI for marking bounded boxes of objects in images for training Yolo v2 开源程序源码

(2) 确保已经安装好Opencv后进行linux环境下的编译,如果出现make编译错误如下:

no known conversion from 'std::vector' (aka 'vector<basic_string<char, char_traits, allocator > >') to 'std::vector &' for 2nd argument

则在当前目录下的main.cpp文件第120行中的代码行:

std::vector filenames_in_folder;
glob(images_path, filenames_in_folder); // void glob(String pattern, std::vector& result, bool recursive = false);

更改为以下内容:

std::vector filenames_in_folder;
//glob(images_path, filenames_in_folder); // void glob(String pattern, std::vector& result, bool recursive = false);
std::vector filenames_in_folder_cv;
glob(imges_path, filenames_in_folder_cv); // void glob(String pattern, std::vector& result, bool recursive = false);
	for (auto &i : filenames_in_folder_cv) 
		filenames_in_folder.push_back(i);

(4) 除了官方视频图片外,构建自己的MNIST手写字符识别九宫格图片。

(4) 将训练集图片放入到../x64/Release/data/img文件夹内。对应的*.txt文件都会在Yolo_mark运行后浏览或操作对应的照片后自动生成。

(5) 在Yolo_mark文件夹中终端进入输入./linux_mark.sh启动标记程序。训练集一共1043张照片,其中180张包括LED显示照片。对全部的图片进行手写字符num1-num9以及LED七段码1-9的手动标记。

5. 存放训练集的路径文件和训练集图片以及每张图片对应的选框文件。

(1)将Yolo_mark/x64/Release/data/img里面的全部*.jpg文件和*.txt文件存入HandOn/rm文件夹里面(rm文件夹为新建的存放图片文件夹)
(2)将Yolo_mark/x64/Release/data/里面train.txt 存入HandOn/scripts文件夹,并通过查找替换功能,将其中全部的路径” /x64/Release/data/img “改成绝对路径” /home/xinhumei/Desktop/HandOn/rm”,同时将文件名从train.txt更名为rm_train.txt

6. 修改../src/yolo程序文件中相关代码和参数

(1)全局变量声明:

char *voc_names[] = {"num0" , "num1" , "num2" , "num3" , "num4" , "num5" , "num6" , "num7" , "num8" , "num9" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" };

(2)train_yolo函数更新:

char *train_images = "/home/xinhumei/Desktop/darknet/train.txt";
char *backup_directory = "/home/xinhumei/Desktop/darknet/backup/";

7. 下载预训练模型 darknet19_448.conv.23

8. 修改Makefile文件为以下内容:(基于已经安装好CUDA 8.0 和cudnn 5.1 以及Opencv 2.4.13的主机环境下)

GPU=1
CUDNN=1
OPENCV=1
DEBUG=0

ARCH= -gencode arch=compute_20,code=[sm_20,sm_21] \
-gencode arch=compute_30,code=sm_30 \
-gencode arch=compute_35,code=sm_35 \
-gencode arch=compute_50,code=[sm_50,compute_50] \
-gencode arch=compute_52,code=[sm_52,compute_52]

# This is what I use, uncomment if you know your arch and want to specify
# ARCH= -gencode arch=compute_52,code=compute_52

VPATH=./src/
EXEC=darknet
OBJDIR=./obj/

CC=gcc
NVCC=/usr/local/cuda-8.0/bin/nvcc
OPTS=-Ofast
LDFLAGS= -lm -pthread
COMMON=
CFLAGS=-Wall -Wfatal-errors

9. 重新编译HandOn系统并进行训练

sudo make clean
sudo make -j4
./darknet detector train cfg/rm.data cfg/rm-voc-yolo-tiny.cfg darknet19_448.conv.23

10. 训练过程中可以通过htop指令和nvidia-smi指令分别观察CPU和GPU占用情况

(1)htop指令使用

sudo apt-get install htop
htop

(2)nvidia-smi指令使用,其中1代表1s刷新状态

watch -n 1 nvidia-smi

针对RoboMasters 2017 神符检测

1. 保存好训练结果权重文件rm-voc-yolo-tiny_final.weights,此为训练好的权重模型。若想观察检测效果直接运行以下指令即可。无需进行后续操作

./darknet detector demo cfg/rm-voc-yolo-tiny.cfg rm-voc-yolo-tiny_final.weights

2. 修改../src/image.h程序文件中相关代码和参数:

(1)全局变量声明和串口通信头文件添加:

#include 
#include 
#include  /* File control definitions */
#include 
#include  /* POSIX terminal control definitions */

typedef struct {
	int fd;
	int isFrictionwheelWork; 
}Comm;

(2)更新draw_detections函数声明

void draw_detections(Comm com, image im, int num, float thresh, box *boxes, float **probs, char **names, image **labels, int classes);

3. 由于draw_detections函数声明的更新,全部调用该函数的相关代码均需要更新,避免编译错误和以后运行相关算法检测出现错误

(1)修改../src/yolo.c程序文件中相关代码和参数:

添加串口全局变量申明和image.h头文件引用

#include "image.h"
Comm RM;

在test_yolo函数中更新draw_detections函数引用

draw_detections(RM, im, l.side*l.side*l.n, thresh, boxes, probs, voc_names, alphabet, 18);

在run_yolo函数中更改摄像头的引用(此步骤针对Nvidia Jetson TX1有效)

int cam_index = find_int_arg(argc, argv, "-c", 1);

(2)修改../src/detector.c程序文件中相关代码和参数:

添加串口全局变量引用和image.h头文件引用

#include "image.h"
extern Comm RM;

在test_detector函数中更新draw_detections函数引用

draw_detections(RM, im, l.w*l.h*l.n, thresh, boxes, probs, names, alphabet, l.classes);

(3)修改../src/coco.c程序文件中相关代码和参数:

添加串口全局变量引用和image.h头文件引用

#include "image.h"
extern Comm RM;

在test_coco函数中更新draw_detections函数引用

draw_detections(RM, im, l.side*l.side*l.n, thresh, boxes, probs, coco_classes, alphabet, 80);

4. 修改../src/demo.c程序文件中相关代码和参数:

(1)串口全局变量引用:

extern Comm RM;

(2)添加串口打开函数(详情见2016年的博文:RoboMasters 的串口发送函数

int openCCOMPort(){
	int fd; /* File descriptor for the port */
	fd = open("/dev/ttyTHS2", O_RDWR | O_NOCTTY | O_NDELAY);
	if (fd == -1){
	     perror("open_port: Unable to open /dev/ttyTHS2.");
 	     return 5;
	}else if(fd == 0){          
             return 10;
        }else{
	     fcntl(fd, F_SETFL, 0);
	     return (fd);
	}     
 }

(3)添加串口设置函数(详情见2016年的博文:RoboMasters 的串口发送函数

int setCCOMPort(int fd, int baudRate, int dataBits, char parity, int stopBits){
	struct termios newConfig, oldConfig;
	int speed;

	if(tcgetattr(fd, &oldConfig) != 0){
		perror("tcgetattr");
		return 0;
	}
	newConfig = oldConfig;

	cfmakeraw(&newConfig);

	newConfig.c_cflag &= ~CSIZE;


	switch(dataBits){
		case 7:
			newConfig.c_cflag |= CS7;
			break;
		default:
		case 8:
			 newConfig.c_cflag |= CS8;
			break;
	}

	switch(baudRate){
		case 2400:
			speed = B2400;
			break;
		case 4800:
			speed = B4800;
			break;
		case 9600:
			speed = B9600;
			break;
		case 19200:
			speed = B19200;
			break;
		case 38400:
			speed = B38400;
			break;
		default:
		case 115200:
			speed = B115200;
			break;
	}
	cfsetispeed(&newConfig, speed);
	cfsetospeed(&newConfig, speed);

	newConfig.c_cflag |= (CLOCAL | CREAD);

	switch(parity){
		default:
		case 'n':
		case 'N':
			 newConfig.c_cflag &= ~PARENB;
			break;
		case 'o':
		case 'O':
			 newConfig.c_cflag |= (PARODD | PARENB);
			break;
		case 'e':
		case 'E':
			 newConfig.c_cflag |= PARENB;
			 newConfig.c_cflag &= ~PARODD;
			break;
		case 's':
		case 'S':
			 newConfig.c_cflag &= ~PARENB;
		break;
	}

	switch (stopBits){
		default:
		case 1:
			 newConfig.c_cflag &= ~CSTOPB;
			break;
		case 2:
			 newConfig.c_cflag |= CSTOPB;
			break;
	}

	if(tcsetattr(fd, TCSANOW, &newConfig) != 0){
		perror("tcsetattr");
		return 0;
	}
	return 1;
}

(4)添加串口初始化函数(详情见2016年的博文:RoboMasters 的串口发送函数

void PortInit(){
	RM.fd = openCCOMPort();
	setCCOMPort(RM.fd, 115200, 8, 'N', 1);
	RM.isFrictionwheelWork = 0;
}

(5)更新detect_in_thread函数中的draw_detections函数引用

draw_detections(RM ,det, l.w*l.h*l.n, demo_thresh, boxes, probs, demo_names, demo_alphabet, demo_classes);

(6)在demo函数添加对串口初始化函数的引用

PortInit();

5. 更新../src/image.c程序文件中相关代码和参数:

(1)LED灯结构体RM_LED[10]声明和九宫格结构体RM_NINE_TAG[18]声明添加,结构体数量为实际需求数量的2倍,是以便存入多识别的错误信息避免结构体数组溢出:

typedef struct _RM_LED_{
	int led_num;
	float led_index;
}RMLED;

RMLED RM_LED[10] , RM_LED_TEMP ;

typedef struct _RM_NINE_TAG_{
	int tag_num;
	float tag_index_x;
	float tag_index_y;
	float prob;
}RMTAG;

RMTAG RM_NINE_TAG[18] , RM_NINE_TAG_TEMP ;

(2)添加串口发送函数(详情见2016年的博文:RoboMasters 的串口发送函数

int sendMsg(Comm com ,char *msg)
{
	int len = 0;
	for(;msg[len]!='\x80';len++)
	{}
	len+=1;
	int n = write(com.fd, msg, len);
	if(n>0)
		return 1;
	else
		return 0;
}

(3)添加LED信息和九宫格信息串口发送函数

int sendTag(Comm com, int FLAG , int led[5], int num[5] , int ERROR)
{
	char msg[18] = "\xff\x00\xfa\x00\x00\x00\x00\x00\xfb\x00\x00\x00\x00\x00\xfc\x00\x80";
	msg[1] = (char)(FLAG + (int)msg[1]);
	msg[15] = (char)(ERROR + (int)msg[15]);
	if (FLAG){
		for (int i = 0; i < 5; ++i){
			msg[ 3 + i ] = (char)(led[i] + (int)msg[ 3 + i ]);
			msg[ 9 + i ] = (char)(num[i] + (int)msg[ 9 + i ]);
		}
	}
	if(!sendMsg(com,msg)){
                printf("SENDTOPORT ERROR");
		return 0;
        } 
	return 1;
}

(4)更新draw_detections函数中相关代码和参数:

修改draw_detections的函数传入变量如下:

void draw_detections(Comm com, image im, int num, float thresh, box *boxes, float **probs, char **names, image **alphabet, int classes)

初始化LED和九宫格的相关数组和标志位

int LEN_LED = 0; //LED结构体存入长度
int LEN_TAG = 0; //九宫格结构体存入长度
	
int FLAG = 1;    //检测有效与否标志位
int ERROR = 0;   //检测无效错误标志码

int LED[5] = {0, 0, 0, 0, 0}; //LED数字信息数组
int TAG[5] = {0, 0, 0, 0, 0}; //九宫格打击位置数字信息数组
int TAG_TMP[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; //九宫格对应位置显示数字信息数组
int TAG_IDX[9] = {0, 1, 2, 3, 4, 5, 6, 7, 8}; //九宫格显示数字对应位置信息数组

在条件判断语句if(prob > thresh)内末端加入以下数组更新语句:

if(LEN_LED > 5){
       FLAG = 0;
       LEN_LED = 5;
       ERROR = 2;
}

if(LEN_TAG > 9){
       FLAG = 0;
       LEN_TAG = 9;
       ERROR = 3;
}
			
if (FLAG){
	if(class > 8){
	          LED[ LEN_LED++ ] = class - 8;			        
	}else{
                  TAG_TMP[ LEN_TAG ] = class + 1;
	          RM_NINE_TAG[ LEN_TAG ].tag_num = class + 1;
	          RM_NINE_TAG[ LEN_TAG ].tag_index_x = b.x;
	          RM_NINE_TAG[ LEN_TAG ].tag_index_y = b.y;
	          RM_NINE_TAG[ LEN_TAG++ ].prob = prob*100;
	} 
} 

判断检测结果是否有效:

if ( (LEN_LED != 5) || (LEN_TAG != 9) ){
    	FLAG = 0;
    	ERROR = 1;
}

如果检测结果有效,再次检查九宫格是否会存在一对相同数字检测的错误情况。如果存在此情况,根据检测出来的置信度进行比较,将置信度较低的识别数字替代成1-9之间目前缺少的那个数字。如果九宫格中存在两队及以上相同数字检测或者某同一个数字在九宫格中检测出三次以上的情况话,将检测结果再次置为无效。在排除这种情况之后,提取出九宫格在当前LED显示数字信息下的打击位置并更新对应数组:

if (FLAG){

        for (int i = 0; i < LEN_TAG; ++i){
            for (int j = 0; j < LEN_TAG - i - 1; ++j){
                if ( TAG_TMP[ j ] < TAG_TMP[ j + 1 ] ){
                    int temp = TAG_TMP[ j ];
                    int index = TAG_IDX[ j ];
                    TAG_TMP[ j ] = TAG_TMP[ j + 1 ];
                    TAG_IDX[ j ] = TAG_IDX[ j + 1 ];
                    TAG_TMP[ j + 1 ] = temp; 
                    TAG_IDX[ j + 1 ] = index;
                }
            }
        }

        int SAME_TAG = 0;
        int SAME_ONE_INDEX = 0;

        for (int i = 0; i < LEN_TAG - 1; ++i){ if ( ( TAG_TMP[ i ] == TAG_TMP[ i + 1 ] ) && ( TAG_TMP[ i ] != -1 ) && ( TAG_TMP[ i + 1 ] != -1) ){ SAME_TAG++; if ( RM_NINE_TAG[ TAG_IDX[ i ] ].prob > RM_NINE_TAG[ TAG_IDX[ i + 1 ] ].prob ){
                    TAG_TMP[ i + 1 ] = -1;
                    SAME_ONE_INDEX = TAG_IDX[ i + 1 ];
                }else{
                    TAG_TMP[ i ] = -1;
                    SAME_ONE_INDEX = TAG_IDX[ i ];
                }
            }
        }

        if ( SAME_TAG > 1 ){
            FLAG = 0;
            ERROR = 4;
        }else if ( SAME_TAG == 1 ){
            for (int i = 1; i < 10; ++i){
                int CKT = 0;
                for (int j = 0; j < 9; ++j){
                    if (i == TAG_TMP[j]){
                        CKT = 1;
                    }
                }
                if (!CKT){
                    RM_NINE_TAG[ SAME_ONE_INDEX ].tag_num = i;
                    break;
                }
            }
        }

        for (int i = 0; i < LEN_LED; ++i){
            for (int j = 0; j < LEN_TAG; ++j){
                if( LED[ i ] == RM_NINE_TAG[j].tag_num )
                    TAG[ i ] = j + 1;
            }
        }

}

串口发送信息函数调用:

sendTag( com , FLAG , LED , TAG , ERROR );

6. 信息报文结构解读:

发表评论