- 教程
- 人工智能
- 机器学习
- 深度学习
【免费下载链接】AI-For-Beginners
12 Weeks, 24 Lessons, AI for All!
本文基于 AI-For-Beginners 课程第 4 周"计算机视觉入门"的官方实验(Lab)文档,完整讲解如何仅用 OpenCV(不借助任何神经网络)从一段手掌在稳定背景上左右上下移动的视频中,利用稠密光流(Dense Optical Flow)加方向直方图,自动判断视频中哪些片段包含上、下、左、右四个方向的运动。读完后,你可以独立完成实验笔记 MovementDetection.ipynb 的全部四个步骤,并理解"纯计算机视觉算法"与深度学习方法各自的适用边界。
1. 实验任务:本 Lab 到底要做什么
实验文档 lab/README.md(该课程讲义 Introduction to Computer Vision 末尾的 "Assignment" 小节直接链接到此实验)给出了明确的任务设定:
- 素材:一段视频 palm-movement.mp4,其中一只手掌在稳定(固定)背景前依次做左移、右移、上移、下移四个动作。
- 主要目标(Goal):使用Optical Flow(光流)技术,判断视频的哪些片段(帧区间)分别包含向上、向下、向左、向右的运动。
- 进阶目标(Stretch goal):进一步结合**肤色(skin tone)**信息,真正对手掌/手指做定位与跟踪,即把光流给出的"全画面运动"收敛到"手本身在往哪走"。
文档在 "Takeaway" 一节总结了本实验的核心认知:像运动检测、指尖检测这类相对复杂的任务,有时完全可以用纯计算机视觉手段解决,不需要深度学习。因此了解 OpenCV 这类库的能力边界非常重要。这一点与课程讲义 06-IntroCV/README.md 的结论一致——先用算法把图像"整理"好,能显著降低后续神经网络对训练数据量的要求。
2. 环境准备与实验材料
2.1 Python 环境
仓库根目录提供统一的 conda 环境定义 environment.yml(环境名ai4beg),与本实验直接相关的依赖有:
| 依赖 | 作用 |
|---|---|
conda-forge::opencv | 本实验的核心库,提供视频读取、光流、色彩空间变换等全部算法 |
matplotlib=3.9 | 可视化帧、光流图、方向直方图 |
numpy=1.26 | 帧与光流的数组化表示、直方图与归一化运算 |
jupyter/ipykernel/ipywidgets | 运行.ipynb实验笔记 |
环境创建方式(在本仓库外执行,仓库本身只读):
conda env create -f environment.yml conda activate ai4beg适用前提:光流函数
cv2.calcOpticalFlowFarneback属于 OpenCV 基础模块(cv2),不依赖 GPU;视频读取依赖系统具备 ffmpeg 后端,conda-forge 版 opencv 已自带该依赖。
2.2 实验文件清单
| 文件 | 说明 |
|---|---|
| lab/README.md | 实验任务书(本文的主体来源) |
| lab/palm-movement.mp4 | 输入视频:手掌四方向移动素材 |
| lab/MovementDetection.ipynb | 起始笔记:四个 Markdown 步骤 + 四个# Code here代码格,需自行补全 |
| OpenCV.ipynb | 课程讲义配套笔记,演示了视频读帧、帧差法、calcOpticalFlowFarneback光流与 HSV 可视化的完整参考实现 |
起始笔记 MovementDetection.ipynb 的结构是:按顺序给出 4 个带说明的 Markdown 单元,每个后面跟一个空代码格。下面按笔记的原始步骤顺序逐步展开,并结合 OpenCV.ipynb 中的已验证代码给出可直接运行的参考实现。
3. 步骤一:读取视频帧
笔记要求:"Start by getting video frames as described in the lecture"(按讲义所述获取视频帧)。讲义 OpenCV.ipynb 中给出的标准读帧代码是:
import cv2 import numpy as np vid = cv2.VideoCapture('palm-movement.mp4') c = 0 frames = [] while vid.isOpened(): ret, frame = vid.read() if not ret: # 读取失败即视频结束 break frames.append(frame) c += 1 vid.release() print(f"Total frames: {c}")要点:
- 每个
frame是一个 NumPy 数组,形状为(H, W, 3),且 OpenCV 传统上以BGR通道序读取; - 本实验只做方向分析、不关心颜色,讲义的通用做法是先把所有帧转灰度:
bwframes = [cv2.cvtColor(x, cv2.COLOR_BGR2GRAY) for x in frames]讲义中还展示了如何用帧间数组差值(frame difference)检测"画面里有没有东西在动":固定相机下相邻帧几乎相同,灰度帧相减后的范数在静止时很低、有显著运动时升高。但 OpenCV.ipynb 明确指出,帧差法只能告诉你变化量有多大,无法回答哪些像素在朝哪个方向移动——这正是本实验要改用光流的原因。
4. 步骤二:计算稠密光流并转换到极坐标
笔记原文要求:"calculate dense optical flow frames as described in the lecture, and convert dense optical flow to polar coordinates"。
4.1 稠密光流(Farneback 算法)
OpenCV.ipynb 中计算相邻帧间稠密光流的代码为:
flows = [cv2.calcOpticalFlowFarneback(f1, f2, None, 0.5, 3, 15, 3, 5, 1.2, 0) for f1, f2 in zip(bwframes[:-1], bwframes[1:])]cv2.calcOpticalFlowFarneback的参数与本课程取值对应如下(Farneback 是基于多项式展开的稠密光流算法,输入必须为灰度图):
| 位置参数 | 本实验取值 | 含义 |
|---|---|---|
prev/next | 相邻两灰度帧 | 上一帧与当前帧 |
flow | None | 输出缓冲区(由 OpenCV 自动分配) |
pyr_scale | 0.5 | 图像金字塔中相邻层间的缩放系数 |
levels | 3 | 金字塔层数,越大越能捕捉大范围运动 |
winsize | 15 | 每个金字塔层的平滑窗口大小 |
iterations | 3 | 每层金字塔的光流估计迭代次数 |
poly_n | 5 | 局部像素邻域的多项式阶数(宽) |
poly_sigma | 1.2 | 多项式拟合的高斯核标准差 |
flags | 0 | 默认行为(OPTFLOW_DEFAULT) |
输出的每个flow形状为(H, W, 2):与帧同尺寸的两个通道,分别是每个像素运动向量的x、y 分量(y 轴向下,故 y 分量为正表示像素在向下移动)。
课程同时介绍了光流的两种类型(见 06-IntroCV/README.md):
- Dense Optical Flow(稠密):为画面中每个像素都计算运动向量,得到完整向量场——本实验需要逐像素方向做直方图,因此选稠密光流;
- Sparse Optical Flow(稀疏):只跟踪特征点(如边缘)在帧间的轨迹,计算量更小,适合特征明显的场景。
4.2 极坐标转换与 HSV 可视化
二维向量难以直接"看"方向。OpenCV.ipynb 用一个巧妙的思路:把光流向量转成极坐标,得到每像素的方向(角度)与强度(模长),再映射到 HSV 色彩空间——色相(Hue)编码方向、明度(Value)编码强度、饱和度固定为 255:
def flow_to_hsv(flow): hsvImg = np.zeros((flow.shape[0], flow.shape[1], 3), dtype=np.uint8) mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1]) hsvImg[..., 0] = 0.5 * ang * 180 / np.pi # 角度 [0,2π) -> 色相 [0,180) hsvImg[..., 1] = 255 # 饱和固定 hsvImg[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX) # 模长归一化为明度 return cv2.cvtColor(hsvImg, cv2.COLOR_HSV2BGR)cv2.cartToPolar一次性输出模长mag与角度ang(弧度,范围[0, 2π)),后一步的直方图统计正是直接基于这个角度数组。讲义笔记中描述,将事件片段内的光流逐帧转 HSV 后,偏绿色对应向左移动、偏蓝色对应向右移动,可直观读出整体运动趋势;此外笔记还提示了一个实用技巧——若整帧像素几乎朝同一方向流动,通常是相机自身在移动,可做反向补偿。
5. 步骤三:对每帧光流构建方向直方图
笔记要求:"Build histogram of directions for each of the optical flow frame",并给出两条关键提示:
- 直方图的作用:统计各方向区间(bin)内落了多少运动向量,不同运动方向会在不同 bin 上形成峰值,从而把帧内的运动方向分离出来;
- 阈值去噪:"You may also want to zero out all vectors whose magnitude is below certain threshold"——把模长小于阈值的向量置零,以滤除眼睛眨动、头部微晃等与手势无关的微小运动(这正是"稳定背景"假设下光流法最容易受到的干扰源)。
基于笔记描述与讲义实现风格,可写出如下参考实现(n_bins取值可自行调整,bin 越宽方向分辨率越低、抗噪越好):
def direction_histogram(flow, mag_thresh=1.0, n_bins=36): mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1]) ang = mag >= mag_thresh # 弱向量置零(布尔掩码),抑制眼部/头部等微动 hist, _ = np.histogram(ang[mag >= mag_thresh], bins=n_bins, range=(0, 2 * np.pi)) return hist对每一帧光流调用一次,并把部分帧的直方图画出来对比(笔记原文要求 "Plot the histograms for some of the frames")。
6. 步骤四:从直方图判断上/下/左/右方向
笔记最后一步:"Looking at histograms, it should be pretty straightforward how to determine direction of movement. You need to select those bins that correspond to up/down/left/right directions, and that are above certain threshold."
结合图像坐标系约定(y 轴向下,见 4.1 节),四个方向在角度域上的中心位置可以推断为:
| 方向 | 向量特征 | 角度中心(弧度) |
|---|---|---|
| 向右(right) | x>0, y≈0 | 0(含2π边界) |
| 向下(down) | x≈0, y>0 | π/2 |
| 向左(left) | x<0, y≈0 | π |
| 向上(up) | x≈0, y<0 | 3π/2 |
参考实现:统计落在每个方向"扇区"内的向量占比,超过判定阈值(例如占有效向量的 25%~30%)才认定该方向成立;若四个方向都不显著,则判为"无明确方向"(例如手掌停顿的帧)。对整段视频逐帧执行后,即可输出形如"第 40~75 帧:左移;第 80~110 帧:上移……"的区间结果,完成任务书要求——找出视频中分别包含 up/down/left/right 运动的部分。
SECTORS = {'right': (0.0, 2*np.pi), 'down': (0, np.pi/2), 'left': (np.pi/2, np.pi), 'up': (np.pi, 3*np.pi/2)} def judge_direction(hist, min_share=0.30): total = hist.sum() if total == 0: return None centers = {'right': 0, 'down': np.pi/2, 'left': np.pi, 'up': 3*np.pi/2} # 简化策略:找最大峰值 bin,检查其是否落在某方向邻域且占比超阈值 peak = int(np.argmax(hist)) ang = (peak + 0.5) * (2 * np.pi / len(hist)) % (2 * np.pi) for name, c in centers.items(): if abs(ang - c) < (np.pi / len(hist)) * 3 and hist[peak] / total >= min_share: return name return None以上两段代码是依据 MovementDetection.ipynb 各步骤的文字描述,并沿用 OpenCV.ipynb 中已验证的
cartToPolar/光流用法整理出的参考实现;实验笔记本身只保留# Code here,具体参数(mag_thresh、n_bins、min_share)需按视频帧率与运动幅度自行调参。
7. 进阶目标:用肤色掩码把"全画面光流"收敛到"手掌光流"
任务书的 Stretch goal 是基于肤色(skin tone)真正跟踪手掌/手指运动。其核心思路可以概括为三步(对应任务书引用的两篇社区教程所讲的 OpenCV + Python 肤色跟踪方案,此处仅描述方法本身):
- 把每帧转 HSV 后,用肤色在 HSV 空间的常见取值范围做掩码(skin mask),得到"手掌区域"二值图;
- 用形态学操作(
cv2.morphologyEx开/闭运算)清理掩码,取最大连通域作为手掌候选区域; - 只对掩码内的光流向量做方向直方图(即第 5 节函数中把
ang掩码与肤色掩码相与),并可用掩码质心(cv2.moments)逐帧追踪手掌位置变化作为方向判定的交叉验证。
这样做的好处是:直方图不再受"手掌移动导致画面其余部分产生反向背景响应"等干扰,方向判定更稳定,也能进一步支持逐帧的指尖/手掌位置输出。
8. 实验总结与延伸
- 本实验的完整技术链路是:读帧 → 灰度化 → Farneback 稠密光流 →
cartToPolar极坐标 → 阈值去弱向量 → 方向直方图 → 扇区阈值判定,全程只使用 OpenCV + NumPy,没有任何神经网络参与。 - 实验印证了课程文档的结论:运动检测这类"相对复杂的任务"完全可以由经典计算机视觉算法解决;知道 OpenCV 能做什么、在什么场景下替代模型,是 AI 工程师的重要基础能力。
- 想回顾光流的完整演示(包括帧差法动量曲线、
flow_to_hsv可视化、事件片段裁剪),可运行课程配套笔记 OpenCV.ipynb;讲义 06-IntroCV/README.md 中另有关于 OpenCV 图像预处理(阈值化、透视变换、特征点提取)的配套示例,可作为"先用 CV 降低 NN 负担"这一思想的更多实例。 - 注意本方法的前提假设:相机固定、背景稳定、光照基本不变——若背景大幅变化或光照剧烈波动,弱向量阈值与方向扇区阈值都需重新校准,必要时改用稀疏光流加特征点方案。
- 教程
- 人工智能
- 机器学习
- 深度学习
【免费下载链接】AI-For-Beginners
12 Weeks, 24 Lessons, AI for All!
相关推荐
AI for Beginners 计算机视觉实验:使用 OpenCV 光流(Optical Flow)检测手掌上下左右运动
AI for Beginners 计算机视觉实验:使用 OpenCV 光流(Optical Flow)检测手掌上下左右运动 本指南围绕 AI for Begin
教程人工智能机器学习深度学习AI for Beginners 计算机视觉实验:用 OpenCV 光流法从视频中判定手掌移动方向
AI for Beginners 计算机视觉实验:用 OpenCV 光流法从视频中判定手掌移动方向 本篇实验指南围绕 AI for Beginners 课程“计
教程人工智能机器学习深度学习AI-For-Beginners 计算机视觉实验:基于 OpenCV 光流的手掌运动方向检测
AI For Beginners 计算机视觉实验:基于 OpenCV 光流的手掌运动方向检测 本文基于 AI For Beginners 课程"Introduc
教程人工智能机器学习深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考