时空行为检测 Webcam Demo 分析

竺翰海
2023-12-01

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,定义了整体模型流程以及组成部分。
      • 即,模型包括 backboneroi_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之间)

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() 来使用模型推理并获取结果。
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
  • 主要包括两个对象

    • 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 所需的输入数据,并调用该函数,不关心细节。
 类似资料: