# 2021电赛D题方案
## 文件夹构成
raspberry: 摄像节点A和B代码和配置文件,使用同一套代码,只有IP配置不同
## 摄像节点端详解
摄像节点所做的事非常少,总共分为两件事
* 读取usb摄像头的图像并显示
* 监听8001端口,如果有设备连接上来,就开始进行图像传输
#### 依赖的库
python版本:python3.6及以上
只需要opencv和opencv_contrib
#### send.py
该文件定义了一个Send类,基于TCP协议,监听本地端口,同时提供了传输图像的函数
主要特性如下
* 支持掉线重连,方便调试,不会出现在调试nano端图像处理代码时,每次启动代码还要重启节点的代码
* 将二维图像降维成一维并编码压缩,在nano端恢复
* 在传输图像的同时传输时间,因为网络延迟的原因,nano端接收到图像的时间其实是不准的,
通过在发送时将发送时间一并发送,可以保证网络出现延迟时nano端收到的时间是准确的
* 传输图像的可靠性由TCP协议保证
#### fix_distortion.py
该文件提供了摄像头畸变矫正功能,是我的队友张同学用matlab计算参数提供,据他说可以矫正畸变,
但是我的肉眼不能分辨添加之后是否有不同,是否有该文件对结果没有任何影响。
#### raspberry.py
树莓派端的主文件,实现了读取视频流和传输视频,需要指出以下几点
1. 创建的显示图像的窗口需要自适应大小,同时设置始终置顶
2. 读取图像和传输图像需要使用双线程以解决帧滞留问题(该问题导致去年省赛识别题只拿了省二,具体定义可以百度)
3. usb摄像头分辨率为480P,即640*480,使用千兆交换机传输帧数每秒可达25帧以上
#### 图像传输协议
* 首先传送16个字节,该字节表示该图像发送时的时间
* 其次再次传送16个字节,该字节表示图像数据的长度
* 压缩后的图像数据直接跟在后面
#### 节点IP配置
终端输入sudo vim /etc/network/interfaces,内容改为本仓库中raspberry/interfaces的内容
#### 开机自启动配置
在/home/pi/.config/autostart(如果没有该目录就创建它),将本仓库中raspberry/opencvtest.desktop复制进去,
该文件中Exec=/home/pi/opencvtest/opencvtest.sh表示需要执行的shell脚本的路径,编写一个shell脚本启动程序就可以了
## Nano终端
Nano终端负责接收图像显示、激光笔识别和测量相关
#### 依赖的库
依赖的库比较少,就不提供requirements.txt了
python版本:3.6及以上
* numpy
* opencv_python
* opencv_contrib_python
* imutils
* pyserial
#### 识别方法
激光笔为黑色激光笔,实际场景中在视野里没有比激光笔更大的黑色色块,
所以只需要先去除掉面积过大的黑色色块,再选取剩下的色块中面积最大的,
即可认为是激光笔。识别流程如下:
1. 为了程序阈值鲁棒性,采用hsv图像来进行颜色识别
2. 对hsv图像二值化,阈值通过hsv的色表查询
3. 对二值化后的图像寻找轮廓并按面积排序
4. 选取面积小于一定阈值且面积最大的色块,该色块就是激光笔
#### 测量方法
* 寻找到激光笔后,记录摆动周期,每一帧的时间通过图传协议接收得到,
同时记录激光笔再两路摄像头中横坐标最大摆动之差。经过计算,
平均摆动时间为2s-2.5s之间,所以测量10个周期去掉两个最大值和两个最小值取平均,
可以使结果更加精确。
* 首先计算角度,公式为tan(theta) = x1/x2,求一个反正切就可以得到角度,
科式力对与角度的影响再短时间内(基本为1分钟)摆动时影响不大,
注意松手时不要手抖。
* 计算完角度后,根据角度计算周期,如果角度小于5度,那么认为B节点测量是准确的,
如果角度大于85度,那么认为A节点测量是准确的,否则用A、B节点的平均值作为周期。
* 周期的公式为L=T^2\*g/(4\*pi^2)
* 这里我们担心再0度和90度时角度会测量不准,所以加了一个小trick,
我们启动的按键有4个,其中3个是相同功能,另外一个按下后,
测量到的角度一定会小于5度或大于85度,不过实际测量时效果很好,
所以这个小trick并没有用到
#### read.py
图像传输client端,提供了读取视频并解码的功能。
#### search.py
提供了识别和测量T和delta_x的功能,因为两路摄像头其实是做相同功能,
所以封装成一个类,使用时只需要实例化两个对象,方便复用。
该类里再识别和计算中有很多trick,以下稍微简述,详情可以看代码:
* 因为激光笔会有反光,所以我们没有直接使用hsv中标准黑色阈值,我们提高了明亮度
* 如果一个黑色色块在图像边缘,那么就忽略他
* 我们除了画边框之外,还画上了坐标,很多同学以为自己是功能做的不够多,
其实更多的是因为界面好不好看,你把结果大大的画在图像上,
肯定比用命令行print给评委老师印象好,你的界面上有一些随时变换的数值,
有一些花花绿绿的线条、方框,评委老师就会觉得你做的很好,
这样你从一开始就已经赢其他队伍太多了。
#### usart.py
封装了串口相关类,提供了包括和32单片机自检,接收开始测量命令,
返回测量结束指令,比较指令是否一致
#### some_image.py
创建空白图像,可以将测量后的结果画在这些空白图像上,比用print打印结果更好
#### algorithm.py
提供了计算线长和角度的函数。注意计算线长时,
得到的长度是激光笔几何中心所在点对应的线长,需要对结果减去一个常量
#### nano.py
程序主入口,具体工作流程如下:
1. 创建两个线程分别创建socket连接并读取图像
2. 主线程使用copy()方法获取图像拷贝,防止识别时图像对象改变
3. 上电后需要自检,具体方法是上位机向下位机发送自检命令,下位机返回一个自检命令表示自检通过
4. 识别时因为使用的是拷贝,所以在你下一次识别时,
不能确定接收线程是否已经接收到另一帧图像,所以需要进行一个判重操作,
否则会对一帧图像多次处理
5. 通过串口接收到测量指令后,会将图像传给A、B两个Search对象,
通过查询对象内的T和dealt_x是否为None,可以得到是否测量结束
6. 根据当前不同的状态,在空白图像里绘制出当前状态或测量结果
7. 画椭圆的代码太复杂,目前不太好整理,这里先不添加画椭圆的办法,
这里先开个坑,等到以后想到怎么简化代码后在添加