基于互联网的摄像测量系统
设计报告
2021 年全国大学生电子设计竞赛试题
<10cm
O
拍摄方向
θ
1mx1m
![](/i/l/?n=23&i=blog/3231279/202307/3231279-20230707114010990-1288234243.png)
拍摄方向
B |
A |
边长为1米的 正方形测试区域
l
互联网
参赛注意事项
(1) 11 月 4 日 8:00 竞赛正式开始。本科组参赛队只能在【本科组】题目中任选一题;高职 高专组参赛队在【高职高专组】题目中任选一题,也可以选择【本科组】题目。
(2) 参赛队认真填写《登记表》内容,填写好的《登记表》交赛场巡视员暂时保存。
(3) 参赛者必须是有正式学籍的全日制在校本、专科学生,应出示能够证明参赛者学生身 份的有效证件(如学生证) 随时备查。
(4) 每队严格限制 3 人,开赛后不得中途更换队员。
(5) 竞赛期间,可使用各种图书资料和网络资源,但不得在学校指定竞赛场地外进行设计 制作,不得以任何方式与他人交流,包括教师在内的非参赛队员必须迴避,对违纪参 赛队取消评审资格。
(6) 11 月 7 日 20:00 竞赛结束,上交设计报告、制作实物及《登记表》,由专人封存。
基于互联网的摄像测量系统(D 题)
【本科组】
一 任务
设计并制作一个基于互联网的摄像测量系统。系统构成如图 1 所示。图中边 长为 1 米的正方形区域三个顶点分别为 A 、B 和 O。系统有两个独立的摄像节 点,分别放置在 A 和B。两个摄像节点拍摄尽量沿 AO 、BO 方向正交,并通过 一个百兆/千兆以太网交换机与连接在该交换机的一个终端节点实现网络互联。 交换机必须为互联网通用交换机,使用的网口可以任意指定。在 O 点上方悬挂 一个用柔性透明细线吊起的激光笔,透明细线长度为 l。激光笔常亮向下指示, 静止下垂时的指示光点与 O 点重合。拉动激光笔偏离静止点的距离小于 10cm, 松开后时激光笔自由摆动,应保证激光笔指示光点的轨迹经 O 点往复直线运动, 轨迹与 OA 边的夹角为 θ。利用该系统实现对长度 l 和角度θ 的测量。
图 1. 摄像测量系统示意图
二 要求
1. 基本要求
(1) 设计并制作两个独立的摄像节点,每个节点由一个摄像头和相应的电 路组成。两个摄像节点均可以拍摄到激光笔的运动视频并显示。
(2) 设计并制作终端节点。在终端显示器上可以分别和同时显示两个摄像 节点拍摄的实时视频。在视频中可以识别出激光笔,并在视频中用红色方框实时 框住激光笔轮廓。
(3) 测量系统在终端节点设置一键启动。从激光笔摆动开始计时,测量系 统通过对激光笔周期摆动视频信号的处理,自动测量长度 l,50cm≤ l ≤150cm ,θ 角度自定。测量完成时,终端声光提示并显示长度 l。要求测量误差绝对值小于 2cm,测量时间小于 30 秒。
2. 发挥部分
(1)一键启动后,测量系统通过两个独立摄像节点的网络协同工作,当 θ=0° 和 θ=90°时,能自动测量长度 l,50cm≤ l ≤150cm。要求测量误差绝对值小于 2cm , 测量时间小于 30 秒。
(2) 一键启动后,可以测量 θ ,0°≤ θ ≤90°。要求测量误差绝对值小于 5°。 测量时间小于 30 秒。
(3) 其他。
三 说明
(1) 摆的柔性透明细线建议采用单股透明的钓鱼线,直径小于 0.2mm。不 要采用一般捻和的缝纫线,防止激光笔吊起后自转。考虑实际摆与理想摆的差异 以及各地重力加速度会有差异,系统应具有校准处理的功能。
(2) 系统获取摆的信息必须来自摄像节点拍摄的视频信息,不得在摆及其 附近安装其他传感器和附加装置。θ角度的标定可利用量角器测量激光指示光点 轨迹与 OA 边的夹角实现。
(3) 两个摄像节点拍摄的取景范围仅限激光笔摆动区间的内容,不能包含 全部柔性细线的内容和地面激光光点轨迹的内容。在测量 l 和θ 的过程中,如果 视频包含上述内容,需用纸片遮挡这部分内容。否则不进行测试。
(4) 拍摄背景为一般实验室场景,背景物体静止即可,不得要求额外处理。
(5) 三个节点不得采用台式计算机和笔记本电脑。
四 评分标准
项 目 | 主要内容 | 满分 | |
设计报告 | 方案论证 | 测量系统总体方案设计 | 4 |
理论分析与计算 | 系统性能分析 网络协同工作原理分析与计算 | 6 | |
电路与程序设计 | 总体电路图 程序设计 | 4 | |
测试方案与测试结果 | 测试数据完整性 测试结果分析 | 4 | |
设计报告结构及规范性 | 摘要 设计报告正文的结构 图表的规范性 | 2 | |
合计 | 20 | ||
基本要求 | 完成第(1) 项 | 6 | |
完成第(2) 项 | 24 | ||
完成第(3) 项 | 20 | ||
合计 | 50 | ||
发挥部分 | 完成第(1) 项 | 20 | |
完成第(2) 项 | 26 | ||
其他 | 4 | ||
合计 | 50 | ||
总 分 | 120 |
准备
一个架子 | 悬挂激光笔 |
一个激光笔长度10cm | 单摆物体,光用来瞄准 |
两个树莓派4B | 作为摄像头节点主机 |
一个卷尺 | 量摆线的长度 |
三个显示器 | 显示图像 |
三个HDMI | 连接主机与显示器 |
一个交换机 | 系统组成局域网 |
三根网线 | 连接主机与交换机 |
一个量角器 | 测量摆线轨迹夹角 |
一个的白板 | 固定设备位置,需要量角 |
两个摄像头 | 记录实时的视频 |
两个摄像头支架 | 支棱摄像头 |
一个矩阵键 | 实现一键启动功能 |
一个蜂鸣器 | 声音提示 |
一个LED | 灯光提示 |
若干长的鱼线 | 挂激光笔 |
两个敲代码的电脑 | 写代码,一个软件、一个硬件 |
二、思路
(1) 设计并制作两个独立的摄像节点,每个节点由一个摄像头和相应的电路组成。两个摄像节点均可以拍摄到激光笔的运动视频并显示。
1、我们都知道摄像头可以拍摄图像,怎样通过摄像节点去拍摄图像,我们考虑通过使用树莓派4B与摄像头的连接。
:树莓派4B本身自带有USB接口,所以通过USB接口与携带有USB接口的摄像头相连接
2、设计代码,通过使用树莓派与摄像头的连接,拍摄激光笔的行动轨迹图像,我们首先先了解如何通过python打开摄像头,这里需要用到opencv的知识点如果你自身的python本身没有安装open cv可以采用pip安装方法
可在命令行模式输入:
pip install opencv-python
当你需要用到该模块可以使用import cv2导入
这是代码:
import cv2 as cv #0表示内置摄像头,1为外置摄像头
cap = cv.VideoCapture(0) #打开内置摄像头
num = 1
while (cap.isOpened()):#检测摄像头是否开启
ret,frame = cap.read()#读取每一针的数据
#设置显示大小
farm=cv.resize(frame,dsize=(1080,1080))
#显示图像
cv.imshow("2",farm)
cv.waitKey(1) & 0xFF#键盘检测
#按键判断
if cv.waitKey(1) & 0xFF == ord("s"):#键盘判断
#对图片进行保存
cv.imwrite(r"D:\xuexi\python\pythonProject2\人脸识别"+str(num)+".jpg",farm)
print("图片保存成功")
num += 1
#设置按键退出程序
elif cv.waitKey(1) & 0xFF == ord("q"):
break
通过了解使用摄像头的方法,只需要在树莓派的python编辑器中,将代码复制后即可打开与树莓派连接的摄像头。我们除了能打开摄像头外,还需要会使用摄像头拍摄视频。
:#在代码中需要导入一些必要的库,包括OpenCV库、numpy库和datetime库
import cv2
import numpy as np
import datetime
# 录制视频之前需要确定视频文件的文件名和视频的分辨率
video_name = "my_video.mp4"
width = 640
height = 480
#使用OpenCV库中的VideoWriter类可以创建一个视频写入对象,用于将视频帧写入到视频文件中
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
video_writer = cv2.VideoWriter(video_name, fourcc, 25, (width, height)) #fourcc参数指定视频编码格式,25表示视频的帧率
#使用OpenCV库的VideoCapture类可以打开摄像头
capture = cv2.VideoCapture(0) #0表示打开默认的摄像头
#循环中,将不断地读取摄像头的每一帧,并将其写入到视频文件中
while(True):
ret, frame = capture.read()
if ret:
video_writer.write(frame)
cv2.imshow("video recording", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
#ret是一个布尔值,表示是否成功读取一帧。如果成功则将帧写入到视频文件中,同时使用imshow方法实时显示读取的帧,按照“q”键退出循环
拍摄后的视频位于在创立这个文件夹的位置
(2) 设计并制作终端节点。在终端显示器上可以分别和同时显示两个摄像节点拍摄的实时视频。在视频中可以识别出激光笔,并在视频中用红色方框实时框住激光笔轮廓。
关于节点,我们的想法是采用树莓派和摄像头的连接作为单独的节点,最后再传入另外一个树莓派(作为最终的平台进行展示和计算)。这里需要采用socket通信,两台连接有摄像头作为客户端,将数据传入为服务端的树莓派,进行展示。
我们先了解socket通信简单的代码:
服务端传输文件:
import socket
# 创建 socket对象
socket_server = socket.socket()
# 绑定 socket_server到指定的ip地址
socket_server.bind(("localhost", 8888))
# 监听端口, listen()内书写数字,表示可以接受链接的数量
socket_server.listen(1)
# 等待客户端连接,接收到的 result是一个二元元组, accept()是一个阻塞的方法,如果没有连接不会往下执行
result = socket_server.accept()
conn = result[0] # 客户端连接对象
address = result[1] # 客户端地址信息
print(f"接收到的客户端连接信息为{address}")
while True:
# 接收客户端信息,recv接受的参数是缓冲区大小,一般1024即可,返回的是一个字节数组,bytes对象,不是字符串,再将其decode解码为字符串对象
data = conn.recv(1024).decode("UTF-8")
print(f"客户端发来的消息是:{data}")
# 回复消息
msg = input("请输入回复的消息:")
if msg == 'exit':
break
conn.send(msg.encode("UTF-8"))
# 关闭连接
conn.close()
socket_server.close()
客户端接收文件:
import socket
# 创建 socket对象
socket_client = socket.socket()
# 让 socket对象 socket_client 连接到服务端
socket_client.connect(("localhost", 8888))
while True:
msg = input("请输入你要发送的消息:")
if msg == 'exit':
break
# 发送消息
socket_client.send(msg.encode("UTF-8"))
# 接收消息
data = socket_client.recv(1024).decode("UTF-8")
print(f"服务器回复的消息为:{data}")
socket_client.close()
视频要求可以识别激光笔,我们的想法是,采用透明的鱼线吊着激光笔,并且激光笔的外面包裹一层与拍摄环境不相同的颜色,以方便被识别框住。这里需要采用opencv中的轮廓检测:
(https://blog.csdn.net/Ggs5s_/article/details/130331330)
为什么用轮廓检测:使用轮廓检测可以获得物体的边界,方便在图像中对他们进行定位
什么是轮廓:当我们把物体边缘所有的点连接在一起可以获得轮廓。对于特定的轮廓是指那些具有相同颜色和亮度的边界点像素
调用流程:
(1)读入图像
(2)将读入图像转化为灰度图
(3)对(2)得到的灰度图进行二值化或者Candy边缘检测处理,从而把感兴趣的物体加亮凸显出来以便于使用轮廓检测算法
(4)进行轮廓检测(使用 findContours()函数来检测图像中的所有的轮廓)
(5)在原图中显示轮廓(使用 drawContours()函数来在原图上显示轮廓)
(3)测量系统在终端节点设置一键启动。从激光笔摆动开始计时,测量系统通过对激光笔周期摆动视频信号的处理,自动测量长度 l,50cm≤ l ≤150cm ,θ 角度自定。测量完成时,终端声光提示并显示长度 l。要求测量误差绝对值小于 2cm,测量时间小于 30 秒。
:根据单摆的摆长公式是:,L是我们要求的未知量,所以我们只需要求出周期,就可以代入公式求出摆长L。怎么计算周期,通过摄像头拍摄的视频,我们知道当放下激光笔后,就开始进行摆动,所以我们如果在摆动过程中通过设置时间节点,计算两边的最高点所记录下的时间节点t1,t2,两者相减就可以求出半个周期,再乘以2就是一整个周期。
方法 | 帧差法 | 模版匹配法 | 阈值筛选法 |
效果 | 100 | 95 | 90 |
稳定性 | 80 | 50 | 90 |
代码量 | 大 | 少 | 一般 |
背景问题 | 不受环境影响 | 很受环境影响 | 有点受环境影响 |
在这里,我们采用帧差法:
帧间差分法是一种通过对视频图像序列的连续两帧图像做差分运算获取运动目标轮廓的方法。当监控场景中出现异常目标运动时,相邻两帧图像之间会出现较为明显的差别,两帧相减,求得图像对应位置像素值差的绝对值,判断其是否大于某一阈值,进而分析视频或图像序列的物体运动特性。其数学公式描述如下:
D(x,y)为连续两帧图像之间的差分图像,I(t)和I(t-1)分别为t和t-1时刻的图像,T为差分图像二值化时选取的阈值,D(x,y) = 1表示前景,D(x,y) = 0表示背景。
优点:算法实现简单,程序设计复杂度低,运行速度快;动态环境自适应性强,对场景光线变化不敏感
import cv2
import numpy as np
import time as t
import socket
import json
# 画框
def drawcontour(img, a):
start = t.time()
print(1)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 转化为灰度图
gray = cv2.GaussianBlur(gray, (21, 21), 0) # 高斯滤波
ret,binary = cv2.threshold(gray, 127, 255 , cv2.THRESH_BINARY)# 转化为二值图
thresh = cv2.dilate(binary, None, iterations=2) # 膨胀
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 获取轮廓
for i in contours:
# 判断激光笔轮廓
if cv2.contourArea(i) < 2000 or cv2.contourArea(i) > 5000:
continue
x, y, w, h = cv2.boundingRect(i)
# 排除不正确的面积比
if h < 2 * w or h > 8 * w:
continue
# 绘画矩形
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
# 获取中心点坐标
M = cv2.moments(i) # 计算第一条轮廓的各阶矩,字典形式
cX = int(M["m10"]/M["m00"])
cY = int(M["m01"]/M["m00"])
date[a][0] = cX# 将x坐标写入列表中
date[a][1] = cY# 将y坐标写入列表中
date[a][2] = start# 将时间写入列表
# 判断周期
def get_T(date):
t = []
THE_point = []# 储存第11号位置的点的时间列表
T = 0
# 判断是否再次经过该点
for i in range(100):
if date[10][0] - 1 < date[i][0] < date[10][0] + 1 and date[10][1] - 1 < date[i][1] < date[10][1] -1:
THE_point.append(date[i][2])
l = len(THE_point)
# 计算周期
for i in range(0,l,2):
if i < l-2:
t1 = THE_point[i+2] - THE_point[i]
if t1 < 1 or t1 > 3:
t.append(t1)
l1 = len(t)
if l1 == 0:
print('未采集到数据。')
else:
T = sum(t)/l1
return T
# 计算周期
def get_length(T):
pi = 3.14125
g = 10
L = (abs(T)**2*g)/(4*pi**2) - 5.2
return L
发挥部分:一键启动后,测量系统通过两个独立摄像节点的网络协同工作,当 θ=0° 和 θ=90°时,能自动测量长度 l,50cm≤ l ≤150cm。要求测量误差绝对值小于 2cm , 测量时间小于 30 秒
(2) 一键启动后,可以测量 θ ,0°≤ θ ≤90°。要求测量误差绝对值小于 5°。 测量时间小于 30 秒
最终展示:
import cv2
import numpy as np
import time as t
import socket
import json
# 画框
def drawcontour(img, a):
start = t.time()
print(1)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转化为灰度图
gray = cv2.GaussianBlur(gray, (21, 21), 0) # 高斯滤波
ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 转化为二值图
thresh = cv2.dilate(binary, None, iterations=2) # 膨胀
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 获取轮廓
for i in contours:
# 判断激光笔轮廓
if cv2.contourArea(i) < 2000 or cv2.contourArea(i) > 5000:
continue
x, y, w, h = cv2.boundingRect(i)
# 排除不正确的面积比
if h < 2 * w or h > 8 * w:
continue
# 绘画矩形
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
# 获取中心点坐标
M = cv2.moments(i) # 计算第一条轮廓的各阶矩,字典形式
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
date[a][0] = cX # 将x坐标写入列表中
date[a][1] = cY # 将y坐标写入列表中
date[a][2] = start # 将时间写入列表
# 判断周期
def get_T(date):
t = []
THE_point = [] # 储存第11号位置的点的时间列表
T = 0
# 判断是否再次经过该点
for i in range(100):
if date[10][0] - 1 < date[i][0] < date[10][0] + 1 and date[10][1] - 1 < date[i][1] < date[10][1] - 1:
THE_point.append(date[i][2])
l = len(THE_point)
# 计算周期
for i in range(0, l, 2):
if i < l - 2:
t1 = THE_point[i + 2] - THE_point[i]
if t1 < 1 or t1 > 3:
t.append(t1)
l1 = len(t)
if l1 == 0:
print('未采集到数据。')
else:
T = sum(t) / l1
return T
# 计算周期
def get_length(T):
pi = 3.14125
g = 10
L = (abs(T) ** 2 * g) / (4 * pi ** 2) - 5.2
return L
# 客户端
def client(rx_lx):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.31.178', 80000))
# 转化为json
json_string = json.dumps(rx_lx)
# 发送数据
s.send(bytes(json_string.encode('utf-8')))
s.close()
# 找到两端的点
def get_left_right(x, y):
left_x = 0
left_y = 0
right_x = 0
right_y = 0
if left_x == 0 and left_y == 0: # 初始化左边的点
left_x, left_y = x, y
if left_x < x and left_y < y: # 最左边点的特点有x为最大且y为最大
left_x, left_y = x, y
if right_x == 0 and right_y == 0: # 初始化右边的点
right_x, right_y = x, y
if right_x > x and right_y < y: # 最右边点的特点有x为最小且y为最大
right_x, right_y = x, y
rx_lx = [right_x, left_x]
return rx_lx
# 主函数
if __name__ == '__main__':
cap = cv2.VideoCapture(0)
a = 0
date = np.zeros((100, 3))
# 循环得到所有数据并画出轮廓
while cap.isOpened():
ret, img = cap.read()
drawcontour(img, a)
rx_lx = get_left_right(date[a][0], [a[1]])
print(a)
cv2.imshow('winname', img)
a += 1
if cv2.waitKey(1) == ord('q'):
break
if a == 99:
break
cv2.destroyAllWindows()
T = get_T(date)
L = get_length(T)
client(rx_lx)
print(L)
传输左右端长度
import cv2
import numpy as np
import time as t
import socket
import json
# 画框
def drawcontour(img, a):
start = t.time()
print(1)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 转化为灰度图
gray = cv2.GaussianBlur(gray, (21, 21), 0) # 高斯滤波
ret,binary = cv2.threshold(gray, 127, 255 , cv2.THRESH_BINARY)# 转化为二值图
thresh = cv2.dilate(binary, None, iterations=2) # 膨胀
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 获取轮廓
for i in contours:
# 判断激光笔轮廓
if cv2.contourArea(i) < 2000 or cv2.contourArea(i) > 5000:
continue
x, y, w, h = cv2.boundingRect(i)
# 排除不正确的面积比
if h < 2 * w or h > 8 * w:
continue
# 绘画矩形
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
# 获取中心点坐标
M = cv2.moments(i) # 计算第一条轮廓的各阶矩,字典形式
cX = int(M["m10"]/M["m00"])
cY = int(M["m01"]/M["m00"])
date[a][0] = cX# 将x坐标写入列表中
date[a][1] = cY# 将y坐标写入列表中
date[a][2] = start# 将时间写入列表
# 客户端
def client(rx_lx):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.31.178',80000))
# 转化为json
json_string = json.dumps(rx_lx)
s.send(bytes(json_string.encode('utf-8')))
s.close()
# 找到两端的点
def get_left_right(x, y):
left_x = 0
left_y = 0
right_x = 0
right_y = 0
if left_x == 0 and left_y ==0:# 初始化左边的点
left_x,left_y =x,y
if left_x < x and left_y < y:# 最左边点的特点有x为最大且y为最大
left_x,left_y = x,y
if right_x == 0 and right_y == 0:# 初始化右边的点
right_x,right_y = x,y
if right_x > x and right_y < y:# 最右边点的特点有x为最小且y为最大
right_x,right_y = x,y
rx_lx = [right_x,left_x]
return rx_lx
# 主函数
if __name__ == '__main__':
cap = cv2.VideoCapture(0)
a = 0
date = np.zeros((100,3))
# 循环得到所有数据并画出轮廓
while cap.isOpened():
ret, img = cap.read()
drawcontour(img, a)
rx_lx = get_left_right(date[a][0], [a[1]])
print(a)
cv2.imshow('winname', img)
a += 1
if cv2.waitKey(1) == ord('q'):
break
if a == 99:
break
cv2.destroyAllWindows()
client(rx_lx)
服务端代码
import socket
import json
import math
from threading import Thread
# 服务端
def server(ip, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((ip, port))
s.listen(1)
con, addr = s.accept()
# 接收消息
msg = con.recv(1024).decode('utf-8')
# json转回列表
mymsg = json.loads(msg)
s.close()
return mymsg
if __name__ == '__main__':
l1 = Thread(target=server, args=('192.168.31.67', 80000))
l2 = Thread(target=server, args=('192.168.31.93', 80000))
x = l1[0] - l1[1]
y = l2[0] - l2[1]
angle = math.atan2(x, y) * 180 / math.pi