2020 Ti杯电赛

差点翻车 吓死我了…

比赛之前的准备:

看到材料表,就开始猜题:

  1. LMT70肯定是检测人体体温的,ADS1292检测心电的,那这俩肯定是一起用的。因为LMT70 Datasheet的Demo是MSP430,所以这一题可能要求用MSP430。
  2. 去年小车是强制MSP的,今年也不排除。但是小车可以自转,故不需要把二维云台放到小车上,并且小车距人头太远,无法红外测温。电赛模拟题是云台追踪小车。这里想干什么不清楚。但是可能用MSP430。
  3. 云台可能和摄像头绑定,可以做人脸识别+体温检测。
  4. 摄像头大概率是和红外绑定的,红外大概率和心电不是一组,但是不排除可能在一组(25%的概率吧)。
  5. 飞行器…控制方向吧…每年都差不多。但是MSP也有飞行器的例程。但是我没买,懒得管。
  6. 模电题一概不会。

首先摄像头上K210,因为这货跑AI,效果很好。
紧接着云台到货,准备调试我比较中意但是没有测试过的新算法: 模糊PID
然后发现这个云台配的360度舵机居然是 脉冲时长控制角度 而不是 脉冲时长控制速度
于是抓紧时间换舵机,调通了PID,云台自稳定和人脸追踪。
PID就随便调调问题不大。

PID自稳定:

人脸追踪:

小车原本想着如果能用STM32的话就做这题,顺便把基础控制和电路板焊了。

比赛前一天晚上:

题目前一天晚上就已经泄漏了 然后我看了下有我之前猜中的人脸识别+测温。

  • A题: 无线运动传感器节点设计 心电温度检测+物联网。不难。没买材料,懒得做。
  • B题: 单相在线式不间断电源 电源题 不会。
  • C题: 坡道行驶电动小车 巡线小车,强制要求用MSP430/MSP432。
  • D题: 绕障飞行器 控制四轴绕杆飞行,没有四轴 做不了。
  • E题: 放大器非线性失真研究装置 模电题 没学过 不会。
  • F题: 简易无接触温度测量与身份识别装置 也就是人脸识别+口罩检测+红外温度报警。
  • G题: 非接触物体尺寸形态测量 用图像处理技术来识别正方形 圆形 三角形, 足球排球篮球,并用图像技术测量体积。

然后猜的还算准吧…

人脸追踪,测温,识别,云台我也调好了 但是小车还没调。

然后看了一眼F题 这不正好和我猜的一样吗。
然后20分钟把口罩识别和人脸识别搞定了 这个时候还没到24点。

于是我就开始准备划水 以及不断的把系统复杂化加入更多的功能想要得到更高的分。

比赛第一天:

第一天上午我就慢悠悠的去把两个模型的demo合成一个。
两个demo 还有整个程序流程其实很简单,但是很快就出现了问题。
因为K210的RAM只有8MB,仅仅在

1
2
3
4
task_fd = kpu.load("/sd/fd.smodel") # 从SD卡加载人脸检测模型
task_ld = kpu.load("/sd/kp.smodel") # 从SD卡加载人脸五点关键点检测模型
task_fe = kpu.load("/sd/fe.smodel") # 从SD卡加载人脸196维特征值模型
task_mk = kpu.load("/sd/MaskDetect.kmodel") # 从SD卡加载口罩检测模型

之后 就报错Out Of Memory。内存不够用了。
于是我就想着大不了自己写一个口罩检测的逻辑算法,就不上神经网络模型了。

然后就一脚踩到另外一个坑里了。
因为OpenMV取色块大多数都是用 ROI ,但是K210在加载完人脸识别的网络模型之后RAM就不够用了,所以必须刷精简版的固件才能跑。
而这个固件不支持ROI…
然后也不支持形状提取,不支持一堆东西。。。

然后突然感觉有点慌,因为鬼知道这个固件阉割成什么样了。。。

很快就中午了,我出去吃饭的时候就一直在想后面还有没有潜在的坑或者其他东西。

吃完饭,想了一个口罩识别的方法,提取人脸肤色和嘴巴处的颜色作对比。这里我根据我自己的发型选择了脑门。因为鼻子以下全被覆盖,两眼之间的位置左右有眉毛下面有口罩。外加双眼定位不是很准。
又因为不能使用ROI,提取色块平均值还得自己实现一个方法…
然后下午三四点,写好了。
测试了一下,效果还行,蓝色白色黑色口罩能轻松识别。
然后叫队友戴口罩测试。。。直接翻车。因为他们刘海很长,我取肤色直接取到了头发上…

然后我推倒重做,想其他方法。
这个时候,隔壁用OpenMV的已经把人脸识别(LBP算法)搞定了。

然后下午五点半,我想到了一个比较巧妙的方法,直接取嘴中央的像素点,用红色通道值减去绿色通道值。
然后因为嘴巴偏红,又很少有红色的口罩。
肤色黄偏红,和蓝色想去甚远。
于是我顺带着取了鼻子像素点,计算口罩是否覆盖到了鼻子,可以把 口罩是否佩戴标准 作为加分项。

下午六点写完调试完,很成功。这个时候我们老师也来讲此次的事项了。
因为这次是线上比赛,所以论文很重要。分值好像也从去年的10分提到了20分。

于是又开始了划水,在想怎么增加加分项。
首先,论文里面可以说自己训练了一个口罩模型,加点分。(因为人脸识别太麻烦了)
然后可以做一波语音播报。
然后二维云台人脸追踪整上去。
然后把红外温度传感器校准,多取几组数据做个线性回归。

但是如果自己训练一个网络模型的话,那又回归到 Out Of Memory 的问题了。
于是请教老师是用两个K210来检测论文扔一个AI训练还是用一个K210。
老师倾向于自己训练一个口罩模型,扔论文里。

既然这样,还可以加一个方差分析,逻辑和网络模型都上,再卡尔曼滤波一下。

于是开始了踩坑之旅。
这个时候我还以为电赛是10号早上8点到14号晚上18点。就想着不慌 慢慢搞。
因为是四天三夜嘛 14-10=4,请假也是从10号请到了14号。

比赛第二三天

然后就一直在查有关于TensorFlow的相关内容。很多教程都是TensorFlow 1.x的东西,2.0+还需要额外踩坑。
就算这样,对于我这一个只是听说过Tensorflow但是没有用过的人来说 还是很难的。

各种踩坑,版本问题,以及搞清楚什么是 目标识别 目标分类。然后如何训练什么的。。
以及如何截图放论文里面效果最好。

随手截了几张。





我们的K210开发板Maixpy官方也提供了一个训练入口。

然后我发现一个问题。
我的模型排队总是越排越靠后,最后在列表里面消失不见…
qnmd,不愿意白送算力就别送啊,浪费时间。

另外一个做F题的在第三天早上已经把大多数任务做完开始划水了。这个时候我还在想语音模块用串口还是I2S,语音队列怎么设计。

然后就这样一直踩坑一直浪费时间到了第三天晚上八点。
然后这个时候队友告诉我,明天得拍视频了。
我说 不是14号晚上截止吗?四天三夜?
他说13号
我说: 14-10=4 然后14号就没有晚上了
三秒后,
我是傻逼。

比赛截止前一晚

然后抓紧时间焊板子,开始写STM32部分。

晚上十一点,队友焊完板子,我开始调程序。
这个时候我状态已经不是很好了,学了一天Tensorflow。

然后抓紧时间用之前建好的工程把云台调通。
结果后来发现这个板子比我原来测试的时候沉很多,这个舵机调PID会有问题。


然后抓紧时间换回原来的舵机。
又因为原来的舵机太过于强劲,每次运动都会拉低电压供电不稳,速度过快PID过抖。
于是我就关闭了上级舵机,让他就这样固定着不动。

这个部分原本应该半小时搞定,结果因为状态不行搞到了晚上两点。

然后我开始考虑语音队列的问题:

  1. 如果上条语音提示没有说完,下一条又来了怎么办? 打断还是等说完?
  2. 如果需要打断,怎么做? 如果不打断,怎么做?
  3. 如果把语音播放函数放到中断函数里面调用,会不会引发中断套嵌? 如果放到主循环里,怎么控制队列?
  4. 语音模块究竟用VS1053还是普通串口模块?

然后就这样耽误到了四点。后来就一狠心,删了,不要了,qnmd。

然后又遇到了滤波问题 因为无论是人脸还是口罩,输出值都会偶尔跳动。这个时候已经五点多了,逻辑已经不清晰了。怎么调都感觉我写的没问题。
于是我就出去走了一圈,六点的时候想了个不错的方法,六点半写完了。

这个时候已经只能写一些很简单的不用动脑子的程序了。
然后又因为队友给板子打洞的时候做工不行,根本没用心做,太浮躁生气了一个小时,已经快放弃了。因为状态不行了。
七点半,整理心情,滚回去Debug.

然后发现我K210的程序多注释了一行关键代码,产生了BUG。

八点发现我之前写的滤波程序有BUG,就索性不用滤波了。

八点半调通了人脸报警口罩报警人脸录入。

然后这个时候评分细则下来了,发现我们之前想的全自动展示方式不符合要求,滚回去改流程。

还需要显示被测物体温度。
原本想直接在K210的LCD上显示,STM32把温度数据一行一行的发送过去,K210直接readline就行了。
结果发现K210这人脸识别固件的readline不是一行一行的读的,只要有数据他就读,不管你存够没存够一行。
然后又因为K210主频实在是太高了,STM32F1+C语言还没发完一行K210+MicroPython就读出来了。
然后K210在连续串口发包的时候 STM32在接收时也会因为速度过快漏掉数据。
这个时候我震惊了,原来Python还是有那么一点点的性能的。
后来想了一下,MicroPython也是用C/C++写的,串口读取过程Python也没做太多贡献,大多数还是C/C++完成的。如果Python跑逻辑运算估计还是不太行。

那就赶紧借一个Oled12864吧。
借到之后,完全按照检测流程写出来已经下午一点了。这放平常就是俩仨小时的代码硬是写了一个通宵。
原本四五百行的代码被我写到了一千二。。。

全是垃圾代码….
当时连拆分文件都懒得做了,太累了。。。

然后一点半测试了一下,效果还行,问题不大。
于是赶紧去帮队友写论文…
AI部分没做完,写不了..
原本要上的语音模块被我砍了,流程图得改。。

真正昨晚的加分项就是那一个误差分析线性回归了…

浪费在TensorFlow上的时间太多了。论文也写的一塌糊涂。

录视频的时候也出了一点小意外。
前面我把滤波删了并且没有遇到BUG,是因为实验室的时候光线角度较好,没有太多杂波。
录视频的时候一个劲的翻车…

K210部分代码

(STM32部分是通宵写的 写的很垃圾 就不放出来了)
那三个smodel去官网下吧

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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import os
import sys
import time
import sensor,image,lcd
import KPU as kpu
#import string

from Maix import FPIOA,GPIO
from fpioa_manager import fm
from machine import UART
from micropython import const #加载常量功能

#硬件连接
KEY_PIN = const(15) # 设置录入人脸引脚
TX_PIN = const(9) # TX Pin
RX_PIN = const(10) # RX Pin

#阈值
MASKED_THERD = const(12) # 戴口罩为负 不带为正
#戴口罩录入时:
MASKED_FACE = const(85) # 戴口罩 相似度大于85
UNMASKED_FACE = const(67) #不戴口罩 相似度大于67
#不戴口罩录入时:
MASK_FACE = const(0)
UNMASK_FACE = const(80)

#初始化串口IO
fpioa = FPIOA()
fpioa.set_function(KEY_PIN,FPIOA.GPIO7)
key_gpio=GPIO(GPIO.GPIO7,GPIO.IN)

#初始化串口
fm.register(9,fm.fpioa.UART2_TX)
fm.register(10,fm.fpioa.UART2_RX)
USART = UART(UART.UART2, 115200, 8, None, 1, timeout=1000, read_buf_len=4096)

#载入AI模型
task_facedetect = kpu.load("/sd/fd.smodel")
task_facepoint = kpu.load("/sd/kp.smodel")
task_faceRecognition = kpu.load("/sd/fe.smodel")
USART.write("AI Inited\r\n")
clock = time.clock() # 初始化系统时钟,计算帧率

#全局变量
##用于检测按键的变量
last_key_state = 1
key_pressed = 0 # 初始化按键引脚
#检测口罩
DeltammRG = 0
DeltanoRG = 0
##标记口罩
mask = True
maskstandard = True

# 初始化lcd
lcd.init()
lcd.rotation(0)

#摄像头初始化
sensor.reset()
sensor.set_pixformat(sensor.RGB565) #16位模式
sensor.set_framesize(sensor.QVGA) #320*240
sensor.set_hmirror(0) #设置摄像头镜像
sensor.set_vflip(1) #设置摄像头翻转
sensor.run(1) #开启摄像头

#人脸识别的anchor
anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025) #anchor for face detect 用于人脸检测的Anchor
#标准正脸的五点坐标
dst_point = [(44,59),(84,59),(64,82),(47,105),(81,105)]
#初始化人脸检测模型
a = kpu.init_yolo2(task_facedetect, 0.5, 0.3, 5, anchor)
#设置显示的buf
img_lcd_buff=image.Image() # 设置显示buff
#人脸128*128方块buff
img_face_buff=image.Image(size=(128,128))
a=img_face_buff.pix_to_ai() # 将图片转为kpu接受的格式
#空列表 用于存储当前196维特征
record_ftr=[]
#空列表 用于存储按键记录下人脸特征
record_ftrs=[]
#每个人的name
names = ['Stu1', 'Stu2', 'Stu3', 'Stu4', 'Stu5', 'Stu6', 'Stu7', 'Stu8', 'Stu9' , 'Stu10'] # 人名标签,与上面列表特征值一一对应。

#进入主程序
while(1):
#读入摄像头图片
img = sensor.snapshot()

clock.tick() #用于计算帧率
code = kpu.run_yolo2(task_facedetect, img) #获取人脸所在方框

#检测到人脸:
if code:
MaxArea = 0
#找到最大的脸
for i in code: # 遍历坐标框
Area = (i.rect()[2]*i.rect()[3])
if(Area > MaxArea):
Maxindex = i
MaxArea = Area

i = Maxindex
# Cut face and resize to 128x128
face_cut=img.cut(i.x(),i.y(),i.w(),i.h()) # 裁剪人脸部分图片到 face_cut
face_cut_128=face_cut.resize(128,128) # 将裁出的人脸图片 缩放到128 * 128像素
a=face_cut_128.pix_to_ai() # 将猜出图片转换为kpu接受的格式
#a = img.draw_image(face_cut_128, (0,0))
# Landmark for face 5 points
fmap = kpu.forward(task_facepoint, face_cut_128) # 运行人脸5点关键点检测模型
pointlist = fmap[:] # 获取关键点预测结果
lefteye = (i.x()+int(pointlist[0]*i.w() - 10), i.y()+int(pointlist[1]*i.h())) #左眼坐标
righteye = (i.x()+int(pointlist[2]*i.w()), i.y()+int(pointlist[3]*i.h())) #右眼位置
nose = (i.x()+int(pointlist[4]*i.w()), i.y()+int(pointlist[5]*i.h())) #鼻子坐标
leftmouth = (i.x()+int(pointlist[6]*i.w()), i.y()+int(pointlist[7]*i.h())) #左嘴角坐标
rightmouth = (i.x()+int(pointlist[8]*i.w()), i.y()+int(pointlist[9]*i.h())) #右嘴角坐标
middlemouth = (int((leftmouth[0] + rightmouth[0])/2), int((leftmouth[1] + rightmouth[1])/2)) #嘴中央坐标

#颜色计算
if(middlemouth[0] > 0 and middlemouth[0] < 320 and middlemouth[1] > 0 and middlemouth[1] < 240 and nose[0] > 0 and nose[0] < 320 and nose[1] > 0 and nose[1] < 240):
DeltammRG=img.get_pixel(middlemouth[0],middlemouth[1])[0] - img.get_pixel(middlemouth[0],middlemouth[1])[1]
DeltanoRG=img.get_pixel(nose[0],nose[1])[0] - img.get_pixel(nose[0],nose[1])[1]

#标出人脸特征
a = img.draw_circle(lefteye[0], lefteye[1], 4)
a = img.draw_circle(righteye[0], righteye[1], 4)
a = img.draw_cross(nose[0], nose[1],(255,255,255), 4)
a = img.draw_line(leftmouth[0], leftmouth[1],rightmouth[0], rightmouth[1])

#转换为正脸
src_point = [lefteye, righteye, nose, leftmouth, rightmouth]
T=image.get_affine_transform(src_point, dst_point)
a=image.warp_affine_ai(img, img_face_buff, T)
a=img_face_buff.ai_to_pix()
del(face_cut_128) # 释放裁剪人脸部分图片

#分析特征并对比
fmap = kpu.forward(task_faceRecognition, img_face_buff) #计算正脸196维特征值
feature=kpu.face_encode(fmap[:])
reg_flag = False
scores = [] # 存储特征比对分数
for j in range(len(record_ftrs)): #遍历已存特征值
score = kpu.face_compare(record_ftrs[j], feature) #计算当前人脸特征值与已存特征值的分数
scores.append(score) #添加分数总表
max_score = 0
index = -1
for k in range(len(scores)): #迭代所有比对分数,找到最大分数和索引值
if max_score < scores[k]:
max_score = scores[k]
index = k

#人脸识别
#自己不戴口罩75左右 自己戴口罩85+ 别人戴口罩70+ 别人不戴口罩 60
#戴口罩, 大于85分, 同一个人
if(DeltammRG <= MASKED_THERD and max_score > MASKED_FACE):
a = img.draw_string(i.x(),i.y(), ("%s :%2.1f" % (names[index], max_score)), color=(0,255,0),scale=2)
a = img.draw_rectangle((i.rect()),color=(0,0,255)) # 标出人脸框
#不戴口罩, 大于67.5, 同一个人
elif(DeltammRG > MASKED_THERD and max_score > UNMASKED_FACE):
a = img.draw_string(i.x(),i.y(), ("%s :%2.1f" % (names[index], max_score)), color=(0,255,0),scale=2)
a = img.draw_rectangle((i.rect()),color=(0,0,255)) # 标出人脸框
#其他情况
else:
a = img.draw_string(i.x(),i.y(), ("???"), color=(255,0,0),scale=2)
a = img.draw_rectangle((i.rect()),color=(255,0,0)) # 标出人脸框
index = -1

#口罩逻辑
if(DeltammRG <= MASKED_THERD):
mask = True
else:
mask = False

#口罩标准
if(DeltanoRG <= MASKED_THERD):
maskStandard = True
else:
maskStandard = False

# Debug
# if(index != -1):
# img.draw_string(0,200,"%s :%2.1f"%(names[index], max_score),color=(0,255,0),scale=2)

#录入人脸
val = key_gpio.value()
if(val == 0 and last_key_state == 1):
record_ftr = feature
record_ftrs.append(record_ftr) #将当前特征添加到已知特征列表
USART.write("Recode=OK\r\n")
last_key_state = 0
elif(val == 1):
last_key_state = 1

#串口2输出数据
#输出鼻子位置
USART.write("Useless package\r\n")
time.sleep(0.003)
USART.write("x=%d\r\n"%(nose[0]))
print("x=%d\r\n"%(nose[0]))
time.sleep(0.003)
USART.write("y=%d\r\n"%(nose[1]))
time.sleep(0.003)
USART.write("Mask=%d\r\n"%mask)
time.sleep(0.003)
USART.write("MaskStandard=%d\r\n"%maskStandard)
time.sleep(0.003)
USART.write("Area=%d\r\n"%MaxArea)
time.sleep(0.003)
if(index != -1):
USART.write("Index=%d\r\n"%(index+1))
else:
USART.write("Index=%d\r\n"%index)
USART.write("keep alive\r\n")
fps = clock.fps() #计算帧率
img.draw_string(0,0,"Masked: %d, Standard: %d"%(-DeltammRG,-DeltanoRG),(255,255,255),3,0,0,False,0,False,False,0,False,False)
img.draw_string(0,32,"FPS: %2.2f"%fps,(255,255,255),2,0,0,False,0,False,False,0,False,False)
a = lcd.display(img) #刷屏显示

Video

扯一点题外话,我上大学的一个愿望就是和几个水平和自己差不多的大佬一起组队打比赛,然后这样我也能赌大的,上一些有冒险性的技术,赌对了血赚,失败了有别人兜底。
很显然这回我们组并没有这个资本,结果却让我拿去赌,差点翻车。。。

最后回去的路上,发现我们没有好好读题,漏掉了一个测试项。。
然后还忘了展示 检测口罩是否戴好 的加分项…

明年国赛加油

2020-10-17
晚上十点突然通知我们组明早9点复测,然后大半夜赶往另外一个校区去拿作品准备复测。
顺带修了两个BUG

第二天:
复测似乎只问了我们录视频中没有录好/录到的内容,让我们把体温测了,让我们仔细拍了一下检测口罩时的屏幕。没用让补充视频中没有提到的自己实现的多余的功能。
当时我们想趁着口罩部分复测,顺带着演示一下我们还有”检测口罩是否佩戴标准”的功能。但是他们似乎不感兴趣,并且以为我们带上口罩还会误报…

第三天:
评级结果复核表下来了,不出意外是省二。