文章目录
0. 前言
- 最近打算基于 MMAction2 实现一个基于webcam的时空行为检测Demo,已提交 PR
- 为了实现上面的功能,需要研究两个方面:
- 如何使用 MMAction2 中时空行为检测模型,主要就是过一遍 demo_spatiotemporal_det 的源码
- 时空行为检测 Webcam Demo 应该怎么写,主要就是过一遍 SlowFast 中 demo_net.py
1. MMAction2 中的时空行为检测模型
- 网上时空行为检测的源码分析比较少,作为 MMAction2 的 contribuer,既然写到这里了,那就稍微介绍一下这部分功能把。
1.1 模型构建源码
- MMAction2 中模型构建都是基于配置文件搭积木。
- 每个模型由多个组件构成,每个组件可以看成是一个类型的积木。
- 每一类模型(如2D行为识别、3D行为识别、时序行为检测、时空行为检测)都对应一个 meta architecture
- meta architecture 这个词来自TensorFlow Object Detection,因为我也找不到更合适的词来形容。
- meta architecture 定义了一类模型的基本结构,即这一类模型一般包含哪几个组件,前向过程一般是什么。
- meta architecture 就是一类积木。
- meta architecture 中的每一类组件也代表一类积木。
- 从这个角度来说,
Recognizer2D
就是2D行为识别模型的 architecture。 - 那么,时空行为检测模型的 meta architecture 是什么呢?
- 是
mmdetection
中的FastRCNN
。 - 从源码层面非常漂亮,毕竟代码复用,不用写太多代码。
- 从新手学习角度,就非常难受了,毕竟找不到这个类在哪里。
- 源码可以参考 这里
- 是
- 时空行为检测模型的主要组件:
FastRCNN
:meta architecture,定义了整体模型流程以及组成部分。- 即,模型包括
backbone
和roi_head
- 即,模型包括
- backbone 就是一些3D特征提取结构,如SlowFast、SlowOnly等
- 换句话说,模型的输入就是 SlowFast、SlowOnly 等backbone的输入
- 输出传入 roi head
- roi_head 就是将目标检测中的 roi 操作扩展到3维。
- 模型的输出就是 roi head 的输出
- 传入的参数就是 backbone 的结果以及 proposals(我们是fast rcnn,所以proposals是外部传入,而不是时空行为检测模型生成的)
- 总而言之,MMAction2 中行为识别模型
- 输入是图像以及proposals
- 输出是一个长度为
num_classes
的list,list中每个元素是一个num_proposals, 5
的ndarray对象。5维分别是bbox + score。
1.2 数据构建源码
- 本节进一步介绍一下MMAction2中时空行为检测模型的输入数据(只关心test,不关心train)。
- 整体流程就是 val_pipeline,主要就是:
- 获取从连续帧中按照一定规则提取若干帧,作为后续模型输入。主要就是每隔
frame_interval
提取一帧,一共提取clip_len
帧图像。 - 对图像进行预处理,包括短边resize以及norm操作。
- 改变输入数据的格式为 NCTHW。
- 获取从连续帧中按照一定规则提取若干帧,作为后续模型输入。主要就是每隔
- 模型的输入是一个dict,测试时模型输入主要需要
img, proposals
两个key。- 其中,img的就是经过预处理的图像。目前,test时bacth size必须是1,所以batch size可能是
1, 1, 3, 32, 256, 256
,这个其实就是Recotnizer3D
模型的输入了。 - proposals 就是人物的bbox,格式为
xmin, ymin, xmax, ymax
,浮点数,数值范围是像素值(而不是0 -1
之间)
- 其中,img的就是经过预处理的图像。目前,test时bacth size必须是1,所以batch size可能是
1.3 demo_spatiotemporal_det.py 分析
- 这个Demo处理的是一个视频文件(而不是实时视频流)。
- 基本流程就是:
- 读取视频文件,将视频帧保存在 内存中 以及 本地文件 中。。。。。第一次看到的时候,吓到了。
- 读取 label map,即一个 class id 到 class name 的字典
- 将对内存中的视频帧进行短边resize到256
- 读取
val_pipeline
中的SampleAVAFrames
,获取clip_len/frame_interval
信息,根据这两个信息以及命令行输入的predict_stepsize
获取所有中间帧id。- 时空行为检测模型的一个输入可以看做一个clip,每个clip都对应了一个中间帧。
- 使用 mmdet 获取所有中间帧的检测结果。
- 这里就会用到保存在本地的视频帧
- 要求使用的模型必须是 COCO 的,因为选择人物类别的时候,就按照COCO的格式,选择了 class 0
- 最终得到的 bbox 是基于内存中的视频帧,即短边resize256后的视频帧,格式为
xmin, ymin, xmax, ymax
,浮点数,数值范围是像素值(而不是0 -1
之间),可直接作为时空行为检测模型的输入。
- 构建时空行为检测模型,导入权重。
- 遍历所有中间帧(以及对应的bbox),换句话说,就是遍历所有 clip,获取推理结果
- 获取每个clip对应的视频帧下标,根据下标读取内存中的视频帧
- 获取新对象(而不是直接改变内存中的视频帧),并进行处理,包括norm操作+改变格式为
1, 3, clip_len, height, width
(不添加 batch size)+存入显存 - 执行时空行为检测模型推理,获取推理结果
- 融合 human detection 结果以及上一部的预测结果
- 结果格式是一个长度为 num_clips 的 list
- 每个元素代表一个clip的预测结果,通过一个元组表示,元组包含三个 bboxes, class names, class scores 三个部分
- bboxes 是一个 ndarray 对象,shape是[num_proposals, 4],格式是 normed xmin, ymin, xmax, ymax
- class names 和 class scores 都是长度为 num_proposals 的数组,数组的元素也是数组(因为一个proposal可能有多个class name 和 class score)
- 展示结果
- 这一步用到了上一部的结果,以及本地的视频帧
- 将整体视频分为 num_clips 份,每一份使用相同的结果进行画框+展示。
- 关于显示的参数有两个
predict_stepsize
以及output_stepsize
。 - 前者表示时空行为检测模型推理的频率,即每
predict_stepsize
帧预测一次行为标签。 - 后者表示展示过程跳过多少针,即每
output_stepsize
帧展示一帧。比如原先fps是20,output_stepsize是4,那最终展示的视频就是5fps
2. SlowFast 中的时空行为检测 Webcam Demo
- 现在提供时空行为检测 Webcam Demo 的开源代码有:
- YOWO:人体检测+行为识别于一体,与 MMAction2 的模型差别较大,没有太大参考价值。
- Alphaction:模型差距较大,可能有一定参考价值,但代码太多还没细看。
- SlowFast:多线程版,很大参考价值。
- 其实按照前面的内容,写一个基于 MMAction2 的单线程版 Webcam SpatioTemporal Demo 已经没问题了。但总还是要看看大佬是怎么写的。
2.1 SlowFast Webcam 源码概述
- 功能包括了行为识别与时空行为检测。由于本文只关注时空行为检测,所以精简了一下。
- 入口函数在demo_net.py中,精简后是:
frame_provider = ThreadVideoManager(cfg)
for task in tqdm.tqdm(run_demo(cfg, frame_provider)):
frame_provider.display(task)
frame_provider.join()
frame_provider.clean()
- 因此,主要源码就是:
ThreadVideoManager
run_demo
2.2 ThreadVideoManager
-
功能:
- 读取视频流数据是一个线程。
- 展示导出结果是另外一个线程。
- 还有一个主线程,为模型提供输入。
-
首先,Demo中数据传输是通过
TaskInfo
- 每个
TaskInfo
可以理解为一个clip的输入数据,主要内容包括 frames 以及其他一些元信息。 - 另外,
TaskInfo
是有 id 的按创建顺序从小到大开始编号。
- 每个
-
其次,对于这个类,一共有3个线程
- 主线程,就是通过上面的
run_demo(cfg, frame_provider)
调用,调用的方法就是__next__
。- 从 read queue中读取task。
run_demo
函数将读取的task作为model输入,获取预测结果、更新task,添加到 write queue 中。
- put thread,从视频流中读取数据,构建task,更新
put_id
,向 read queue 或 write queue 添加 task。 - read thread,从 write queue 中读取task,更新
get_id
,并输出(本地视频或cv2.imshow)当前clip还没有显示过的若干帧。
- 主线程,就是通过上面的
-
有一堆线程安全的操作:
- 读取/写入
put_id
- 读取/写入
get_id
- 读取/写入 write queue
- 读取/写入 read queue
- 读取/写入
2.3 run_demo
- 定义了整体Demo的流程,精简后的代码如下
- 数据读取类,前一节已经介绍,下面就是
frame_provider
- 模型推理使用
ActionPredictor
,是单GPU版本。- 多GPU版本的也有
AsyncDemo
,由于我不关注,所以相关代码取消了。 - 后面就是使用
model.put(task)
和model.get()
来使用模型推理并获取结果。
- 多GPU版本的也有
- 数据读取类,前一节已经介绍,下面就是
video_vis = VideoVisualizer(
num_classes=cfg.MODEL.NUM_CLASSES,
class_names_path=cfg.DEMO.LABEL_FILE_PATH,
top_k=cfg.TENSORBOARD.MODEL_VIS.TOPK_PREDS,
thres=cfg.DEMO.COMMON_CLASS_THRES,
lower_thres=cfg.DEMO.UNCOMMON_CLASS_THRES,
common_class_names=common_classes,
colormap=cfg.TENSORBOARD.MODEL_VIS.COLORMAP,
mode=cfg.DEMO.VIS_MODE,
)
async_vis = AsyncVis(video_vis, n_workers=cfg.DEMO.NUM_VIS_INSTANCES)
model = ActionPredictor(cfg=cfg, async_vis=async_vis)
seq_len = cfg.DATA.NUM_FRAMES * cfg.DATA.SAMPLING_RATE
num_task = 0
# Start reading frames.
frame_provider.start()
for able_to_read, task in frame_provider:
if not able_to_read:
break
if task is None:
time.sleep(0.02)
continue
num_task += 1
model.put(task)
try:
task = model.get()
num_task -= 1
yield task
except IndexError:
continue
while num_task != 0:
try:
task = model.get()
num_task -= 1
yield task
except IndexError:
continue
2.4 ActionPredictor
-
这个是同步版(单GPU)的模型推理与可视化工具。
- 有异步版(多GPU版),即
AsyncDemo
。
- 有异步版(多GPU版),即
-
主要包括两个对象
Predictor
:包括构建模型、导入权重、数据预处理(resize/crop/norm等)、模型推理,其中还包括一个 detectron2 的目标检测模型。AsyncVis
:后面会单独介绍。
-
主要函数就是:
put
:执行模型推理,并可视化数据get
:获取推理结果(保存在一个task对象中),返回task对象
-
AsyncVis
- 本质就是一个多进程的生产者、消费者模式类。
- 生产者就是调用
AsynVis
对象的put
方法,向进程安全的task_queue
队列中放入数据。同时,也会向get_indices_ls
数组添加 task.id - 消费者,就是通过一个
VideoVisualizer
对象作为输入,构建num_workders
个进程。- 消费者会从
task_queue
中读取数据,并将结果放入线程安全的result_queue
中。 - 消费者也会将
result_queue
中的数据转换为线程不安全的result_data
中。 - 每个进程的实际操作就是
draw_predictions
函数,即对指定的task画图。
- 消费者会从
- put操作就是向
task_queue
中添加数据,get 操作就是向result_queue
中获取数据。
2.5 结果可视化
- 入口函数:即上面提到的 draw_predictions,主要用到的就是 task 对象以及 VideoVisualizer 对象。
draw_predictions
方法主要就是一些数据预处理,构建VideoVisualizer.draw_clip_range
所需的输入数据,并调用该函数,不关心细节。