Skip to content

Latest commit

 

History

History
122 lines (103 loc) · 6.72 KB

README.md

File metadata and controls

122 lines (103 loc) · 6.72 KB

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. 画椭圆的代码太复杂,目前不太好整理,这里先不添加画椭圆的办法, 这里先开个坑,等到以后想到怎么简化代码后在添加