15 OpenCV 运动检测
OpenCV 运动检测
本章教程使用 OpenCV 来检测画面中的变化,你可以为变化多少设置一个阈值,更改阈值,可以更改运动检测的敏感度。
本章节需要前置章节的基础。
准备工作
由于产品开机默认会自动运行主程序,主程序会占用摄像头资源,这种情况下是不能使用本教程的,需要结束主程序或禁止主程序自动运行后再重新启动机器人。
这里需要注意的是,由于机器人主程序中使用了多线程且由 crontab 配置开机自动运行,所以常规的 sudo killall python 的方法通常是不起作用的,所以我们这里介绍禁用主程序自动运行的方法。
如果你已经禁用了机器人主程序的开机自动运行,则不需要执行下面的结束主程序章节。
结束主程序
1. 点击上方本页面选项卡旁边的 “+”号,会打开一个新的名为 Launcher 的选项卡。
2. 点击 Other 内的 Terminal,打开终端窗口。
3. 在终端窗口内输入 bash 后按回车。
4. 现在你可以使用 Bash Shell 来控制机器人了。
5. 输入命令: crontab -e
6. 如果询问希望使用什么编辑器,输入 1 后按回车,选择使用 nano。
7. 打开 crontab 的配置文件后,你可以看到以下两行内容
@reboot ~/ugv_pt_rpi/ugv-env/bin/python ~/ugv_pt_rpi/app.py >> ~/ugv.log 2>&1 @reboot /bin/bash ~/ugv_pt_rpi/start_jupyter.sh >> ~/jupyter_log.log 2>&1
8.在 `……app.py >> ……` 这行的最前面添加一个 # 号来注释掉这行。
# @reboot ~/ugv_pt_rpi/ugv-env/bin/python ~/ugv_pt_rpi/app.py >> ~/ugv.log 2>&1 @reboot /bin/bash ~/ugv_pt_rpi/start_jupyter.sh >> ~/jupyter_log.log 2>&1
9. 在终端页面,按 Ctrl + X 退出,它会询问你 Save modified buffer? 输入 Y,按回车,保存变更。
10. 重启设备,注意该过程会暂时关闭当前的 jupyter Lab,如果你上一步没有注释掉 ……start_jupyter.sh >>…… 这一行,那么当机器人重新开机后,你仍然可以正常使用 jupyter Lab (JupyterLab 与 机器人主程序 app.py 是互相独立运行的),可能需要重新刷新页面。
11. 这里需要注意一点,由于下位机持续通过串口与上位机通信,上位机在重启过程中有可能会由于串口电平的连续变化不能正常开机,拿上位机为树莓派的情况举例,重启时树莓派关机后不会再开机,红灯常亮绿灯不亮,此时可以关闭机器人电源开关,再打开,机器人就能够正常重启了。
12. 输入重启命令: sudo reboot
13. 等待设备重启后(重启过程中树莓派的绿灯会闪烁,当绿灯闪烁频率降低或灭掉后即代表已经启动成功),刷新页面,继续该教程的剩余部分。
例程
以下代码块可以直接运行:
1. 选中下面的代码块
2. 按 Shift + Enter 运行代码块
3. 观看实时视频窗口
4. 按 STOP 关闭实时视频,释放摄像头资源
如果运行时不能看到摄像头实时画面
- 需要点击上方的 Kernel - Shut down all kernels
- 关闭本章节选项卡,再次打开
- 点击 STOP 释放摄像头资源后重新运行代码块
- 重启设备
注意事项
如果使用CSI摄像头则需要注释`frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)`这一句。
本章节的特性
你需要更改一些参数来调增 OpenCV 对画面中变化检测的阈值(灵敏度)`threshold`,这个阈值越低,OpenCV 对画面的变化越敏感。
运行
运行代码块是,你可以看到摄像头的实时画面,可以在画面前挥手,本例程会自动将出现变化的部分使用绿色的方框圈起来。
import cv2
from picamera2 import Picamera2
import numpy as np
from IPython.display import display, Image
import ipywidgets as widgets
import threading
import imutils # 用于简化图像处理任务的库
threshold = 2000 # 设置动态检测阈值
# 创建一个“停止”按钮来控制流程
# ================
stopButton = widgets.ToggleButton(
value=False,
description='Stop',
disabled=False,
button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
tooltip='Description',
icon='square' # 按钮图标(FontAwesome 名称,不带 `fa-` 前缀)
)
# 显示函数定义,用于捕获和处理视频帧,同时进行运动检测
# ================
def view(button):
# 如果你使用的是CSI摄像头 需要取消注释 picam2 这些代码,并注释掉 camera 这些代码
# 因为新版本的 OpenCV 不再支持 CSI 摄像头(4.9.0.80),你需要使用 picamera2 来获取摄像头画面
# picam2 = Picamera2() # 创建 Picamera2 实例
# picam2.configure(picam2.create_video_configuration(main={"format": 'XRGB8888', "size": (640, 480)})) # 配置摄像头参数
# picam2.start() # 启动摄像头
camera = cv2.VideoCapture(-1) # 创建摄像头实例
#设置分辨率
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
display_handle=display(None, display_id=True)
i = 0
avg = None # 用于存储平均帧
while True:
# frame = picam2.capture_array() # 从摄像头捕获帧
# frame = cv2.flip(frame, 1) # if your camera reverses your image
_, frame = camera.read() # 从摄像头捕获一帧图像
img = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # 将帧颜色从 RGB 转换为 BGR
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 将帧转换为灰度图
gray = cv2.GaussianBlur(gray, (21, 21), 0) # 对灰度图应用高斯模糊
if avg is None: # 如果平均帧不存在,则创建它
avg = gray.copy().astype("float")
continue
try:
cv2.accumulateWeighted(gray, avg, 0.5) # 更新平均帧
except:
continue
frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg)) # 计算当前帧和平均帧的差值
# 应用阈值,找到差值图像中的轮廓
thresh = cv2.threshold(frameDelta, 5, 255, cv2.THRESH_BINARY)[1]
thresh = cv2.dilate(thresh, None, iterations=2)
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# 遍历轮廓
for c in cnts:
# 如果轮廓太小,则忽略
if cv2.contourArea(c) < threshold:
continue
# 计算轮廓的边界框,将其绘画到矩形框
(mov_x, mov_y, mov_w, mov_h) = cv2.boundingRect(c)
cv2.rectangle(frame, (mov_x, mov_y), (mov_x + mov_w, mov_y + mov_h), (128, 255, 0), 1) # 在移动区域画矩形框
_, frame = cv2.imencode('.jpeg', frame) # 将处理后的帧编码为 JPEG 格式
display_handle.update(Image(data=frame.tobytes())) # 更新显示的图像
if stopButton.value==True: # 检查是否按下了“停止”按钮
# picam2.close() # 如果是,则关闭摄像头
cv2.release() # 如果是,则关闭摄像头
display_handle.update(None) # 清除显示的图像
# 显示停止按钮并启动视频流显示线程
# ================
display(stopButton)
thread = threading.Thread(target=view, args=(stopButton,))
thread.start()