基于yolov5、fcos的目标识别跟踪系统

​ 几个月前做一个需求,需要做一个关于一类物体的识别和跟踪任务。当时首先考虑到是一类物体、身份的识别,使用单纯的对一个特定物体特征提取的识别并不能帮助我对一类物体进行识别跟踪,因此,我打算使用yolo这种端到端的目标识别算法。后来考虑到我需要部署的平台算力又很有限,同时还要注重实时性,也就是通讯速率的问题,这种情况下对于我实时监测的帧数fps要求很高,还需要部署加速模型。

​ 思来想去,树莓派上能够利用的加速方案即便加速了也不够识别算法的要求(onnx转ncnn等,加速之后大概也在10fps以下,仍然不够,个人建议稳定20fps以上),最后选择地瓜机器人(原地平线x3派),使用板载部署fcos跟踪识别。板载使用双核BPU资源,(AI算力达到5TOPS,比香橙派更小)将后处理等操作从神经元网络中提出来单独放在板上跑,最终能够稳定30fps(如果想跑自己的识别算法,需要通过docker将onnx文件转为bin文件,挂载天工开物toolchain),在部署代码中加入串口通讯等内容将识别数据与下位机通讯,从而达到跟踪的目的。

本项目学习逻辑顺序:首先,我们需要知道在电脑端部署上位机过程及输出过程,因为pc端属于算力相当充足的平台,可以看作理想平台,在此基础上,我们写下位机的逻辑代码,并搭建通讯协议,这时我们可以测试下位机的代码逻辑在理想平台运行下是否正确,也即电脑做上位机,32做下位机;测试完成后,再将上位机移植到算力有限平台,在此过程中,我们只需要解决有关通讯速率的问题就好了。

​ 博客主要内容为:

​ 1.将yolo算法本地运行以及部署至树莓派的过程;

​ 2.使用x3派官方部署的fcos识别作为上位机去与下位机通讯,完成识别并跟踪的需求。

方案设计&引脚分配。

​ 本案例采用单目摄像头识别,通过usb连接地平线X3派。地平线X3派官方已经安装好USB转TTL驱动(若这里使用的是树莓派,则一定要提前看下位机使用usb转ttl通讯的芯片型号安装驱动),通过usb连接STM32F103C8T6驱动板,板载获取信息后发出PWM波信号控制舵机转动。具体接线如下图所示:

​ 其中,由电脑通过typeC对typeC口对地平线X3派供电,地平线和下位机通过usb对typec口通信,并且通过该线对下位机供电。此处供电只对PWM输出口其中一半的引脚进行供电,在测试时,若接入其中未供电的引脚,舵机会发出电流异响。下两图是下位机的原理图以及系统引脚分配:

引脚分配:

需求 需求个数 使用功能 引脚对应 说明
底部旋转舵机 1 使用TIM4定时器功能产生PWM波形(output) PB6(R26) TIM4 PWMgenerationCH1
俯仰舵机 1 使用TIM4定时器功能产生PWM波形(output) PB7(R27) TIM4 PWMgenerationCH2
串口通讯与接收 1 在调试时可以使用电脑usb口直接进行串口通信,或者在打开串口通信设置后,进行引脚引出 PA13/PA14复用功能 通过usb进行通讯,改为串口通信
OLED屏幕显示 1 在调试时返回变换前和后的坐标值 PB5\PB4\PB3\PA15 使用SPI1通讯;对外推挽输出即可
KY-008激光模块 1 数字IO口(舵机占用) PB8(R29) 一边接地,一边直接接入GPIO口即可。
syn6288语音播报模块 1 输出实时合成中文字符 PB10isTX/PB11isRX 串口资源3,目前已经禁用,使用socket协议直接和工控机通讯即可。
DEBUG 1 PA4 LEDBLUE
stlink 4 下载与调试 下4:swclk-PA14 swdio-PA13 3.3-3.3 GND-GND
上位机接线需求 需求个数 使用功能 引脚对应 说明
以太网远程桌面 1 VNCVIEWER RJ45端子接网线 链接树莓派或X3派时,需要指定本地以太网IPV4地址为固定,树莓派请参考CSDN上教程,X3派请进入网络与共享中心-更改适配器设置-IPV4地址-属性-192.168.0.100(参照地平线手册)
通讯 1 usb usb3.0
摄像头 1 usb usb3.0 注意,若是定焦需要记住焦距,若是变焦则需要确定出厂焦距(目前焦距),如果实在记不住也没关系

在计算机本地下载并应用yolo算法。

配置环境。

​ 由于平台部署算力有限,因此选择使用YOLOV5-LITE轻量化版本。由于YOLOV5LITE的1.5版本export的环境与我所搭配的主环境冲突,于是选择1.4版本。以下是我的环境:

1
2
3
4
CUDA版本:12.1
Python=3.11 matplotlib=3.8.3 opencv-python=4.9.0.80 pillow=10.2.0
scipy=1.12.0 torch=2.2.1 torchversion=0.17.1 tensorboard=2.16.2
seaborn=0.13.2 pandas serial(通讯)

​ 使用conda创建虚拟环境命令:

1
2
3
conda create -n env_name python=3.11#创建环境以及python版本
conda env list#显示所有环境或者cd进入conda的evns中去ls
conda activate env_name#激活进入环境中,可以在激活后在powershell中下载并配置环境

本地运行YOLOV5。

​ 前往:https://github.com/ppogg/YOLOv5-Lite,下载源代码.zip至本地。

​ 在release中下载.pt预权重文件,这里使用YOLOV5LITE-S.pt文件,使用权重文件的s版本(最简化)。

​ 以下是对YOLOV5LITE-1.4源码进行一些最基本的处理:

​ 到基础能用的地步,更改所涉及到的文件:detect.py、plot.py、datasets.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
detect.py#
#修改1:修改预训练权重文件的绝对地址------------------------------------------------------------------------------
parser.add_argument('--weights', nargs='+', type=str, default='D:/YOLOv5- Litev1.5/v5lites.pt',help='model.pt path(s)')#此处default修改为自己下载的.pt文件的绝对地址
#修改2:将YOLO从从指定地址读取图片识别模式修改至使用本地摄像头实时检测模式----------------------------------------------
parser.add_argument('--source', type=str, default='0', help='source') #default修改为绝对地址为图片识别,若为0、1等等则为使用该数字端口的摄像头,一般0为本地,1为所接的第一个外部摄像头

datasets.py
#若要调用摄像头,同时还要在utils文件夹中logging文件夹下的datasets需要修改第279行代码,以下是datasets语言文件下代码修改:
if 'youtube.com/' in str(url) or 'youtu.be/' in str(url): # if source is
YouTube video
#str(url)是本地摄像头,从url修改为str(url)
check_requirements(('pafy', 'youtube_dl'))

plot.py
#修改3:输出获取中线点坐标并显示---------------------------------------------------------------------------------
#在plot.py中ctrl+h搜索plot_one_box函数,并将函数修改至如下:
def plot_one_box(x, img, color=None, label=None, line_thickness=3):
# Plots one bounding box on image img
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2)+ 1 # line/font thickness
color = color or [random.randint(0, 255) for _ in range(3)]
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
cv2.circle(img, ((c1[0] + c2[0]) // 2, (c1[1] + c2[1]) // 2), 3, (0, 0,
255), thickness=tl)
# 创建了个中心点坐标变量
Center = (((c2[0] - c1[0]) / 2 + c1[0]), ((c2[1] - c1[1]) / 2 + c1[1]))
cv2.putText(img, str(Center), ((c1[0] + c2[0]) // 2, (c1[1] + c2[1]) //2), 0, 0.8, (0, 0, 255), thickness=4, lineType=cv2.LINE_AA)
print("左上点的坐标为:(" + str(c1[0]) + "," + str(c1[1]) + "),右下点的坐标为(" + str(c2[0]) + "," +str(c2[1]) + ")")
print("中心点的坐标为:(" + str((c2[0] - c1[0]) / 2 + c1[0]) + "," +str((c2[1] - c1[1]) / 2 + c1[1]) + ")")
#打印左上角和右下角的坐标
if label:
tf = max(tl - 1, 1) # font thickness
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255,255], thickness=tf, lineType=cv2.LINE_AA)

​ 此时,在右下角添加pycharm虚拟环境,选择之前创建好的虚拟环境,运行detect.py,此时若出现显示调用摄像头识别框且显示识别框的中心点坐标则说明配置成功。

将文件export。

​ 首先,需要对export文件做出解释:export文件只是一个输出性文件,所输出的相当于只是另一个形式的.pt纯权重文件,因此若要在板载上部署仍需要进行以上基础修改操作:添加onnx权重矩阵地址、修改摄像头端口号及其分辨率、修改plot函数。

​ 一般来说,我们为了识别一类物品,需要自己单独训练一个模型,但是我懒OVO,所以我会直接用官方识别几十种label的预权重模型,并加以修改为只输出识别一类物品(如人)。如果有时间的话,下一次研究的时候我再加上训练过程的记录吧。

​ 言归正传,我们需要输出能够被树莓派和x3派成功使用,则需要我们修改opset版本号:

1
torch.onnx.export(...,opset_version=11,...)#修改为11

​ 直接输出即可。

跟踪思路:下位机处理逻辑与源代码

​ 我们从上位机得到输出的只能是一个人物检测框的中心点坐标,我们需要据此将舵机云台进行调整。

坐标系转换

​ 为了让云台知道自己该如何旋转,我们需要将其特征转换到一个中央对准的坐标系中去,我们所使用的像素画幅为640*480的矩形。即从识别坐标系opencv转换到旋转判定坐标系中。如果该类物体的坐标在1、2象限,那俯仰舵机向上旋转单位角度并继续获取旋转后坐标再进行比对;若该类物体坐标在2、3象限,那么水平舵机则向左旋转单位角度并继续获取旋转后坐标再进行比对。假设我们设定一个瞄准区域为(-25,25),也即当其旋转至此范围内时,判定为已经瞄准,则停止旋转,如下图所示:

​ 但是若旋转角度不变,则会产生一个问题,那就是:如果单位旋转角度过小,那么瞄准所需要的时间则会很长;如果单位旋转角度过大,那么瞄准所需要的时间虽然很短,但是由于中心瞄准区域的大小不宜太大,很容易造成转过了的情况,从而导致在人物中心点处左右摇摆。因此,针对不同的角度差值,使用不同的单位旋转角度,可以在一定程度上模拟位置式pid的效果——即,差的越远转的越快,差的越近转的越慢。实测跟踪效果会更好。

T.I.P.S 不建议对角度进行惯性滤波,会导致跟随太慢的问题。(通信频率远小于100HZ)

P.S.在使用之前,我们应当先对舵机进行标定(放在行程的中心点处)再进行安装。

焦距变换+坐标系转换(未验证)

​ 我们可以看出来,上面那种方法麻烦且定位慢,那么为什么我们不能直接一下子就转到想要的角度呢?原因是我们使用的工业USB摄像头属于单目摄像头,一般来说不用单目摄像头进行测距,没有三维空间第三个坐标的信息,我们很难获取直接的坐标。但是,如果我们了解单目摄像头以及YOLO算法识别坐标的本质,那我们也同样能够获得这个三维空间的Z坐标信息。

​ 在引脚分配部分,我曾提到要记住摄像头的焦距信息,使用单目摄像机的YOLO算法识别时,世界的影像被投影到一个平行于目前摄像头平面的二维平面上,它们之间的距离则是焦距,此时焦距单位按照像素计算。如下图所示:

​ 在平视情况下,我们也可以通过该种计算方法来直接旋转到目标转角。

P.S.按照理论来说这种方法很准,但是我个人在使用的时候,实际部署到上位机的时候确实不怎么准确(应该是我把焦距搞错了),没找到特别具体的原因,欢迎一试。

下位机控制代码

​ 基于单纯的坐标系转换方法,使用STM32CUBEIDE编写和配置下位机。经过配置和引脚分配,配置如下图所示:

​ 总体引脚:

​ 时钟树:

PWM波:

烧录与通讯口:(usart3未使用)

使用最简单方案:坐标转换法

这里只对部分代码进行解析:

1
2
3
4
5
int x1=changebuff[0]*100+changebuff[1]*10+changebuff[2];
int y1=changebuff[3]*100+changebuff[4]*10+changebuff[5];
int x=changebuff[0]*100+changebuff[1]*10+changebuff[2]-320;
int y=-(changebuff[3]*100+changebuff[4]*10+changebuff[5]-240);
//从缓冲区读取并将坐标转换为中心坐标系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int quadrant;//判断坐标系
if(x>0&&y>0)
{
quadrant=1;
}
else if(x<0&&y>0)
{
quadrant=2;
}
else if(x<0&&y<0)
{
quadrant=3;
}
else if(x>0&&y<0)
{
quadrant=4;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//控制左右旋转,确定当x在正负50以内为锁定成功,当其远不在锁定范围内时,移动步长为5,当其小于120时,移动步长为1,精确锁定。
int change_angle=5;
int change_angle1=1;
int range_max=100;
int range_min=50;
if(abs(x)>50)
{
if(abs(x)>120)
{
if(quadrant==1||quadrant==4)
{
orgin1=orgin1-change_angle;
if(orgin1>=180)
{
orgin1=180;
}
if(orgin1<5)
{
orgin1=5;
}
servo_angle_left_right(orgin1);
}
if(quadrant==2||quadrant==3)
{
orgin1=orgin1+change_angle;
if(orgin1>=180)
{
orgin1=180;
}
if(orgin1<5)
{
orgin1=5;
}
servo_angle_left_right(orgin1);
}
if(quadrant==1||quadrant==2)
{
orgin2=orgin2+change_angle; change_angle;
if(orgin2>=180)
{
orgin2=180;
}
if(orgin2<5)
{
orgin2=5;
}
//servo_angle_up_down(orgin2);
}
if(quadrant==3||quadrant==4)
{
orgin2=orgin2-change_angle;
if(orgin2>=180)
{
orgin2=180;
}
if(orgin2<5)
{
orgin2=5;
}
//servo_angle_up_down(orgin2);
}
}
else
{
if(quadrant==1||quadrant==4)
{
orgin1=orgin1-change_angle1;
if(orgin1>=180)
{
orgin1=180;
}
if(orgin1<5)
{
orgin1=5;
}
servo_angle_left_right(orgin1);
}
if(quadrant==2||quadrant==3)
{
orgin1=orgin1+change_angle1;
if(orgin1>=180)
{
orgin1=180;
}
if(orgin1<5)
{
orgin1=5;
}
servo_angle_left_right(orgin1);
}
}

}

在树莓派(地平线X3派)中使用yolov5算法(fcos目标检测算法)。

地平线X3派中已经部署完毕,有机会再补充这部分部署代码(官方代码抄录);

树莓派中部署:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import cv2
import numpy as np
import onnxruntime as ort
import time

def plot_one_box(x, img, color=None, label=None, line_thickness=None):
"""
description: Plots one bounding box on image img,
this function comes from YoLov5 project.
param:
x: a box likes [x1,y1,x2,y2]
img: a opencv image object
color: color to draw rectangle, such as (0,255,0)
label: str
line_thickness: int
return:
no return
"""
tl = (
line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1
) # line/font thickness
color = color or [random.randint(0, 255) for _ in range(3)]
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
if label:
tf = max(tl - 1, 1) # font thickness
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
cv2.putText(
img,
label,
(c1[0], c1[1] - 2),
0,
tl / 3,
[225, 255, 255],
thickness=tf,
lineType=cv2.LINE_AA,
)

def _make_grid( nx, ny):
xv, yv = np.meshgrid(np.arange(ny), np.arange(nx))
return np.stack((xv, yv), 2).reshape((-1, 2)).astype(np.float32)

def cal_outputs(outs,nl,na,model_w,model_h,anchor_grid,stride):

row_ind = 0
grid = [np.zeros(1)] * nl
for i in range(nl):
h, w = int(model_w/ stride[i]), int(model_h / stride[i])
length = int(na * h * w)
if grid[i].shape[2:4] != (h, w):
grid[i] = _make_grid(w, h)

outs[row_ind:row_ind + length, 0:2] = (outs[row_ind:row_ind + length, 0:2] * 2. - 0.5 + np.tile(
grid[i], (na, 1))) * int(stride[i])
outs[row_ind:row_ind + length, 2:4] = (outs[row_ind:row_ind + length, 2:4] * 2) ** 2 * np.repeat(
anchor_grid[i], h * w, axis=0)
row_ind += length
return outs



def post_process_opencv(outputs,model_h,model_w,img_h,img_w,thred_nms,thred_cond):
conf = outputs[:,4].tolist()
c_x = outputs[:,0]/model_w*img_w
c_y = outputs[:,1]/model_h*img_h
w = outputs[:,2]/model_w*img_w
h = outputs[:,3]/model_h*img_h
p_cls = outputs[:,5:]
if len(p_cls.shape)==1:
p_cls = np.expand_dims(p_cls,1)
cls_id = np.argmax(p_cls,axis=1)

p_x1 = np.expand_dims(c_x-w/2,-1)
p_y1 = np.expand_dims(c_y-h/2,-1)
p_x2 = np.expand_dims(c_x+w/2,-1)
p_y2 = np.expand_dims(c_y+h/2,-1)
areas = np.concatenate((p_x1,p_y1,p_x2,p_y2),axis=-1)

areas = areas.tolist()
ids = cv2.dnn.NMSBoxes(areas,conf,thred_cond,thred_nms)
if len(ids)>0:
return np.array(areas)[ids],np.array(conf)[ids],cls_id[ids]
else:
return [],[],[]
def infer_img(img0,net,model_h,model_w,nl,na,stride,anchor_grid,thred_nms=0.4,thred_cond=0.5):
# 图像预处理
img = cv2.resize(img0, [model_w,model_h], interpolation=cv2.INTER_AREA)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32) / 255.0
blob = np.expand_dims(np.transpose(img, (2, 0, 1)), axis=0)

# 模型推理
outs = net.run(None, {net.get_inputs()[0].name: blob})[0].squeeze(axis=0)

# 输出坐标矫正
outs = cal_outputs(outs,nl,na,model_w,model_h,anchor_grid,stride)

# 检测框计算
img_h,img_w,_ = np.shape(img0)
boxes,confs,ids = post_process_opencv(outs,model_h,model_w,img_h,img_w,thred_nms,thred_cond)

return boxes,confs,ids




if __name__ == "__main__":

# 模型加载切换为自己的权重矩阵
model_pb_path = "best.onnx"
so = ort.SessionOptions()
net = ort.InferenceSession(model_pb_path, so)

# 标签字典,切换为官方的字典,并且修改为只检测一类
dic_labels= {0:'drug',
1:'glue',
2:'prime'}

# 模型参数
model_h = 320
model_w = 320
nl = 3
na = 3
stride=[8.,16.,32.]
anchors = [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]]
anchor_grid = np.asarray(anchors, dtype=np.float32).reshape(nl, -1, 2)

video = 0
cap = cv2.VideoCapture(video)
flag_det = False
while True:
success, img0 = cap.read()
if success:

if flag_det:
t1 = time.time()
det_boxes,scores,ids = infer_img(img0,net,model_h,model_w,nl,na,stride,anchor_grid,thred_nms=0.4,thred_cond=0.5)
t2 = time.time()


for box,score,id in zip(det_boxes,scores,ids):
label = '%s:%.2f'%(dic_labels[id],score)

plot_one_box(box.astype(np.int16), img0, color=(255,0,0), label=label, line_thickness=None)

str_FPS = "FPS: %.2f"%(1./(t2-t1))

cv2.putText(img0,str_FPS,(50,50),cv2.FONT_HERSHEY_COMPLEX,1,(0,255,0),3)


cv2.imshow("video",img0)
key=cv2.waitKey(1) & 0xFF
if key == ord('q'):

break
elif key & 0xFF == ord('s'):
flag_det = not flag_det
print(flag_det)

cap.release()

地平线x3派与下位机通讯。

基于以上部署,我们做出以下修改:

首先修改为自己权重矩阵的绝对路径和字典库罗列。

修改为只检测一类:

1
2
3
4
5
6
7
8
9
if label == 'person'
plot_one_box(box.astype(np.int16), img0, color=(255,0,0), label=label, line_thickness=None)

str_FPS = "FPS: %.2f"%(1./(t2-t1))

cv2.putText(img0,str_FPS,(50,50),cv2.FONT_HERSHEY_COMPLEX,1,(0,255,0),3)


cv2.imshow("video",img0)

[warning]此种方法依然检测了多种类,只是只显示和输出了一类结果,实时性有待提高,还是应该自己训练为最佳。

若实现通讯,上位机应该在plot中加入修改以下内容(以YOLO为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#修改plots.py中plot_one_box内容
def plot_one_box(x, img, color=None, label=None, line_thickness=3):
# Plots one bounding box on image img
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
color = color or [random.randint(0, 255) for _ in range(3)]
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
cv2.circle(img, ((c1[0] + c2[0]) // 2, (c1[1] + c2[1]) // 2), 3, (0, 0, 255), thickness=tl)
# 创建了个中心点坐标变量
Center = (((c2[0] - c1[0]) / 2 + c1[0]), ((c2[1] - c1[1]) / 2 + c1[1]))
cv2.putText(img, str(Center), ((c1[0] + c2[0]) // 2, (c1[1] + c2[1]) // 2), 0, 0.8, (0, 0, 255), thickness=4, lineType=cv2.LINE_AA)
#print("左上点的坐标为:(" + str(c1[0]) + "," + str(c1[1]) + "),右下点的坐标为(" + str(c2[0]) + "," + str(c2[1]) + ")")
#print(str(int((c2[0] - c1[0]) / 2) + c1[0])+str(int((c2[1] - c1[1]) / 2) + c1[1]))
##ser = serial.Serial('COM6', 115200, timeout=1) # 'COM5'是你的串口号,115200是波特率,timeout是超时时间(单位为秒)
##if ser.is_open:
## print("串口已打开")
if 10<=Center[0]<100:
#print(str(0)+str(int(Center[0])) + str(int(Center[1])))
str2=str(0)+str(int(Center[0])) + str(int(Center[1]))
#encoded_str = str2.encode('utf-8')
## send_data_hex = bytes.fromhex(str2)
## ser.write(send_data_hex)
sleep(0.1)
## data=ser.read_all().hex()
## print(data)

if Center[0]<10:
#print(str(0)+str(0) + str(int(Center[0])) + str(int(Center[1])))
str2 = str(0)+str(0) + str(int(Center[0])) + str(int(Center[1]))
#encoded_str = str2.encode('utf-8')
send_data_hex = bytes.fromhex(str2)
## ser.write(send_data_hex)
sleep(0.1)
## data = ser.read_all().hex()
## print(data)

if 10<=Center[1]<100:
#print(str(int(Center[0])) + str(0)+str(int(Center[1])))
str2 = str(int(Center[0])) + str(0)+str(int(Center[1]))
#encoded_str = str2.encode('utf-8')
send_data_hex = bytes.fromhex(str2)
## ser.write(send_data_hex)
sleep(0.1)
## data = ser.read_all().hex()
## print(data)

if Center[1]<10:
#print(str(int(Center[0])) + str(0)+str(0)+str(int(Center[1])))
str2 = str(int(Center[0])) + str(0)+str(0)+str(int(Center[1]))
#encoded_str = str2.encode('utf-8')
## send_data_hex = bytes.fromhex(str2)
## ser.write(send_data_hex)
sleep(0.1)
## data = ser.read_all().hex()
## print(data)

if Center[0]>100 and Center[1]>100:
print(str(int(Center[0])) + str(int(Center[1])))
str2 = str(int(Center[0])) + str(int(Center[1]))
#print(str2)
#encoded_str = str2.encode('utf-8')
## send_data_hex = bytes.fromhex(str2)
## ser.write(send_data_hex)
sleep(0.1)
## data = ser.read_all()
# print(data)
## data1 = data.hex()
## print(data1)

if label:
tf = max(tl - 1, 1) # font thickness
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)

因为是使用十六进制传递坐标信息,因此下位机应该将所传输的十六进制ASCII码值解码,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HAL_UART_Receive(&huart3,buff,3,HAL_MAX_DELAY);
HAL_UART_Transmit(&huart3,buff,3,HAL_MAX_DELAY);
intbuff[0]=buff[0];
intbuff[1]=buff[1];
intbuff[2]=buff[2];
intbuff[0]=floor(intbuff[0]/16)*10+intbuff[0]%16;
intbuff[1]=floor(intbuff[1]/16)*10+intbuff[1]%16;
intbuff[2]=floor(intbuff[2]/16)*10+intbuff[2]%16;
changebuff[0]=floor(intbuff[0]/10);
changebuff[1]=intbuff[0]-changebuff[0]*10;
changebuff[2]=floor(intbuff[1]/10);
changebuff[3]=intbuff[1]-changebuff[2]*10;
changebuff[4]=floor(intbuff[2]/10);
changebuff[5]=intbuff[2]-changebuff[4]*10;

其它

训练过程:遥遥无期ing

抄录fcos部署源代码:遥遥无期ing

下位机使用CUBEIDE配置,全部代码已经开源至git仓库:https://github.com/SAINT784167/YOLO-LOW-CONTORL