diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..4a57410537c5d26c11360506f183bcfad372f91e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include LICENSE +include README.md +include tools/__init__.py +include tools/utils.py +include tools/paddlevideo_clas.py +include data/k400/Kinetics-400_label_list.txt + +recursive-include paddlevideo/ *.py *.txt \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9e709cf1658573b0e245c243013c6cf4d5ff9b1f --- /dev/null +++ b/README.md @@ -0,0 +1,242 @@ +[English](README_en.md) | 中文 + +# PaddleVideo + +## 近期更新 + +- 新增骨骼点行为识别模型[CTR-GCN](./docs/zh-CN/model_zoo/recognition/ctrgcn.md). +- 新增轻量化行为识别模型[MoViNet](./docs/zh-CN/model_zoo/recognition/movinet.md). +- 新增视频时序分割模型[MS-TCN](./docs/zh-CN/model_zoo/segmentation/mstcn.md)、[ASRF](./docs/zh-CN/model_zoo/segmentation/asrf.md). + + +👀 🌟 **《产业级视频技术与应用案例》系列课程回放链接**: https://aistudio.baidu.com/aistudio/course/introduce/6742 🌟 + +​ 💖 **欢迎大家扫码入群讨论** 💖 +
+
+ +- 添加成功后回复【视频】加入交流群 + +## 简介 + +![python version](https://img.shields.io/badge/python-3.7+-orange.svg) ![paddle version](https://img.shields.io/badge/PaddlePaddle-2.0-blue) + + +PaddleVideo是[飞桨官方](https://www.paddlepaddle.org.cn/?fr=paddleEdu_github)出品的视频模型开发套件,旨在帮助开发者更好的进行视频领域的学术研究和产业实践。 + +
+
+
+ + +## 模型案例库 + +### 模型 + +- 模型库使用前请参考[安装说明](docs/zh-CN/install.md)、[使用指南](docs/zh-CN/usage.md)。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
行为识别方法
PP-TSM (PP series)PP-TSN (PP series)PP-TimeSformer (PP series)TSN (2D’)TSM (2D‘)
SlowFast (3D’)TimeSformer (Transformer‘)VideoSwin (Transformer’)AttentionLSTM (RNN‘)MoViNet (Lite‘)
基于骨骼点的动作识别方法
ST-GCN (GCN’)AGCN (GCN‘)CTR-GCN (GCN‘)
时序动作检测方法
BMN (One-stage‘)
视频时序分割
MS-TCN ASRF
时空动作检测方法
SlowFast+Fast R-CNN +
多模态
ActBERT (Learning‘)T2VLAD (Retrieval‘)
视频目标分割
CFBI (Semi‘)MA-Net (Supervised‘)
单目深度估计
ADDS (Unsupervised‘)
+ + +### 数据集 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
动作识别
Kinetics-400 (Homepage) (CVPR'2017)UCF101 (Homepage) (CRCV-IR-12-01)ActivityNet (Homepage) (CVPR'2015)YouTube-8M (Homepage) (CVPR'2017)
动作定位
ActivityNet (Homepage) (CVPR'2015)
时空动作检测
AVA (Homepage) (CVPR'2018)
基于骨架的动作识别
NTURGB+D (Homepage) (IEEE CS'2016)FSD (Homepage)
单目深度估计
Oxford-RobotCar (Homepage) (IJRR'2017)
文本视频检索
MSR-VTT (Homepage) (CVPR'2016)
文本视频预训练
HowTo100M (Homepage) (ICCV'2019)
+ + +### 应用案例 + +| Applications | Descriptions | +| :--------------- | :-------- | +| [FootballAction](https://github.com/PaddlePaddle/PaddleVideo/tree/application/FootballAction) | 足球动作检测方案| +| [BasketballAction](applications/BasketballAction) | 篮球动作检测方案 | +| [TableTennis](applications/TableTennis) | 乒乓球动作识别方案| +| [FigureSkating](applications/FigureSkating) | 花样滑冰动作识别方案| +| [VideoTag](applications/VideoTag) | 3000类大规模视频分类方案 | +| [MultimodalVideoTag](applications/MultimodalVideoTag) | 多模态视频分类方案| +| [VideoQualityAssessment](applications/VideoQualityAssessment) | 视频质量评估方案| +| [PP-Care](applications/PP-Care) | 3DMRI医疗图像识别方案 | +| [EIVideo](applications/EIVideo) | 视频交互式分割工具| +| [Anti-UAV](applications/Anti-UAV) |无人机检测方案| +| [AbnormalActionDetection](applications/AbnormalActionDetection) |异常行为检测方案| +| [PP-Human](applications/PPHuman) | 行人分析场景动作识别方案 | + + +## 文档教程 +- AI-Studio教程 + - [【官方】Paddle 2.1实现视频理解优化模型 -- PP-TSM](https://aistudio.baidu.com/aistudio/projectdetail/3399656?contributionType=1) + - [【官方】Paddle 2.1实现视频理解优化模型 -- PP-TSN](https://aistudio.baidu.com/aistudio/projectdetail/2879980?contributionType=1) + - [【官方】Paddle 2.1实现视频理解经典模型 -- TSN](https://aistudio.baidu.com/aistudio/projectdetail/2250682) + - [【官方】Paddle 2.1实现视频理解经典模型 -- TSM](https://aistudio.baidu.com/aistudio/projectdetail/2310889) + - [BMN视频动作定位](https://aistudio.baidu.com/aistudio/projectdetail/2250674) + - [花样滑冰选手骨骼点动作识别ST-GCN教程](https://aistudio.baidu.com/aistudio/projectdetail/2417717) + - [【实践】CV领域的Transformer模型TimeSformer实现视频理解](https://aistudio.baidu.com/aistudio/projectdetail/3413254?contributionType=1) +- 贡献代码 + - [如何添加新算法](./docs/zh-CN/contribute/add_new_algorithm.md) + - [配置系统设计解析](./docs/en/tutorials/config.md) + - [如何提pr](./docs/zh-CN/contribute/how_to_contribute.md) + + +## 赛事支持 + +- [基于飞桨实现花样滑冰选手骨骼点动作识别大赛](https://aistudio.baidu.com/aistudio/competition/detail/115/0/introduction), [AI Studio项目](https://aistudio.baidu.com/aistudio/projectdetail/2417717), [视频教程](https://www.bilibili.com/video/BV1w3411172G) +- [基于飞桨实现乒乓球时序动作定位大赛](https://aistudio.baidu.com/aistudio/competition/detail/127/0/introduction) +- [CCKS 2021:知识增强的视频语义理解](https://www.biendata.xyz/competition/ccks_2021_videounderstanding/) + +## 许可证书 +本项目的发布受[Apache 2.0 license](LICENSE)许可认证。 + +## 致谢 +- 非常感谢 [mohui37](https://github.com/mohui37)、[zephyr-fun](https://github.com/zephyr-fun)、[voipchina](https://github.com/voipchina) 贡献相关代码 diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000000000000000000000000000000000000..c5402dbfe6d5d30cb1edf1d7ce68b4109719f5d6 --- /dev/null +++ b/README_en.md @@ -0,0 +1,241 @@ +[简体中文](README.md) | English + +# PaddleVideo + +## Update: + +- add skeleton-base action recognition model [CTR-GCN](./docs/en/model_zoo/recognition/ctrgcn.md). +- add lite action recognition model [MoViNet](./docs/zh-CN/model_zoo/recognition/movinet.md). +- add temporal segment model [MS-TCN](./docs/zh-CN/model_zoo/segmentation/mstcn.md), [ASRF](./docs/zh-CN/model_zoo/segmentation/asrf.md). + +​ 💖 **Welcome to scan the code and join the group discussion** 💖 + +
+
+ +- Scan the QR code below with your Wechat and reply "video", you can access to official technical exchange group. Look forward to your participation. + +## Introduction + +![python version](https://img.shields.io/badge/python-3.7+-orange.svg) ![paddle version](https://img.shields.io/badge/PaddlePaddle-2.0-blue ) + + +PaddleVideo is a toolset for video tasks prepared for the industry and academia. This repository provides examples and best practice guildelines for exploring deep learning algorithm in the scene of video area. + +
+
+
+ + +## Model and Applications + +### Model zoo + +- Please refer to [Installation guide](docs/zh-CN/install.md) and [Usage doc](docs/zh-CN/usage.md) before using the model zoo. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Action recognition method
PP-TSM (PP series)PP-TSN (PP series)PP-TimeSformer (PP series)TSN (2D’)TSM (2D')
SlowFast (3D’)TimeSformer (Transformer')VideoSwin (Transformer’)AttentionLSTM (RNN')MoViNet (Lite‘)
Skeleton based action recognition
ST-GCN (Custom’)AGCN (Adaptive')CTR-GCN (GCN‘)
Sequence action detection method
BMN (One-stage')
temporal segment
MS-TCN ASRF
Spatio-temporal motion detection method
SlowFast+Fast R-CNN +
Multimodal
ActBERT (Learning')T2VLAD (Retrieval')
Video target segmentation
CFBI (Semi')MA-Net (Supervised')
Monocular depth estimation
ADDS (Unsupervised‘)
+ + +### Dataset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Action Recognition
Kinetics-400 (Homepage) (CVPR'2017)UCF101 (Homepage) (CRCV-IR-12-01)ActivityNet (Homepage) (CVPR'2015)YouTube-8M (Homepage) (CVPR'2017)
Action Localization
ActivityNet (Homepage) (CVPR'2015)
Spatio-Temporal Action Detection
AVA (Homepage) (CVPR'2018)
Skeleton-based Action Recognition
NTURGB+D (Homepage) (IEEE CS'2016)FSD (Homepage)
Depth Estimation
Oxford-RobotCar (Homepage) (IJRR'2017)
Text-Video Retrieval
MSR-VTT (Homepage) (CVPR'2016)
Text-Video Pretrained Model
HowTo100M (Homepage) (ICCV'2019)
+ + +### Applications + +| Applications | Descriptions | +| :--------------- | :------------ | +| [FootballAction]() | Football action detection solution| +| [BasketballAction](applications/BasketballAction) | Basketball action detection solution | +| [TableTennis](applications/ableTennis) | Table tennis action recognition solution| +| [FigureSkating](applications/FigureSkating) | Figure skating action recognition solution| +| [VideoTag](applications/VideoTag) | 3000-category large-scale video classification solution | +| [MultimodalVideoTag](applications/MultimodalVideoTag) | Multimodal video classification solution| +| [VideoQualityAssessment](applications/VideoQualityAssessment) | Video quality assessment solution| +| [PP-Care](applications/PP-Care) | 3DMRI medical image recognition solution | +| [EIVideo](applications/EIVideo) | Interactive video segmentation tool| +| [Anti-UAV](applications/Anti-UAV) |UAV detection solution| +| [AbnormalActionDetection](applications/AbnormalActionDetection) |Abnormal action detection solution| +| [PP-Human](applications/PPHuman) | Action recognition solution for pedestrian analysis scene | + + +## Documentation tutorial +- AI-Studio Tutorial + - [[Official] Paddle 2.1 realizes video understanding optimization model -- PP-TSM](https://aistudio.baidu.com/aistudio/projectdetail/3399656?contributionType=1) + - [[Official] Paddle 2.1 realizes video understanding optimization model -- PP-TSN](https://aistudio.baidu.com/aistudio/projectdetail/2879980?contributionType=1) + - [[Official] Paddle 2.1 realizes the classic model of video understanding -- TSN](https://aistudio.baidu.com/aistudio/projectdetail/2250682) + - [[Official] Paddle 2.1 realizes the classic model of video understanding -- TSM](https://aistudio.baidu.com/aistudio/projectdetail/2310889) + - [BMN video action positioning](https://aistudio.baidu.com/aistudio/projectdetail/2250674) + - [ST-GCN Tutorial for Figure Skate Skeleton Point Action Recognition](https://aistudio.baidu.com/aistudio/projectdetail/2417717) + - [[Practice]video understanding transformer model TimeSformer](https://aistudio.baidu.com/aistudio/projectdetail/3413254?contributionType=1) +- Contribute code + - [How to add a new algorithm](./docs/zh-CN/contribute/add_new_algorithm.md) + - [Configuration system design analysis](./docs/en/tutorials/config.md) + - [How to mention PR](./docs/zh-CN/contribute/how_to_contribute.md) + + +## Competition + +- [Figure skating action recoginition using skeleton based on PaddlePaddle](https://aistudio.baidu.com/aistudio/competition/detail/115/0/introduction), [AI Studio projects](https://aistudio.baidu.com/aistudio/projectdetail/2417717), [video course](https://www.bilibili.com/video/BV1w3411172G) +- [Table tennis action proposal localization based on PaddlePaddle](https://aistudio.baidu.com/aistudio/competition/detail/127/0/introduction) +- [CCKS 2021: Knowledge Augmented Video Semantic Understanding](https://www.biendata.xyz/competition/ccks_2021_videounde) + +## License + +PaddleVideo is released under the [Apache 2.0 license](LICENSE). + + +## Thanks +- Many thanks to [mohui37](https://github.com/mohui37)、[zephyr-fun](https://github.com/zephyr-fun)、[voipchina](https://github.com/voipchina) for contributing the code for prediction. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c4f28815b563cc28dcc5a16885a35d0655e8e7d5 --- /dev/null +++ b/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ['PaddleVideo'] +from .tools import PaddleVideo \ No newline at end of file diff --git a/applications/AbnormalActionDetection/README.md b/applications/AbnormalActionDetection/README.md new file mode 100644 index 0000000000000000000000000000000000000000..77935131c9fb95c1f0faf2ee98e1a34d2ba877ef --- /dev/null +++ b/applications/AbnormalActionDetection/README.md @@ -0,0 +1,153 @@ +# 异常行为识别 + +## 内容 +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型评估](#模型评估) +- [模型推理](#模型推理) +- [模型部署](#模型部署) +- [参考论文](#参考论文) + + +## 模型简介 +该代码库用于异常行为检测, 基于paddle2.2版本开发,结合PaddleVideo中的SlowFast+FasterRCNN模型实现7个异常行为的检测。 +主要框架如下: +
+
+
+ +AIStudio项目: [基于时空信息的异常行为检测](https://aistudio.baidu.com/aistudio/projectdetail/3431613) + +## 数据准备 + +### Step1 稀疏抽取视频帧 +首先稀疏抽取视频帧用于检测每帧中人的位置: + +``` +cd data/ava/script && bash extract_video_frames.sh abnormal_action_videos abnormal_action_frames 2 +``` + +* 第一个参数abnormal_action_videos:被抽帧的视频根目录; +* 第二个参数abnormal_action_frames:抽取的视频帧存放目录; +* 第三个参数2:抽帧帧率。 + +### Step2 目标检测 +用成熟的可检测人的目标检测模型检测上述步骤抽得的视频帧中的人。如PaddleDetection套件中的基于coco数据集训练得到的[PP-YOLOv2](https://github.com/PaddlePaddle/PaddleDetection/tree/develop/configs/ppyolo)模型。 + +### Step3 生成pkl文件 +将上述步骤得到的每个视频帧的检测结果进行转化,得到SlowFast_FasterRCNN模型需要的输入格式。注意我们只需要人的检测结果,其他目标不需要。 +SlowFast_FasterRCNN模型需要的proposals是pkl格式文件,该文件以字典形式存储检测结果,字典的key是视频帧的索引(video_id+frame_id拼接得到),value是一个list,每个元素是检测得到的人的位置信息和置信度。 + +``` +{ + 打架,0001: + [[0.036 0.098 0.55 0.979 0.995518] # x1,y1,x2,y2,score + [0.443 0.04 0.99 0.989 0.977824]] +} +``` + +### Step4 密集抽取视频帧 +对视频数据进行密集抽帧。 +SlowFast_FasterRCNN输入的视频帧是密集帧,因此需要再次对视频进行抽帧。具体命令如下: +``` +cd data/ava/script && bash extract_video_frames.sh abnormal_action_videos abnormal_action_frames_30fps 30 +``` + +具体参数同步骤1,只不过次数抽帧率为30fps。 + +### Step5 准备标签数据 +标签数据以pbtxt文件个数存储,本案例具体如下(注意行为标签id从1开始): +``` +item { + name: "挥棍" + id: 1 +} +item { + name: "打架" + id: 2 +} +item { + name: "踢东西" + id: 3 +} +item { + name: "追逐" + id: 4 +} +item { + name: "争吵" + id: 5 +} +item { + name: "快速奔跑" + id: 6 +} +item { + name: "摔倒" + id: 7 +} +``` + +## 模型训练 +异常行为检测模型基于在AVA数据集上训练得到模型进行迁移学习。具体训练命令如下: +``` +python main.py --validate -w AVA_SlowFast_FastRcnn_best.pdparams \ + -c configs/abnoraml_action.yaml +``` + + - w 预训练模型路径 + - c 配置文件路径 + +## 模型评估 +``` +python main.py --test \ + -w abnormal_action_SlowFast_FastRcnn.pdparams \ + -c configs/abnoraml_action.yaml +``` + +## 模型推理 +基于动态图的推理: +``` +python tools/ava_predict.py \ + -c configs/abnoraml_action.yaml \ + -w abnormal_action_SlowFast_FastRcnn.pdparams \ + --video_path data/wave_9.mp4 \ + --detection_model_name 'faster_rcnn/faster_rcnn_r50_fpn_1x_coco' \ + --detection_model_weights 'faster_rcnn_r50_fpn_1x_coco.pdparams' +``` + +- video_path 视频路径 +- detection_model_name 检测模型名称 +- detection_model_weights 检测模型权重路径 + +基于静态图模型进行推理: + +导出模型,动态图模型转换为静态图模型: + +``` +python tools/export_model.py \ + -c configs/abnoraml_action.yaml \ + -o inference_output \ + -p abnormal_action_SlowFast_FastRcnn.pdparams +``` + +- o 导出模型存放文件夹 +- p 被导出模型路径 + +基于导出的模型做推理: +``` +python tools/predict.py \ + -c configs/abnoraml_action.yaml \ + --input_file "data/wave_9.mp4" \ + --model_file "inference_output/abnormal_action_SlowFast_FastRcnn.pdmodel" \ + --params_file "inference_output/abnormal_action_SlowFast_FastRcnn.pdiparams" \ + --use_gpu=True \ + --use_tensorrt=False +``` + +## 模型部署 +请参考[Paddle Inference示例](https://paddle-inference.readthedocs.io/en/latest/quick_start/python_demo.html) + +## 参考论文 +- [SlowFast Networks for Video Recognition](https://arxiv.org/pdf/1812.03982.pdf), Christoph Feichtenhofer, Haoqi Fan, Jitendra Malik, Kaiming He diff --git a/applications/AbnormalActionDetection/configs/abnormal_action.yaml b/applications/AbnormalActionDetection/configs/abnormal_action.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8de5afc4328f2943dca7e031c3734baaf8aaded5 --- /dev/null +++ b/applications/AbnormalActionDetection/configs/abnormal_action.yaml @@ -0,0 +1,181 @@ +MODEL: #MODEL field + framework: "FastRCNN" + backbone: + name: "ResNetSlowFast" + depth: 50 + alpha: 4 + beta: 8 + width_per_group: 64 + fusion_kernel_sz: 5 + fuse_bn_relu: 0 + spatial_strides: [[1, 1], [2, 2], [2, 2], [1, 1]] + use_pool_af_s2: 0 + head: + name: "AVARoIHead" + bbox_roi_extractor: + name: "SingleRoIExtractor3D" + roi_layer_type: "RoIAlign" + output_size: 8 + with_temporal_pool: True + bbox_head: + name: 'BBoxHeadAVA' + in_channels : 2304 + num_classes : 8# +backgroud + multilabel: True + dropout_ratio: 0.5 + assigner: + name: 'MaxIoUAssignerAVA' + pos_iou_thr: 0.9 + neg_iou_thr: 0.9 + min_pos_iou: 0.9 + sampler: + name: 'RandomSampler' + num: 32 + pos_fraction: 1 + neg_pos_ub: -1 + add_gt_as_proposals: True + pos_weight: 1.0 + action_thr: 0.01 + +DATASET: #DATASET field + batch_size: 10 #single card bacth size + valid_batch_size: 1 + test_batch_size: 1 #Optional, test batch size per gpu. + num_workers: 0 + train: + format: "AVADataset" #the Class name of the dataset used by youerself + data_prefix: 'data/abnormal_action_frames_30fps' #Mandatory, train data root path + file_path: "data/abnormal_action_train.csv" #Mandatory, train data index file path + exclude_file: "data/excluded_timestamps.csv" + proposal_file: "data/frames_proposal_faster_rcnn.pkl" + label_file: "data/abnormal_action_list.pbtxt" + person_det_score_thr: 0.9 + num_max_proposals: 1000 + timestamp_start: 1 + timestamp_end: -1 + num_classes: 8 # with background + FPS: 15 + valid: + format: "AVADataset" + data_prefix: 'data/abnormal_action_frames_30fps' #Mandatory, train data root path + file_path: "data/abnormal_action_train.csv" #Mandatory, train data index file path + exclude_file: "data/excluded_timestamps.csv" + proposal_file: "data/frames_proposal_faster_rcnn.pkl" + label_file: "data/abnormal_action_list.pbtxt" + person_det_score_thr: 0.9 + num_max_proposals: 1000 + timestamp_start: -1 + timestamp_end: -1 + test_mode: True + num_classes: 8 # with background + FPS: 15 + test: + format: "AVADataset" #Mandotary, indicate the type of test dataset, please refer to the 'paddlevideo/loader/dataset'. + data_prefix: 'data/abnormal_action_frames_30fps' #Mandatory, train data root path + file_path: "data/abnormal_action_train.csv" #Mandatory, train data index file path + exclude_file: "data/excluded_timestamps.csv" + proposal_file: "data/frames_proposal_faster_rcnn.pkl" + label_file: "data/abnormal_action_list.pbtxt" + suffix: "{:05}.jpg" + person_det_score_thr: 0.9 + num_max_proposals: 1000 + timestamp_start: 900 + timestamp_end: 1800 + test_mode: True + FPS: 15 + +PIPELINE: + train: + sampler: + name: "SampleAVAFrames" + clip_len: 32 + frame_interval: 2 + decoder: + name: "RawFrameDecode" + transform: #Mandotary, image transfrom operator + - RandomRescale: + scale_range: (256,320) + - RandomCrop_v2: + size: 256 + - Flip: + flip_ratio: 0.5 + - Normalize: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + to_bgr: False + - PackOutput: + alpha: 4 + valid: + sampler: + name: "SampleAVAFrames" + clip_len: 32 + frame_interval: 2 + decoder: + name: "RawFrameDecode" + transform: #Mandotary, image transfrom operator + - Rescale: + scale_range: (-1, 256) + - Normalize: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + to_bgr: False + - PackOutput: + alpha: 4 + test: #Mandatory, indicate the pipeline to deal with the validing data. please refer to the 'paddlevideo/loader/pipelines/' + sample: + name: "SampleAVAFrames" #Sampler type. + clip_len: 32 + frame_interval: 2 + #test_mode: True + decoder: + name: "RawFrameDecode" + #test_mode: True + transform: #Mandatory, image transform operator. + - Rescale: + scale_range: (-1, 256) + - Normalize: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + to_bgr: False + - PackOutput: + alpha: 4 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + iter_step: True + name: 'CustomWarmupPiecewiseDecay' + warmup_epochs: 5 + warmup_start_lr: 0.0075 + step_base_lr: 0.075 + lrs: [1, 0.1, 0.01, 0.001, 0.0001, 0.00001] + gamma: 0.1 + steps: [0, 10, 15] + max_epoch: 20 + weight_decay: + name: 'L2' + value: 1e-5 + +METRIC: + name: 'AVAMetric' + file_path: "data/abnormal_action_train.csv" #Mandotary, test data index file path. + exclude_file: "data/excluded_timestamps.csv" + label_file: "data/abnormal_action_list.pbtxt" + custom_classes: 8 # +background + +INFERENCE: + name: 'AVA_SlowFast_FastRCNN_Inference_helper' + config_file_path: 'abnoraml_action.yaml' + detection_model_name: 'faster_rcnn/faster_rcnn_r50_fpn_1x_coco' + detection_model_weights: 'faster_rcnn_r50_fpn_1x_coco.pdparams' + predict_stepsize: 4 + num_frames: 32 + alpha: 4 + target_size: 256 + +model_name: AVA_SlowFast_FastRcnn +save_interval: 10 +val_interval: 1 +epochs: 2 #Mandatory, total epoch +log_level: "INFO" diff --git a/applications/AbnormalActionDetection/images/SlowFast_FasterRCNN.png b/applications/AbnormalActionDetection/images/SlowFast_FasterRCNN.png new file mode 100644 index 0000000000000000000000000000000000000000..5911405a1c709bad16db4fb73975a81a3e0801b5 Binary files /dev/null and b/applications/AbnormalActionDetection/images/SlowFast_FasterRCNN.png differ diff --git a/applications/Anti-UAV/README.md b/applications/Anti-UAV/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c55903c4ab6e02b3103cd4f044862d3a7ae7a195 --- /dev/null +++ b/applications/Anti-UAV/README.md @@ -0,0 +1,39 @@ +# Paddle-Anti-UAV +Anti-UAV base on PaddleDetection + +## Background +UAVs are very popular and we can see them in many public spaces, such as parks and playgrounds. Most people use UAVs for taking photos. +However, many areas like airport forbiden UAVs since they are potentially dangerous. In this case, we need to detect the flying UAVs in +these areas. + +In this repository, we show how to train a detection model using [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection). + +## Data preparation +The dataset can be found [here](https://anti-uav.github.io/dataset/). We direcly download the ```test-dev``` split composed of 140 videos +train the detection model. +* Download the ```test-dev``` dataset. +* Run `unzip Anti_UAV_test_dev.zip -d Anti_UAV`. +* Run `python get_image_label.py`. In this step, you may change the path to the videos and the value of `interval`. + +After the above steps, you will get a MSCOCO-style datasst for object detection. + +## Install PaddleDetection +Please refer to this [link](https://github.com/PaddlePaddle/PaddleDetection/blob/release/2.3/docs/tutorials/INSTALL.md). + +We use `python=3.7`, `Paddle=2.2.1`, `CUDA=10.2`. + +## Train PP-YOLO +We use [PP-YOLO](https://github.com/PaddlePaddle/PaddleDetection/tree/release/2.3/configs/ppyolo) as the detector. +* Run `git clone https://github.com/PaddlePaddle/PaddleDetection.git`. Note that you should finish this step when you install PaddleDetection. +* Move the anti-UAV dataset to `dataset`. +* Move `anti_uav.yml` to `configs/datasets`, move `ppyolo_r50vd_dcn_1x_antiuav.yml` to `configs/ppyolo` and move `ppyolo_r50vd_dcn_antiuav.yml` +to `configs/ppyolo/_base`. +* Keep the value of `anchors` in `configs/ppyolo/_base/ppyolo_reader.yml` the same as `ppyolo_r50vd_dcn_antiuav.yml`. +* Run `python -m paddle.distributed.launch --log_dir=./ppyolo_dygraph/ --gpus 0,1,2,3,4,5,6,7 tools/train.py -c configs/ppyolo/ppyolo_r50vd_dcn_1x_antiuav.yml &>ppyolo_dygraph.log 2>&1 &`. +Note that you may change the arguments, such as `batch_size` and `gups`. + +## Inference +Please refer to the infernce section on this [webpage](https://github.com/PaddlePaddle/PaddleDetection/blob/release/2.3/docs/tutorials/GETTING_STARTED.md). You can just switch the configeration file and trained model to your own files. + +![](https://github.com/qingzwang/Paddle-Anti-UAV/blob/main/demo1.gif) +![](https://github.com/qingzwang/Paddle-Anti-UAV/blob/main/demo.gif) diff --git a/applications/Anti-UAV/anti_uav.yml b/applications/Anti-UAV/anti_uav.yml new file mode 100644 index 0000000000000000000000000000000000000000..12a7fc67273d9e51150f3ca42b9712ba20e92377 --- /dev/null +++ b/applications/Anti-UAV/anti_uav.yml @@ -0,0 +1,19 @@ +metric: COCO +num_classes: 2 + +TrainDataset: + !COCODataSet + image_dir: train_imgs + anno_path: annotations/train.json + dataset_dir: dataset/anti_uav + data_fields: ['image', 'gt_bbox', 'gt_class', 'is_crowd'] + +EvalDataset: + !COCODataSet + image_dir: val_imgs + anno_path: annotations/val.json + dataset_dir: dataset/anti_uav + +TestDataset: + !ImageFolder + anno_path: annotations/val.json diff --git a/applications/Anti-UAV/get_image_label.py b/applications/Anti-UAV/get_image_label.py new file mode 100644 index 0000000000000000000000000000000000000000..f084d30105c7c0b8c269a29a0ab5582ef49a44f1 --- /dev/null +++ b/applications/Anti-UAV/get_image_label.py @@ -0,0 +1,164 @@ +import cv2 +import os +import json + +# please change it to your path +path = '/workspace/wangqingzhong/Anti_UAV' +annotation_path = 'annotations' +train_img_path = 'train_imgs' +val_img_path = 'val_imgs' +if not os.path.exists(annotation_path): + os.makedirs(annotation_path) +if not os.path.exists(train_img_path): + os.makedirs(train_img_path) +if not os.path.exists(val_img_path): + os.makedirs(val_img_path) + +train_info = { + 'images': [], + 'type': + 'instances', + 'annotations': [], + 'categories': [{ + "supercategory": "none", + "id": 1, + "name": "drone" + }, { + "supercategory": "none", + "id": 2, + "name": "noise" + }] +} +val_info = { + 'images': [], + 'type': + 'instances', + 'annotations': [], + 'categories': [{ + "supercategory": "none", + "id": 1, + "name": "drone" + }, { + "supercategory": "none", + "id": 2, + "name": "noise" + }] +} + +# you can change it +interval = 5 +dirs = os.listdir(path) +train_img_id = 0 +val_img_id = 0 +for d in dirs: + if 'new' in d: + video_file = os.path.join(path, d, 'IR.mp4') + label_file = os.path.join(path, d, 'IR_label.json') + labels = json.load(open(label_file, 'r')) + exits = labels['exist'] + gt_bbox = labels['gt_rect'] + assert len(exits) == len(gt_bbox) + videocap = cv2.VideoCapture(video_file) + i = 0 + while True: + success, frame = videocap.read() + if success: + if i % interval == 0: + img_name = d + '_' + str(i) + '.jpg' + cv2.imwrite(os.path.join(val_img_path, img_name), frame) + height, width, depth = frame.shape + x, y, w, h = gt_bbox[i] + isexist = exits[i] + if isexist: + category_id = 1 + else: + category_id = 2 + draw_frame = cv2.rectangle(frame, (x, y), (x + w, y + h), + (0, 255, 0), 2) + img_name_draw = d + '_' + str(i) + 'draw.jpg' + cv2.imwrite(os.path.join(val_img_path, img_name_draw), + draw_frame) + + img_info = { + 'file_name': img_name, + 'height': float(height), + 'width': float(width), + 'id': val_img_id + } + ann_info = { + 'area': float(w) * float(h), + 'iscrowd': 0, + 'bbox': [float(x), + float(y), + float(w), + float(h)], + 'category_id': category_id, + 'ignore': 0, + 'image_id': val_img_id, + 'id': val_img_id + 1 + } + val_info['images'].append(img_info) + val_info['annotations'].append(ann_info) + val_img_id += 1 + i += 1 + else: + print('finish {}'.format(d)) + break + else: + video_file = os.path.join(path, d, 'IR.mp4') + label_file = os.path.join(path, d, 'IR_label.json') + labels = json.load(open(label_file, 'r')) + exits = labels['exist'] + gt_bbox = labels['gt_rect'] + assert len(exits) == len(gt_bbox) + videocap = cv2.VideoCapture(video_file) + i = 0 + while True: + success, frame = videocap.read() + if success: + if i % interval == 0: + img_name = d + '_' + str(i) + '.jpg' + cv2.imwrite(os.path.join(train_img_path, img_name), frame) + height, width, depth = frame.shape + x, y, w, h = gt_bbox[i] + isexist = exits[i] + if isexist: + category_id = 1 + else: + category_id = 2 + draw_frame = cv2.rectangle(frame, (x, y), (x + w, y + h), + (0, 255, 0), 2) + img_name_draw = d + '_' + str(i) + 'draw.jpg' + cv2.imwrite(os.path.join(train_img_path, img_name_draw), + draw_frame) + + img_info = { + 'file_name': img_name, + 'height': height, + 'width': width, + 'id': train_img_id + } + ann_info = { + 'area': float(w) * float(h), + 'iscrowd': 0, + 'bbox': [float(x), + float(y), + float(w), + float(h)], + 'category_id': category_id, + 'ignore': 0, + 'image_id': train_img_id, + 'id': train_img_id + 1 + } + train_info['images'].append(img_info) + train_info['annotations'].append(ann_info) + train_img_id += 1 + i += 1 + else: + print('finish {}'.format(d)) + break + +with open('annotations/train.json', 'w') as f: + json.dump(train_info, f) +with open('annotations/val.json', 'w') as f: + json.dump(val_info, f) diff --git a/applications/Anti-UAV/ppyolo_r50vd_dcn_1x_antiuav.yml b/applications/Anti-UAV/ppyolo_r50vd_dcn_1x_antiuav.yml new file mode 100644 index 0000000000000000000000000000000000000000..29357b7210f467d8a09e437c49d2306236a621c9 --- /dev/null +++ b/applications/Anti-UAV/ppyolo_r50vd_dcn_1x_antiuav.yml @@ -0,0 +1,10 @@ +_BASE_: [ + '../datasets/anti_uav.yml', + '../runtime.yml', + './_base_/ppyolo_r50vd_dcn_antiuav.yml', + './_base_/optimizer_1x.yml', + './_base_/ppyolo_reader.yml', +] + +snapshot_epoch: 16 +weights: output/ppyolo_r50vd_dcn_1x_antiuav/model_final diff --git a/applications/Anti-UAV/ppyolo_r50vd_dcn_antiuav.yml b/applications/Anti-UAV/ppyolo_r50vd_dcn_antiuav.yml new file mode 100644 index 0000000000000000000000000000000000000000..aca5d9a38d896887648a2a612f032c6de634bf4b --- /dev/null +++ b/applications/Anti-UAV/ppyolo_r50vd_dcn_antiuav.yml @@ -0,0 +1,66 @@ +architecture: YOLOv3 +pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/ResNet50_vd_ssld_pretrained.pdparams +norm_type: sync_bn +use_ema: true +ema_decay: 0.9998 + +YOLOv3: + backbone: ResNet + neck: PPYOLOFPN + yolo_head: YOLOv3Head + post_process: BBoxPostProcess + +ResNet: + depth: 50 + variant: d + return_idx: [1, 2, 3] + dcn_v2_stages: [3] + freeze_at: -1 + freeze_norm: false + norm_decay: 0. + +PPYOLOFPN: + coord_conv: true + drop_block: true + block_size: 3 + keep_prob: 0.9 + spp: true + +YOLOv3Head: + anchors: [[6, 6], [10, 10], [15, 16], + [23, 13], [21, 19], [32, 16], + [31, 27], [44, 20], [52, 36]] + anchor_masks: [[6, 7, 8], [3, 4, 5], [0, 1, 2]] + loss: YOLOv3Loss + iou_aware: true + iou_aware_factor: 0.4 + +YOLOv3Loss: + ignore_thresh: 0.7 + downsample: [32, 16, 8] + label_smooth: false + scale_x_y: 1.05 + iou_loss: IouLoss + iou_aware_loss: IouAwareLoss + +IouLoss: + loss_weight: 2.5 + loss_square: true + +IouAwareLoss: + loss_weight: 1.0 + +BBoxPostProcess: + decode: + name: YOLOBox + conf_thresh: 0.01 + downsample_ratio: 32 + clip_bbox: true + scale_x_y: 1.05 + nms: + name: MatrixNMS + keep_top_k: 100 + score_threshold: 0.01 + post_threshold: 0.01 + nms_top_k: -1 + background_label: -1 diff --git a/applications/BasketballAction/README.md b/applications/BasketballAction/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f531e744c0277bed357db9f9c0a49eb57eacd654 --- /dev/null +++ b/applications/BasketballAction/README.md @@ -0,0 +1,389 @@ +# 篮球动作检测模型 + + +## 内容 +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型评估](#模型评估) +- [模型推理](#模型推理) +- [模型优化](#模型优化) +- [模型部署](#模型部署) +- [参考论文](#参考论文) + + +## 模型简介 +该代码库用于篮球动作检测+识别, 基于paddle2.0版本开发,结合PaddleVideo中的ppTSM, BMN, attentionLSTM的多个视频模型进行视频时空二阶段检测算法。 +主要分为如下几步 + - 特征抽取 + - 图像特性,ppTSM + - 音频特征,Vggsound + - proposal提取,BMN + - LSTM,动作分类 + 回归 + + +## 数据准备 +数据集处理代码 +``` +参考https://github.com/PaddlePaddle/PaddleVideo/tree/application/FootballAction/datasets +``` + +- 数据集label格式 +``` +{ + "0": "背景", + "1": "回放", + "2": "进球-三分球", + "3": "进球-两分球", + "4": "进球-扣篮", + "5": "罚球", + "6": "跳球" +} +``` + +- 数据集gts处理, 将原始标注数据处理成如下json格式 +``` +{ + 'fps': 5, + 'gts': [ + { + 'url': 'xxx.mp4', + 'total_frames': 6341, + 'actions': [ + { + "label_ids": [6], + "label_names": ["跳球"], + "start_id": 395, + "end_id": 399 + }, + ... + ] + }, + ... + ] +} +``` + +- 数据集抽帧, 由mp4, 得到frames和pcm, 这里需要添加ffmpeg环境 +``` +cd datasets/script && python get_frames_pcm.py +``` + +- 数据预处理后保存格式如下 +``` + |-- datasets # 训练数据集和处理脚本 + |-- basketball # xx数据集 + |-- mp4 # 原始视频.mp4 + |-- frames # 图像帧, fps=5, '.jpg'格式 + |-- pcm # 音频pcm, 音频采样率16000,采用通道数1 + |-- url.list # 视频列表 + |-- label_train.json # 训练集原始gts + |-- label_val.json # 验证集原始gts +``` + + +## 模型训练 +代码参考足球动作检测:https://github.com/PaddlePaddle/PaddleVideo/tree/application/FootballAction + +将该代码库的文件夹 [datasets](https://github.com/PaddlePaddle/PaddleVideo/tree/application/FootballAction/datasets),[extractor](https://github.com/PaddlePaddle/PaddleVideo/tree/application/FootballAction/extractor),[train_lstm](https://github.com/PaddlePaddle/PaddleVideo/tree/application/FootballAction/train_lstm), 拷贝到本代码库复用。 + + - image 采样频率fps=5,如果有些动作时间较短,可以适当提高采样频率 + - BMN windows=200,即40s,所以测试自己的数据时,视频时长需大于40s + +### 基础镜像 +``` +docker pull tmtalgo/paddleaction:action-detection-v2 +``` + +### step1 ppTSM训练 +我们提供了篮球数据训练的模型,参考checkpoints_basketball。如果使用提供的pptsm模型,可直接跳过下边的pptsm训练数据处理和训练步骤。 +如果需要在自己的数据上训练,ppTSM训练代码为:https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +ppTSM文档参考:https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/model_zoo/recognition/pp-tsm.md + +#### step1.1 ppTSM 训练数据处理 +由frames结合gts生成训练所需要的正负样本 +``` +cd ${BasketballAction} +cd datasets/script && python get_instance_for_tsn.py + +# 文件名按照如下格式 +'{}_{}_{}_{}'.format(video_basename, start_id, end_id, label) +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- basketball # xx数据集 + |-- input_for_tsn # tsn/tsm训练的数据 +``` + +#### step1.2 ppTSM模型训练 +``` +# https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +cd ${PaddleVideo} +# 修改config.yaml参数修改为 ${BasketballAcation}/configs_train/pptsm_basketball.yaml +python -B -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + --log_dir=$save_dir/logs \ + main.py \ + --validate \ + -c {BasketballAcation}/configs_train/pptsm_basketball.yaml \ + -o output_dir=$save_dir +``` + +#### step1.3 ppTSM模型转为预测模式 +``` +# https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +$cd {PaddleVideo} +python tools/export_model.py -c ${BasketballAcation}/configs_train/pptsm_basketball.yaml \ + -p ${pptsm_train_dir}/checkpoints/models_pptsm/ppTSM_epoch_00057.pdparams \ + -o {BasketballAcation}/checkpoints/ppTSM +``` + +#### step1.4 基于ppTSM视频特征提取 +image and audio特征提取,保存到datasets features文件夹下 +``` +cd ${BasketballAcation} +cd extractor && python extract_feat.py +# 特征维度, image(2048) + audio(1024) + pcm(640) +# 特征保存格式如下,将如下dict保存在pkl格式,用于接下来的BMN训练 +video_features = {'image_feature': np_image_features, + 'audio_feature': np_audio_features + 'pcm_feature': np_pcm_features} +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- basketball # xx数据集 + |-- features # 视频的图像+音频特征 +``` + + +### step2 BMN训练 +BMN训练代码为:https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +BMN文档参考:https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/model_zoo/localization/bmn.md + +#### step2.1 BMN训练数据处理 +用于提取二分类的proposal,windows=40,根据gts和特征得到BMN训练所需要的数据集 +``` +cd ${BasketballAcation} +cd datasets/script && python get_instance_for_bmn.py +# 数据格式 +{ + "719b0a4bcb1f461eabb152298406b861_753_793": { + "duration_second": 40.0, + "duration_frame": 200, + "feature_frame": 200, + "subset": "train", + "annotations": [ + { + "segment": [ + 15.0, + 22.0 + ], + "label": "6.0", + "label_name": "跳球" + } + ] + }, + ... +} +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- basketball # xx数据集 + |-- input_for_bmn # bmn训练的proposal +``` + +#### step2.2 BMN模型训练 +``` +# https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +cd ${PaddleVideo} +# 修改config.yaml参数修为${BasketballAcation}/configs_train/bmn_basketball.yaml +python -B -m paddle.distributed.launch \ + --gpus="0,1" \ + --log_dir=$out_dir/logs \ + main.py \ + --validate \ + -c ${BasketballAcation}/configs_train/bmn_basketball.yaml \ + -o output_dir=$out_dir +``` + +#### step2.3 BMN模型转为预测模式 +``` +# https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +${PaddleVideo} +python tools/export_model.py -c $${BasketballAcation}/configs_train/bmn_basketball.yaml \ + -p ${bmn_train_dir}/checkpoints/models_bmn/bmn_epoch16.pdparams \ + -o {BasketballAcation}/checkpoints/BMN +``` + +#### step2.4 BMN模型预测 +得到动作proposal信息: start_id, end_id, score +``` +cd ${BasketballAcation} +cd extractor && python extract_bmn.py +# 数据格式 +[ + { + "video_name": "c9516c903de3416c97dae91a59e968d7", + "num_proposal": 5534, + "bmn_results": [ + { + "start": 7850.0, + "end": 7873.0, + "score": 0.77194699622342 + }, + { + "start": 4400.0, + "end": 4443.0, + "score": 0.7663803287641536 + }, + ... + ] + }, + ... +] +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- basketball # xx数据集 + |-- feature_bmn + |-- prop.json # bmn 预测结果 +``` + +### step3 LSTM训练 +LSTM训练代码为:train_lstm + +#### step3.1 LSTM训练数据处理 +将BMN得到的proposal截断并处理成LSTM训练所需数据集 +``` +cd ${BasketballAcation} +cd datasets/script && python get_instance_for_lstm.py +# 数据格式1,label_info +{ + "fps": 5, + "results": [ + { + "url": "https://xxx.mp4", + "mode": "train", # train or validation + "total_frames": 6128, + "num_gts": 93, + "num_proposals": 5043, + "proposal_actions": [ + { + "label": 6, + "norm_iou": 0.7575757575757576, + "norm_ioa": 0.7575757575757576, + "norm_start": -0.32, + "proposal": { + "start": 5011, + "end": 5036, + "score": 0.7723643666324231 + }, + "hit_gts": { + "label_ids": [ + 6 + ], + "label_names": [ + "跳球" + ], + "start_id": 5003, + "end_id": 5036 + } + }, + ... + }, + ... +} +# 数据格式2,LSTM训练所需要的feature +{ + 'features': np.array(feature_hit, dtype=np.float32), # TSM audio and pcm 特征, 可根据需求选择组合 + 'feature_fps': 5, # fps = 5 + 'label_info': {'norm_iou': 0.5, 'label': 3, ...}, # 数据格式1中的'proposal_actions' + 'video_name': 'c9516c903de3416c97dae91a59e968d7' # video_name +} +# 数据格式3,LSTM训练所需label.txt +'{} {}'.format(filename, label) +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- basketball # xx数据集 + |-- input_for_lstm # LSTM训练数据集 +``` + +#### step3.2 LSTM训练 +``` +#conf.yaml修改为 ${BasketballAcation}/configs_train/lstm_basketball.yaml +cd ${BasketballAcation} +python -u scenario_lib/train.py \ + --model_name=ActionNet \ + --config=${BasketballAcation}/configs_train/lstm_basketball.yaml \ + --save_dir=${out_dir}"/models_lstm/" \ + --log_interval=5 \ + --valid_interval=1 +``` + +#### step3.3 LSTM模型转为预测模式 +``` +${BasketballAcation} +python tools/export_model.py -c ${BasketballAction}/train_lstm/conf/conf.yaml \ + -p ${lstm_train_dir}/checkpoints/models_lstm/bmn_epoch29.pdparams \ + -o {BasketballAcation}/checkpoints/LSTM +``` + + +## 模型推理 +测试数据格式,可参考使用样例 +``` +wget https://bj.bcebos.com/v1/acg-algo/PaddleVideo_application/basketball/datasets.tar.gz +``` +测试模型,可使用我们提供的模型 +``` +wget https://bj.bcebos.com/v1/acg-algo/PaddleVideo_application/basketball/checkpoints_basketball.tar.gz +``` +运行预测代码 +``` +cd ${BasketballAction} +cd predict +# 如果使用自己训练的模型,请将各训练过程中转换的inference模型放到predict库 +# cp -rf ../checkpoints checkpoints_basketball +python predict.py +``` +产出文件 +``` +${BasketballAction}/predict/results.json +``` + + +## 模型评估 +``` +cd ${BasketballAction} +cd predict +python eval.py results.json +``` + + +## 模型优化 +在实际使用场景中可根据视频内容尝试优化策略 +- 可根据动作运动速度,调整抽帧采样率,本代码默认为fps=5 +- 统计动作的时间分布,调整bmn采样窗口 +- 根据图像和音频的关联程度,调整图像和音频特征的融合方式:本代码将图像特征和音频在时间维度对齐,融合后再进入模型训练。也可尝试分别模型训练后,加权融合等 +- 本代码的解决方案也可用于其他动作检测。变换场景后,图像特征重新训练效果更好。音频特征采用的VGGSound训练,如果使用场景仍为生活场景,可直接复用。 + + +## 模型部署 +本代码解决方案在动作的检测和召回指标F1-score=80.14% +
+
+
+ + +## 参考论文 + +- [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf), Ji Lin, Chuang Gan, Song Han +- [BMN: Boundary-Matching Network for Temporal Action Proposal Generation](https://arxiv.org/abs/1907.09702), Tianwei Lin, Xiao Liu, Xin Li, Errui Ding, Shilei Wen. +- [Attention Clusters: Purely Attention Based Local Feature Integration for Video Classification](https://arxiv.org/abs/1711.09550), Xiang Long, Chuang Gan, Gerard de Melo, Jiajun Wu, Xiao Liu, Shilei Wen +- [YouTube-8M: A Large-Scale Video Classification Benchmark](https://arxiv.org/abs/1609.08675), Sami Abu-El-Haija, Nisarg Kothari, Joonseok Lee, Paul Natsev, George Toderici, Balakrishnan Varadarajan, Sudheendra Vijayanarasimhan diff --git a/applications/BasketballAction/configs_train/bmn_basketball.yaml b/applications/BasketballAction/configs_train/bmn_basketball.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d915126955466c4eb2adeddabf2b383c41fe4642 --- /dev/null +++ b/applications/BasketballAction/configs_train/bmn_basketball.yaml @@ -0,0 +1,101 @@ +MODEL: #MODEL field + framework: "BMNLocalizer" + backbone: + name: "BMN" + feat_dim: 2688 + tscale: 200 + dscale: 200 + prop_boundary_ratio: 0.5 + num_sample: 32 + num_sample_perbin: 3 + loss: + name: "BMNLoss" + tscale: 200 + dscale: 200 + +DATASET: #DATASET field + batch_size: 6 #single card bacth size + test_batch_size: 1 + num_workers: 8 + train: + format: "BMNDataset" + file_path: "dataset/basketball/tsm_bmn_lstm/train/train_list/basketball_bmn_label.json" + subset: "train" + valid: + format: "BMNDataset" + file_path: "dataset/basketball/tsm_bmn_lstm/train/train_list/basketball_bmn_label.json" + subset: "validation" + test: + format: "BMNDataset" + test_mode: True + file_path: "dataset/basketball/tsm_bmn_lstm/train/train_list/basketball_bmn_label.json" + subset: "validation" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal data + load_feat: + name: "LoadFeat" + feat_path: "dataset/basketball/tsm_bmn_lstm/data/basketball-20210622-200/input_for_bmn/feature/" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + + valid: #Mandotary, indicate the pipeline to deal data + load_feat: + name: "LoadFeat" + feat_path: "dataset/basketball/tsm_bmn_lstm/data/basketball-20210622-200/input_for_bmn/feature/" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + + test: #Mandatory, indicate the pipeline to deal data + load_feat: + name: "LoadFeat" + feat_path: "dataset/basketball/tsm_bmn_lstm/data/basketball-20210622-200/input_for_bmn/feature/" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + iter_step: True + name: 'CustomPiecewiseDecay' + boundaries: [8000] + values: [0.001, 0.0001] + weight_decay: + name: 'L2' + value: 1e-4 + +METRIC: + name: 'BMNMetric' + tscale: 200 + dscale: 200 + file_path: "data/dataset/bmn_data/activitynet_1.3_annotations.json" + ground_truth_filename: "data/dataset/bmn_data/activity_net_1_3_new.json" + subset: "validation" + output_path: "data/bmn/BMN_Test_output" + result_path: "data/bmn/BMN_Test_results" + + +INFERENCE: + name: 'BMN_Inference_helper' + feat_dim: 2688 + dscale: 200 + tscale: 200 + result_path: "data/bmn/BMN_INFERENCE_results" + + +model_name: BMN +epochs: 20 #Mandatory, total epoch +log_level: "INFO" +resume_from: "" #checkpoint path. diff --git a/applications/BasketballAction/configs_train/lstm_basketball.yaml b/applications/BasketballAction/configs_train/lstm_basketball.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b25c51f367705cb660898a59d81c753159e3df2b --- /dev/null +++ b/applications/BasketballAction/configs_train/lstm_basketball.yaml @@ -0,0 +1,36 @@ +[MODEL] +name = "ActionNet" +dataset = "Actiondata" +bone_nework = None +modelbn_min_everygpu_bs = 10 +drop_rate = 0.5 +feature_num = 1 +feature_names = ['rgb', 'audio'] +feature_dims = [2688, 1024] # [2688, 640] +embedding_size = 512 +lstm_size_img = 512 +lstm_size_audio = 128 +num_classes = 7 +save_dir = "." + +[TRAIN] +epoch = 30 +learning_rate = 0.0007 +decay_gamma = 0.2 +l2_weight_decay = 8e-4 +decay_epochs = [5,10,15,20] +num_samples = 397885 +batch_size = 1200 +droplast = False +use_gpu = True +num_gpus = 4 +filelist = "dataset/basketball/tsm_bmn_lstm/train/train_list/basketball_lstm_train.list" +[VALID] +batch_size = 16 +num_samples = 20902 +droplast = True +filelist = "dataset/basketball/tsm_bmn_lstm/train/train_list/basketball_lstm_train.list" +[INFER] +batch_size = 1 +droplast = True +filelist = "data_demo/batch_val/val.list" diff --git a/applications/BasketballAction/configs_train/pptsm_basketball.yaml b/applications/BasketballAction/configs_train/pptsm_basketball.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8c82903d9bfd4ae4344bebaaab7eb9cfefd5f85b --- /dev/null +++ b/applications/BasketballAction/configs_train/pptsm_basketball.yaml @@ -0,0 +1,124 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network. + backbone: #Mandatory, indicate the type of backbone. + name: "ResNetTweaksTSM" #Mandatory, The name of backbone. + pretrained: "dataset/basketball/tsm_bmn_lstm/train/pretrain/ResNet50_vd_ssld_v2_pretrained.pdparams" + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "ppTSMHead" #Mandatory, indicate the type of head + num_classes: 7 #101 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset + data_prefix: "" #Mandatory, train data root path + file_path: "dataset/basketball/tsm_bmn_lstm/train/train_list/basketball_tsn_train.list" + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset + data_prefix: "" #Mandatory, valid data root path + file_path: "dataset/basketball/tsm_bmn_lstm/train/train_list/basketball_tsn_val.list" + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset + data_prefix: "" #Mandatory, valid data root path + file_path: "dataset/basketball/tsm_bmn_lstm/train/train_list/basketball_tsn_val.list" + suffix: 'img_{:05}.jpg' + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + - MultiScaleCrop: + target_size: 256 + - RandomCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + valid: #Mandatory, indicate the pipeline to deal with the validing data + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + test: #Mandatory, indicate the pipeline to deal with the validing data + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 80 + warmup_epochs: 10 + warmup_start_lr: 0.005 + cosine_base_lr: 0.01 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + +MIX: + name: "Mixup" + alpha: 0.2 + +PRECISEBN: + preciseBN_interval: 5 # epoch interval to do preciseBN, default 1. + num_iters_preciseBN: 200 # how many batches used to do preciseBN, default 200. + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "ppTSM" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 80 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/applications/BasketballAction/images/BasketballAction_demo.gif b/applications/BasketballAction/images/BasketballAction_demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..ede46636b2afd3b5fcbe36b95c5794d8a2e66d7c Binary files /dev/null and b/applications/BasketballAction/images/BasketballAction_demo.gif differ diff --git a/applications/BasketballAction/predict/action_detect/action.py b/applications/BasketballAction/predict/action_detect/action.py new file mode 100644 index 0000000000000000000000000000000000000000..4bca40ad55805c133ab1a3b80518526a89c87370 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/action.py @@ -0,0 +1,175 @@ +#!./python27-gcc482/bin/python +# coding: utf-8 +""" +BAIDU CLOUD action +""" + +import os +import sys +import pickle +import json +import time +import functools + +import numpy as np + +from utils.preprocess import get_images +from utils.config_utils import parse_config, print_configs +import mfcc.feature_extractor as mfcc_extractor + +import models.pptsm_infer as image_model +import models.audio_infer as audio_model +import models.bmn_infer as prop_model +import models.lstm_infer as classify_model + +import logger +logger = logger.Logger() + +def record_time_info(func): + """decorator func to log cost time for func + """ + @functools.wraps(func) + def timer(*args): + """log cost time for func + """ + logger.info("function [{}] processing ...".format(func.__name__)) + start_time = time.time() + retval = func(*args) + cost_time = round(time.time() - start_time, 5) + logger.info("function [{}] run time: {:.2f} min".format(func.__name__, cost_time / 60)) + return retval + return timer + + +class ActionDetection(object): + """ModelPredict""" + def __init__(self, cfg_file="configs/configs.yaml"): + cfg = parse_config(cfg_file) + self.configs = cfg + print_configs(self.configs, "Infer") + + name = 'COMMON' + self.DEBUG = cfg[name]['DEBUG'] + self.BMN_ONLY = cfg[name]['BMN_ONLY'] + self.LSTM_ONLY = cfg[name]['LSTM_ONLY'] + self.PCM_ONLY = cfg[name]['PCM_ONLY'] + if self.LSTM_ONLY: + self.prop_dict = {} + for dataset in ['EuroCup2016']: + prop_json = '/home/work/datasets/{}/feature_bmn/prop.json'.format(dataset) + json_data = json.load(open(prop_json, 'r')) + for item in json_data: + basename = prop_json.replace('feature_bmn/prop.json', 'mp4') + basename = basename + '/' + item['video_name'] + '.mp4' + self.prop_dict[basename] = item['bmn_results'] + + + @record_time_info + def load_model(self): + """ + load_model + """ + if not self.DEBUG: + self.image_model = image_model.InferModel(self.configs) + if not self.PCM_ONLY: + self.audio_model = audio_model.InferModel(self.configs) + + if not self.LSTM_ONLY: + self.prop_model = prop_model.InferModel(self.configs) + + if not self.BMN_ONLY: + self.classify_model = classify_model.InferModel(self.configs) + + logger.info("==> Action Detection prepared.") + + @record_time_info + def infer(self, imgs_path, pcm_path, fps=5): + """ + extract_feature + """ + print("imgs_path = ", imgs_path) + self.imgs_path = imgs_path + self.pcm_path = pcm_path + self.configs['COMMON']['fps'] = fps + + logger.info("==> input video {}".format(os.path.basename(self.imgs_path))) + + # step 1: extract feature + video_features = self.extract_feature() + + # step2: get proposal + bmn_results = self.extract_proposal(video_features) + + # step3: classify + material = {'feature': video_features, 'proposal': bmn_results} + action_results = self.video_classify(material) + + return bmn_results, action_results + + @record_time_info + def video_classify(self, material): + """video classify""" + if self.BMN_ONLY: + return [] + action_results = self.classify_model.predict(self.configs, material=material) + logger.info('action shape {}'.format(np.array(action_results).shape)) + return action_results + + @record_time_info + def extract_proposal(self, video_features): + """extract proposal""" + if self.LSTM_ONLY: + basename = self.imgs_path.replace('frames', 'mp4') + '.mp4' + bmn_results = self.prop_dict[basename] + return bmn_results + bmn_results = self.prop_model.predict(self.configs, material=video_features) + logger.info('proposal shape {}'.format(np.array(bmn_results).shape)) + return bmn_results + + @record_time_info + def extract_feature(self): + """extract feature""" + if not self.DEBUG: + image_path_list = get_images(self.imgs_path) + self.configs['PPTSM']['frame_list'] = image_path_list + self.configs['AUDIO']['pcm_file'] = self.pcm_path + image_features = self.image_model.predict(self.configs) + if self.PCM_ONLY: + sample_rate = self.configs['AUDIO']['sample_rate'] + pcm_features = mfcc_extractor.extract_pcm(self.pcm_path, sample_rate) + audio_features = [] + else: + audio_features, pcm_features = self.audio_model.predict(self.configs) + + np_image_features = np.array(image_features, dtype=np.float32) + np_audio_features = np.array(audio_features, dtype=np.float32) + np_pcm_features = np.array(pcm_features, dtype=np.float32) + + video_features = {'image_feature': np_image_features, + 'audio_feature': np_audio_features, + 'pcm_feature': np_pcm_features} + else: + feature_path = self.imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + logger.info("feature shape {} {} {}".format(video_features['image_feature'].shape, + video_features['audio_feature'].shape, + video_features['pcm_feature'].shape)) + + return video_features + +if __name__ == '__main__': + + model_predict = ActionDetection(cfg_file="../configs/configs.yaml") + model_predict.load_model() + + imgs_path = "/home/work/datasets/EuroCup2016/frames/1be705a8f67648da8ec4b4296fa80895" + pcm_path = "/home/work/datasets/EuroCup2016/pcm/1be705a8f67648da8ec4b4296fa80895.pcm" + + bmn_results, action_results = model_predict.infer(imgs_path, pcm_path) + results = {'bmn_results': bmn_results, 'action_results': action_results} + + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + diff --git a/applications/BasketballAction/predict/action_detect/logger.py b/applications/BasketballAction/predict/action_detect/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..b03348721df29fdf6cbd22954b718cec2b83efc2 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/logger.py @@ -0,0 +1,24 @@ +""" +logger +""" +import os +import logging + +class Logger(logging.Logger): + """Customized logger for news stripper + """ + def __init__(self): + super(Logger, self).__init__(self) + if not os.path.exists('logs'): + os.mkdir('logs') + handler = logging.FileHandler("logs/action_detect.log") + # handler.setLevel(logging.DEBUG) + handler.setLevel(logging.INFO) + + format = "%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d %(message)s" + datefmt = "%y-%m-%d %H:%M:%S" + + formatter = logging.Formatter(format, datefmt) + handler.setFormatter(formatter) + self.addHandler(handler) + diff --git a/applications/BasketballAction/predict/action_detect/mfcc/__init__.py b/applications/BasketballAction/predict/action_detect/mfcc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/BasketballAction/predict/action_detect/mfcc/feature_extractor.py b/applications/BasketballAction/predict/action_detect/mfcc/feature_extractor.py new file mode 100755 index 0000000000000000000000000000000000000000..43b1100465aae999c1a413f851ee672640f3e574 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/mfcc/feature_extractor.py @@ -0,0 +1,158 @@ +""" +audio feature extract +""" +# coding: utf-8 +import os +import numpy as np +import pickle +import mfcc.vgg_params as vgg_params + + +def frame(data, window_length, hop_length): + """ + frame + """ + num_samples = data.shape[0] + num_frames = 1 + int(np.floor((num_samples - window_length) / hop_length)) + shape = (num_frames, window_length) + data.shape[1:] + strides = (data.strides[0] * hop_length, ) + data.strides + return np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides) + + +def periodic_hann(window_length): + """ + periodic_hann + """ + return 0.5 - (0.5 * + np.cos(2 * np.pi / window_length * np.arange(window_length))) + + +def stft_magnitude(signal, fft_length, hop_length=None, window_length=None): + """ + stft_magnitude + """ + frames = frame(signal, window_length, hop_length) + window = periodic_hann(window_length) + windowed_frames = frames * window + return np.abs(np.fft.rfft(windowed_frames, int(fft_length))) + + +_MEL_BREAK_FREQUENCY_HERTZ = 700.0 +_MEL_HIGH_FREQUENCY_Q = 1127.0 + + +def hertz_to_mel(frequencies_hertz): + """ + hertz_to_mel + """ + return _MEL_HIGH_FREQUENCY_Q * np.log(1.0 + (frequencies_hertz / + _MEL_BREAK_FREQUENCY_HERTZ)) + + +def spectrogram_to_mel_matrix(num_mel_bins=20, + num_spectrogram_bins=129, + audio_sample_rate=8000, + lower_edge_hertz=125.0, + upper_edge_hertz=3800.0): + """ + spectrogram_to_mel_matrix + """ + nyquist_hertz = audio_sample_rate / 2. + if lower_edge_hertz >= upper_edge_hertz: + raise ValueError("lower_edge_hertz %.1f >= upper_edge_hertz %.1f" % + (lower_edge_hertz, upper_edge_hertz)) + spectrogram_bins_hertz = np.linspace(0.0, nyquist_hertz, + num_spectrogram_bins) + spectrogram_bins_mel = hertz_to_mel(spectrogram_bins_hertz) + band_edges_mel = np.linspace(hertz_to_mel(lower_edge_hertz), + hertz_to_mel(upper_edge_hertz), + num_mel_bins + 2) + mel_weights_matrix = np.empty((num_spectrogram_bins, num_mel_bins)) + for i in range(num_mel_bins): + lower_edge_mel, center_mel, upper_edge_mel = band_edges_mel[i:i + 3] + lower_slope = ((spectrogram_bins_mel - lower_edge_mel) / + (center_mel - lower_edge_mel)) + upper_slope = ((upper_edge_mel - spectrogram_bins_mel) / + (upper_edge_mel - center_mel)) + mel_weights_matrix[:, + i] = np.maximum(0.0, + np.minimum(lower_slope, upper_slope)) + mel_weights_matrix[0, :] = 0.0 + return mel_weights_matrix + + +def log_mel_spectrogram(data, + audio_sample_rate=8000, + log_offset=0.0, + window_length_secs=0.025, + hop_length_secs=0.010, + **kwargs): + """ + log_mel_spectrogram + """ + window_length_samples = int(round(audio_sample_rate * window_length_secs)) + hop_length_samples = int(round(audio_sample_rate * hop_length_secs)) + fft_length = 2**int(np.ceil(np.log(window_length_samples) / np.log(2.0))) + spectrogram = stft_magnitude(data, + fft_length=fft_length, + hop_length=hop_length_samples, + window_length=window_length_samples) + mel_spectrogram = np.dot( + spectrogram, + spectrogram_to_mel_matrix(num_spectrogram_bins=spectrogram.shape[1], + audio_sample_rate=audio_sample_rate, + **kwargs)) + + return np.log(mel_spectrogram + log_offset) + + +def wav_to_example(wav_data, sample_rate): + """ + wav_to_example + """ + assert wav_data.dtype == np.int16, 'Bad sample type: %r' % wav_data.dtype + pad_zero_num = int(sample_rate * (vgg_params.STFT_WINDOW_LENGTH_SECONDS - + vgg_params.STFT_HOP_LENGTH_SECONDS)) + wav_data_extend = np.hstack((wav_data, np.zeros(pad_zero_num))) + wav_data = wav_data_extend + wav_data = wav_data / 32768.0 # Convert to [-1.0, +1.0] + if len(wav_data.shape) > 1: + wav_data = np.mean(wav_data, axis=1) + log_mel = log_mel_spectrogram( + wav_data, + audio_sample_rate=vgg_params.SAMPLE_RATE, + log_offset=vgg_params.LOG_OFFSET, + window_length_secs=vgg_params.STFT_WINDOW_LENGTH_SECONDS, + hop_length_secs=vgg_params.STFT_HOP_LENGTH_SECONDS, + num_mel_bins=vgg_params.NUM_MEL_BINS, + lower_edge_hertz=vgg_params.MEL_MIN_HZ, + upper_edge_hertz=vgg_params.MEL_MAX_HZ) + # Frame features into examples. + features_sample_rate = 1.0 / vgg_params.STFT_HOP_LENGTH_SECONDS + example_window_length = int( + round(vgg_params.EXAMPLE_WINDOW_SECONDS * features_sample_rate)) + + example_hop_length = int( + round(vgg_params.EXAMPLE_HOP_SECONDS * features_sample_rate)) + log_mel_examples = frame(log_mel, + window_length=example_window_length, + hop_length=example_hop_length) + return log_mel_examples + + +def extract_pcm(pcm_file, sample_rate): + with open(pcm_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + examples = wav_to_example(audio_data, sample_rate) + return examples + + +if __name__ == "__main__": + wav_file = sys.argv[1] + print("wav_file = ", wav_file) + with open(wav_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype = np.int16) + examples_batch = wav_to_example(audio_data, 16000) + print("examples_batch.shape", examples_batch.shape) diff --git a/applications/BasketballAction/predict/action_detect/mfcc/model_config.py b/applications/BasketballAction/predict/action_detect/mfcc/model_config.py new file mode 100644 index 0000000000000000000000000000000000000000..194365ece7545bd469c8ad23a46e7e461ac29edf --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/mfcc/model_config.py @@ -0,0 +1,51 @@ +""" +audio model config +""" +import numpy as np + +import mfcc.feature_extractor as feature_extractor + + +class ModelAudio(object): + """ + modelAudio + """ + def __init__(self, configs, use_gpu=1): + self.use_gpu = use_gpu + + self.audio_fps = configs.COMMON.fps + self.audio_feat_scale = configs.TSN.audio_scale + self.sample_rate = 16000 + + def predict_slice(self, wav_data, sample_rate): + """ + audio predict + """ + examples_batch = feature_extractor.wav_to_example( + wav_data, sample_rate)[0] + return examples_batch + + def predict_audio(self, audio_file): + """ + predict_audio + """ + audio_feature_list = [] + # read pcm + sample_rate = self.sample_rate + try: + with open(audio_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + audio_status = "audio load success" + except Exception as e: + audio_data = [] + audio_status = "audio load failed" + step = 1 + len_video = int(len(audio_data) / sample_rate) + print(len_video) + for i in range(0, len_video, step): + audio_data_part = audio_data[i * sample_rate:(i + step) * + sample_rate] + feature_audio = self.predict_slice(audio_data_part, sample_rate) + audio_feature_list.append(feature_audio) + return audio_feature_list diff --git a/applications/BasketballAction/predict/action_detect/mfcc/vgg_params.py b/applications/BasketballAction/predict/action_detect/mfcc/vgg_params.py new file mode 100755 index 0000000000000000000000000000000000000000..0a995196103f12e1d975d4d7ee7eaa122a19fb5f --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/mfcc/vgg_params.py @@ -0,0 +1,37 @@ +"""Global parameters for the VGGish model. +See vggish_slim.py for more information. +""" + +# Architectural constants. +NUM_FRAMES = 50 # Frames in input mel-spectrogram patch. +NUM_BANDS = 64 # Frequency bands in input mel-spectrogram patch. +EMBEDDING_SIZE = 128 # Size of embedding layer. + +# Hyperparameters used in feature and example generation. +SAMPLE_RATE = 16000 +STFT_WINDOW_LENGTH_SECONDS = 0.040 +STFT_HOP_LENGTH_SECONDS = 0.020 +NUM_MEL_BINS = NUM_BANDS +MEL_MIN_HZ = 125 +MEL_MAX_HZ = 7500 +LOG_OFFSET = 0.01 # Offset used for stabilized log of input mel-spectrogram. +EXAMPLE_WINDOW_SECONDS = 1.00 # Each example contains 96 10ms frames +EXAMPLE_HOP_SECONDS = 1.00 # with zero overlap. + +# Parameters used for embedding postprocessing. +PCA_EIGEN_VECTORS_NAME = 'pca_eigen_vectors' +PCA_MEANS_NAME = 'pca_means' +QUANTIZE_MIN_VAL = -2.0 +QUANTIZE_MAX_VAL = +2.0 + +# Hyperparameters used in training. +INIT_STDDEV = 0.01 # Standard deviation used to initialize weights. +LEARNING_RATE = 1e-4 # Learning rate for the Adam optimizer. +ADAM_EPSILON = 1e-8 # Epsilon for the Adam optimizer. + +# Names of ops, tensors, and features. +INPUT_OP_NAME = 'vggish/input_features' +INPUT_TENSOR_NAME = INPUT_OP_NAME + ':0' +OUTPUT_OP_NAME = 'vggish/embedding' +OUTPUT_TENSOR_NAME = OUTPUT_OP_NAME + ':0' +AUDIO_EMBEDDING_FEATURE_NAME = 'audio_embedding' diff --git a/applications/BasketballAction/predict/action_detect/models/__init__.py b/applications/BasketballAction/predict/action_detect/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/BasketballAction/predict/action_detect/models/audio_infer.py b/applications/BasketballAction/predict/action_detect/models/audio_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..7b19c90ed369572bd78ad6fa5cf619d1bda8dc61 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/models/audio_infer.py @@ -0,0 +1,80 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """audio infer""" + def __init__(self, cfg, name='AUDIO'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + self.output_tensor = self.predictor.get_output_handle(output_names[0]) + + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output = self.output_tensor.copy_to_cpu() + return output + + + def predict(self, infer_config): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config) + feature_list = [] + pcm_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = np.array(data, dtype = 'float32') + output = self.infer(inputs) + feature_list.append(np.squeeze(output)) + pcm_list.append(inputs) + feature_values = np.vstack(feature_list) + pcm_values = np.vstack(pcm_list) + return feature_values, pcm_values + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + pcm_path = '/home/work/datasets/WorldCup2018/pcm/6e577252c4004961ac7caa738a52c238.pcm' + t0 = time.time() + cfg['AUDIO']['pcm_file'] = pcm_path + outputs = model.predict(cfg) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + t1 = time.time() + + print(outputs.shape) + print(outputs[0]) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/BasketballAction/predict/action_detect/models/bmn_infer.py b/applications/BasketballAction/predict/action_detect/models/bmn_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..9e739d1afb1c5248ba8826b6fd511f4d27340377 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/models/bmn_infer.py @@ -0,0 +1,155 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import json +import pickle +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config +from utils.process_result import process_proposal + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """bmn infer""" + def __init__(self, cfg, name='BMN'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + self.nms_thread = cfg[name]['nms_thread'] + self.min_pred_score = cfg[name]['score_thread'] + self.min_frame_thread = cfg['COMMON']['fps'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + self.output1_tensor = self.predictor.get_output_handle(output_names[0]) + self.output2_tensor = self.predictor.get_output_handle(output_names[1]) + self.output3_tensor = self.predictor.get_output_handle(output_names[2]) + + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output1 = self.output1_tensor.copy_to_cpu() + output2 = self.output2_tensor.copy_to_cpu() + output3 = self.output3_tensor.copy_to_cpu() + return output1, output2, output3 + + + def generate_props(self, pred_bmn, pred_start, pred_end, max_window=200, min_window=5): + """generate_props""" + video_len = min(pred_bmn.shape[-1], min(pred_start.shape[-1], pred_end.shape[-1])) + pred_bmn = pred_bmn[0, :, :] * pred_bmn[1, :, :] + start_mask = self.boundary_choose(pred_start) + start_mask[0] = 1. + end_mask = self.boundary_choose(pred_end) + end_mask[-1] = 1. + score_results = [] + for idx in range(min_window, max_window): + for jdx in range(video_len): + start_index = jdx + end_index = start_index + idx + if end_index < video_len and start_mask[start_index] == 1 and end_mask[end_index] == 1: + xmin = start_index + xmax = end_index + xmin_score = pred_start[start_index] + xmax_score = pred_end[end_index] + bmn_score = pred_bmn[idx, jdx] + conf_score = xmin_score * xmax_score * bmn_score + score_results.append([xmin, xmax, conf_score]) + return score_results + + + def boundary_choose(self, score_list): + """boundary_choose""" + max_score = max(score_list) + mask_high = (score_list > max_score * 0.5) + score_list = list(score_list) + score_middle = np.array([0.0] + score_list + [0.0]) + score_front = np.array([0.0, 0.0] + score_list) + score_back = np.array(score_list + [0.0, 0.0]) + mask_peak = ((score_middle > score_front) & (score_middle > score_back)) + mask_peak = mask_peak[1:-1] + mask = (mask_high | mask_peak).astype('float32') + return mask + + + def predict(self, infer_config, material): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config, material=material) + feature_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = [items[0] for items in data] + winds = [items[1] for items in data] + feat_info = [items[2] for items in data] + feature_T = feat_info[0][0] + feature_N = feat_info[0][1] + + inputs = np.array(inputs) + pred_bmn, pred_sta, pred_end = self.infer(inputs) + + if infer_iter == 0: + sum_pred_bmn = np.zeros((2, feature_N, feature_T)) + sum_pred_sta = np.zeros((feature_T, )) + sum_pred_end = np.zeros((feature_T, )) + sum_pred_cnt = np.zeros((feature_T, )) + + for idx, sub_wind in enumerate(winds): + sum_pred_bmn[:, :, sub_wind[0]: sub_wind[1]] += pred_bmn[idx] + sum_pred_sta[sub_wind[0]: sub_wind[1]] += pred_sta[idx] + sum_pred_end[sub_wind[0]: sub_wind[1]] += pred_end[idx] + sum_pred_cnt[sub_wind[0]: sub_wind[1]] += np.ones((sub_wind[1] - sub_wind[0], )) + + pred_bmn = sum_pred_bmn / sum_pred_cnt + pred_sta = sum_pred_sta / sum_pred_cnt + pred_end = sum_pred_end / sum_pred_cnt + + score_result = self.generate_props(pred_bmn, pred_sta, pred_end) + results = process_proposal(score_result, self.min_frame_thread, self.nms_thread, self.min_pred_score) + + return results + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238' + + # feature + feature_path = imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + t0 = time.time() + outputs = model.predict(cfg, video_features) + t1 = time.time() + + results = {'proposal': outputs} + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/BasketballAction/predict/action_detect/models/lstm_infer.py b/applications/BasketballAction/predict/action_detect/models/lstm_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..a3acb674ed706f9a58f64e8516c7288d2ddfade1 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/models/lstm_infer.py @@ -0,0 +1,145 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import json +import pickle +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config +from utils.process_result import get_action_result + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """lstm infer""" + def __init__(self, cfg, name='ACTION'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + self.topk = cfg[name]['topk'] + self.frame_offset = cfg[name]['nms_offset'] + self.nms_thread = cfg[name]['nms_thread'] + self.cls_thread = cfg[name]['classify_score_thread'] + self.iou_thread = cfg[name]['iou_score_thread'] + + self.label_map_file = cfg['COMMON']['label_dic'] + self.fps = cfg['COMMON']['fps'] + self.nms_id = 5 + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input1_tensor = self.predictor.get_input_handle(input_names[0]) + #self.input2_tensor = self.predictor.get_input_handle(input_names[1]) + + output_names = self.predictor.get_output_names() + self.output1_tensor = self.predictor.get_output_handle(output_names[0]) + self.output2_tensor = self.predictor.get_output_handle(output_names[1]) + + + def infer(self, input1_arr, input1_lod, input2_arr=None, input2_lod=None): + """infer""" + self.input1_tensor.copy_from_cpu(input1_arr) + self.input1_tensor.set_lod(input1_lod) + if not input2_arr is None: + self.input2_tensor.copy_from_cpu(input2_arr) + self.input2_tensor.set_lod(input2_lod) + self.predictor.run() + output1 = self.output1_tensor.copy_to_cpu() + output2 = self.output2_tensor.copy_to_cpu() + # print(output.shape) + return output1, output2 + + def pre_process(self, input): + """pre process""" + input_arr = [] + input_lod = [0] + start_lod = 0 + end_lod = 0 + for sub_item in input: + end_lod = start_lod + len(sub_item) + input_lod.append(end_lod) + input_arr.extend(sub_item) + start_lod = end_lod + input_arr = np.array(input_arr) + return input_arr, [input_lod] + + def predict(self, infer_config, material): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config, material=material) + results = [] + for infer_iter, data in enumerate(infer_reader()): + video_id = [[items[-2], items[-1]] for items in data] + input1 = [items[0] for items in data] + input1_arr, input1_lod = self.pre_process(input1) + output1, output2 = self.infer(input1_arr, input1_lod) + + predictions_id = output1 + predictions_iou = output2 + for i in range(len(predictions_id)): + topk_inds = predictions_id[i].argsort()[0 - self.topk:] + topk_inds = topk_inds[::-1] + preds_id = predictions_id[i][topk_inds] + preds_iou = predictions_iou[i][0] + results.append((video_id[i], preds_id.tolist(), topk_inds.tolist(), preds_iou.tolist())) + + predict_result = get_action_result(results, self.label_map_file, self.fps, + self.cls_thread, self.iou_thread, + self.nms_id, self.nms_thread, self.frame_offset) + return predict_result + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + # proposal total + prop_dict = {} + for dataset in ['EuroCup2016', 'WorldCup2018']: + prop_json = '/home/work/datasets/{}/feature_bmn/prop.json'.format(dataset) + json_data = json.load(open(prop_json, 'r')) + for item in json_data: + basename = prop_json.replace('feature_bmn/prop.json', 'mp4') + basename = basename + '/' + item['video_name'] + '.mp4' + prop_dict[basename] = item['bmn_results'] + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238' + + # feature + feature_path = imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + # proposal + basename = imgs_path.replace('frames', 'mp4') + '.mp4' + bmn_results = prop_dict[basename] + + material = {'feature': video_features, 'proposal': bmn_results} + + t0 = time.time() + outputs = model.predict(cfg, material) + t1 = time.time() + results = {'actions': outputs} + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/BasketballAction/predict/action_detect/models/pptsm_infer.py b/applications/BasketballAction/predict/action_detect/models/pptsm_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..63f31367a03f4f81dbf268ec18a94349f4080d37 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/models/pptsm_infer.py @@ -0,0 +1,83 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """pptsm infer""" + def __init__(self, cfg, name='PPTSM'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + print("output_names = ", output_names) + #self.output_tensor = self.predictor.get_output_handle(output_names[1]) + self.output_tensor = self.predictor.get_output_handle(output_names[0]) + + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output = self.output_tensor.copy_to_cpu() + return output + + + def predict(self, infer_config): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config) + feature_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = [items[:-1] for items in data] + inputs = np.array(inputs) + output = self.infer(inputs) + #print("inputs", inputs.shape) + #print("outputs", output.shape) + feature_list.append(np.squeeze(output)) + feature_list = np.vstack(feature_list) + return feature_list + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238/' + imgs_list = get_images(imgs_path) + t0 = time.time() + cfg['PPTSM']['frame_list'] = imgs_list + outputs = model.predict(cfg) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + t1 = time.time() + + print(outputs.shape) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/BasketballAction/predict/action_detect/reader/__init__.py b/applications/BasketballAction/predict/action_detect/reader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..547b2d6bb2495dadccd53e178ab80cb656a66496 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/reader/__init__.py @@ -0,0 +1,15 @@ +""" +read map for model +""" +from reader.reader_utils import regist_reader, get_reader +import reader.tsminf_reader as tsminf_reader +import reader.audio_reader as audio_reader +import reader.bmninf_reader as bmninf_reader +import reader.feature_reader as feature_reader + +# regist reader, sort by alphabet +regist_reader("TSM", tsminf_reader.TSMINFReader) +regist_reader("PPTSM", tsminf_reader.TSMINFReader) +regist_reader("AUDIO", audio_reader.AudioReader) +regist_reader("BMN", bmninf_reader.BMNINFReader) +regist_reader("ACTION", feature_reader.FeatureReader) diff --git a/applications/BasketballAction/predict/action_detect/reader/audio_reader.py b/applications/BasketballAction/predict/action_detect/reader/audio_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..2e1f1d28f7f21d329923841c2f04c3e857778425 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/reader/audio_reader.py @@ -0,0 +1,78 @@ +""" +audio reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import os +import _pickle as cPickle +#from .reader_utils import DataReader +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle + from io import BytesIO +import numpy as np +import random +import code + +from .reader_utils import DataReader +import mfcc.feature_extractor as feature_extractor + +class AudioReader(DataReader): + """ + Data reader for youtube-8M dataset, which was stored as features extracted by prior networks + This is for the three models: lstm, attention cluster, nextvlad + + dataset cfg: num_classes + batch_size + list + NextVlad only: eigen_file + """ + + def __init__(self, name, mode, cfg, material=None): + self.name = name + self.mode = mode + + # set batch size and file list + self.sample_rate = cfg[self.name.upper()]['sample_rate'] + self.batch_size = cfg[self.name.upper()]['batch_size'] + self.pcm_file = cfg[self.name.upper()]['pcm_file'] + self.material = material + + def create_reader(self): + """create_reader""" + with open(self.pcm_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + examples = feature_extractor.wav_to_example(audio_data, self.sample_rate) + # print(examples.shape) + + def reader(): + """reader""" + batch_out = [] + batch_out_pre = [] + + for audio in examples: + # batch_out.append([audio]) + batch_out.append(audio) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + if len(batch_out) > 0: + yield batch_out + + return reader diff --git a/applications/BasketballAction/predict/action_detect/reader/bmninf_reader.py b/applications/BasketballAction/predict/action_detect/reader/bmninf_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..afc15b886c361439c39dc44c1782c0927374cd35 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/reader/bmninf_reader.py @@ -0,0 +1,151 @@ +""" +# @File : bmninf_reader.py +# @Author: macaihong +# @Date : 2019/12/15 +# @Desc : +""" + +import os +import random +import pickle +import json +import numpy as np +import multiprocessing + +import numpy as np + +from .reader_utils import DataReader + + +def get_sw_prop(duration, window=200, step=10): + """ + get_sw_prop + """ + pr = [] + local_boxes = [] + for k in np.arange(0, duration - window + step, step): + start_id = k + end_id = min(duration, k + window) + if end_id - start_id < window: + start_id = end_id - window + local_boxes = (start_id, end_id) + pr.append(local_boxes) + + def valid_proposal(duration, span): + """ + valid_proposal + """ + # fileter proposals + # a valid proposal should have at least one second in the video + real_span = min(duration, span[1]) - span[0] + return real_span >= 1 + + pr = list(filter(lambda x: valid_proposal(duration, x), pr)) + return pr + + +class BMNINFReader(DataReader): + """ + Data reader for BMN model, which was stored as features extracted by prior networks + dataset cfg: feat_path, feature path, + tscale, temporal length of BM map, + dscale, duration scale of BM map, + anchor_xmin, anchor_xmax, the range of each point in the feature sequence, + batch_size, batch size of input data, + num_threads, number of threads of data processing + """ + + def __init__(self, name, mode, cfg, material=None): + self.name = name + self.mode = mode + self.tscale = cfg[self.name.upper()]['tscale'] # 200 + self.dscale = cfg[self.name.upper()]['dscale'] # 200 + self.tgap = 1. / self.tscale + self.step = cfg[self.name.upper()]['window_step'] + + self.material = material + src_feature = self.material + + image_feature = src_feature['image_feature'] + pcm_feature = src_feature['pcm_feature'] + pcm_feature = pcm_feature.reshape((pcm_feature.shape[0] * 5, 640)) + min_length = min(image_feature.shape[0], pcm_feature.shape[0]) + image_feature = image_feature[:min_length, :] + pcm_feature = pcm_feature[:min_length, :] + self.features = np.concatenate((image_feature, pcm_feature), axis=1) + + self.duration = len(self.features) + self.window = self.tscale + + self.get_dataset_dict() + self.get_match_map() + + self.batch_size = cfg[self.name.upper()]['batch_size'] + if (mode == 'test') or (mode == 'infer'): + self.num_threads = 1 # set num_threads as 1 for test and infer + + def get_dataset_dict(self): + """ + get_dataset_dict + """ + self.video_list = get_sw_prop(self.duration, self.window, self.step) + + def get_match_map(self): + """ + get_match_map + """ + match_map = [] + for idx in range(self.tscale): + tmp_match_window = [] + xmin = self.tgap * idx + for jdx in range(1, self.tscale + 1): + xmax = xmin + self.tgap * jdx + tmp_match_window.append([xmin, xmax]) + match_map.append(tmp_match_window) + match_map = np.array(match_map) + match_map = np.transpose(match_map, [1, 0, 2]) + match_map = np.reshape(match_map, [-1, 2]) + self.match_map = match_map + self.anchor_xmin = [self.tgap * i for i in range(self.tscale)] + self.anchor_xmax = [self.tgap * i for i in range(1, self.tscale + 1)] + + + def load_file(self, video_wind): + """ + load_file + """ + start_feat_id = video_wind[0] + end_feat_id = video_wind[1] + video_feat = self.features[video_wind[0]: video_wind[1]] + video_feat = video_feat.T + video_feat = video_feat.astype("float32") + return video_feat + + def create_reader(self): + """ + reader creator for ctcn model + """ + return self.make_infer_reader() + + def make_infer_reader(self): + """ + reader for inference + """ + def reader(): + """ + reader + """ + batch_out = [] + # for video_name in self.video_list: + for video_wind in self.video_list: + video_idx = self.video_list.index(video_wind) + video_feat = self.load_file(video_wind) + batch_out.append((video_feat, video_wind, [self.duration, self.dscale])) + + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + if len(batch_out) > 0: + yield batch_out + + return reader diff --git a/applications/BasketballAction/predict/action_detect/reader/feature_reader.py b/applications/BasketballAction/predict/action_detect/reader/feature_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..a2e74fe8d2d8acbbf8196a78c2f982c71438712a --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/reader/feature_reader.py @@ -0,0 +1,87 @@ +""" +attention-lstm feature reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle +import numpy as np +import random +import code + +from .reader_utils import DataReader + +class FeatureReader(DataReader): + """ + Data reader for youtube-8M dataset, which was stored as features extracted by prior networks + This is for the three models: lstm, attention cluster, nextvlad + + dataset cfg: num_classes + batch_size + list + NextVlad only: eigen_file + """ + + def __init__(self, name, mode, cfg, material=None): + self.name = name + self.mode = mode + self.batch_size = cfg[self.name.upper()]['batch_size'] + + self.feature = material['feature'] + self.proposal = material['proposal'] + self.fps = 5 + + def create_reader(self): + """ + create_reader + """ + image_feature_list = self.feature['image_feature'] + audio_feature_list = self.feature['audio_feature'] + pcm_feature_list = self.feature['pcm_feature'] + pcm_feature_list = pcm_feature_list.reshape((pcm_feature_list.shape[0] * 5, 640)) + + fl = self.proposal + + if self.mode == 'train': + random.shuffle(fl) + + def reader(): + """ + reader + """ + batch_out = [] + for prop_info in fl: + start_id = int(prop_info['start']) + end_id = int(prop_info['end']) + bmn_score = float(prop_info['score']) + try: + image_feature = image_feature_list[start_id: end_id] + audio_feature = audio_feature_list[int(start_id / self.fps): int(end_id / self.fps)] + pcm_feature = pcm_feature_list[start_id: end_id] + + image_feature = np.concatenate((image_feature, pcm_feature), axis=1) + + batch_out.append((image_feature, audio_feature, 0, prop_info)) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + except Exception as e: + continue + return reader + diff --git a/applications/BasketballAction/predict/action_detect/reader/reader_utils.py b/applications/BasketballAction/predict/action_detect/reader/reader_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f76b5d38d10100a4362b8f9fe9a7186f7284e840 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/reader/reader_utils.py @@ -0,0 +1,109 @@ +""" +reader_util +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import numpy as np + + +class ReaderNotFoundError(Exception): + """ + "Error: reader not found" + """ + + def __init__(self, reader_name, avail_readers): + super(ReaderNotFoundError, self).__init__() + self.reader_name = reader_name + self.avail_readers = avail_readers + + def __str__(self): + msg = "Reader {} Not Found.\nAvailiable readers:\n".format( + self.reader_name) + for reader in self.avail_readers: + msg += " {}\n".format(reader) + return msg + + +class DataReader(object): + """ + data reader for video input + """ + + def __init__(self, model_name, mode, cfg): + self.name = model_name + self.mode = mode + self.cfg = cfg + + def create_reader(self): + """ + Not implemented + """ + pass + + def get_config_from_sec(self, sec, item, default=None): + """ + get_config_from_sec + """ + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + +class ReaderZoo(object): + """ + ReaderZoo + """ + def __init__(self): + """ + __init__ + """ + self.reader_zoo = {} + + def regist(self, name, reader): + """ + regist + """ + assert reader.__base__ == DataReader, "Unknow model type {}".format( + type(reader)) + self.reader_zoo[name] = reader + + def get(self, name, mode, cfg, material=None): + """ + get + """ + for k, v in self.reader_zoo.items(): + if k == name: + return v(name, mode, cfg, material) + raise ReaderNotFoundError(name, self.reader_zoo.keys()) + + +# singleton reader_zoo +reader_zoo = ReaderZoo() + + +def regist_reader(name, reader): + """ + regist_reader + """ + reader_zoo.regist(name, reader) + + +def get_reader(name, mode, cfg, material=None): + """ + get_reader + """ + reader_model = reader_zoo.get(name, mode, cfg, material) + return reader_model.create_reader() diff --git a/applications/BasketballAction/predict/action_detect/reader/tsminf_reader.py b/applications/BasketballAction/predict/action_detect/reader/tsminf_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..241ba4bc652df91d5235263c2cfa57cb33348abc --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/reader/tsminf_reader.py @@ -0,0 +1,366 @@ +""" +tsn frame reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import random +import functools +import concurrent.futures +import multiprocessing + +import numpy as np +import paddle +from PIL import Image, ImageEnhance + +from .reader_utils import DataReader + + +class TSMINFReader(DataReader): + """ + Data reader for video dataset of jpg folder. + """ + def __init__(self, name, mode, cfg, material=None): + super(TSMINFReader, self).__init__(name, mode, cfg) + name = name.upper() + self.num_seg = cfg[name]['num_seg'] + self.seglen = cfg[name]['seglen'] + self.short_size = cfg[name]['short_size'] + self.target_size = cfg[name]['target_size'] + self.batch_size = cfg[name]['batch_size'] + self.reader_threads = cfg[name]['reader_threads'] + self.buf_size = cfg[name]['buf_size'] + self.video_path = cfg[name]['frame_list'] + + self.img_mean = np.array(cfg[name]['image_mean']).reshape( + [3, 1, 1]).astype(np.float32) + self.img_std = np.array(cfg[name]['image_std']).reshape( + [3, 1, 1]).astype(np.float32) + + self.material = material + + def create_reader(self): + """ + batch loader for TSN + """ + _reader = self._inference_reader_creator_longvideo( + self.video_path, + self.mode, + num_seg=self.num_seg, + seglen=self.seglen, + short_size=self.short_size, + target_size=self.target_size, + img_mean=self.img_mean, + img_std=self.img_std, + num_threads=self.reader_threads, + buf_size=self.buf_size) + + def _batch_reader(): + batch_out = [] + for imgs, label in _reader(): + if imgs is None: + continue + batch_out.append((imgs, label)) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + if len(batch_out) > 1: + yield batch_out[:-1] + + return _batch_reader + + def _inference_reader_creator_longvideo(self, video_path, mode, num_seg, + seglen, short_size, target_size, + img_mean, img_std, num_threads, + buf_size): + """ + inference reader for video + """ + def reader(): + """ + reader + """ + def image_buf(image_id_path_buf): + """ + image_buf reader + """ + try: + img_path = image_id_path_buf[1] + img = Image.open(img_path).convert("RGB") + image_id_path_buf[2] = img + except: + image_id_path_buf[2] = None + + frame_len = len(video_path) + read_thread_num = num_seg + for i in range(0, frame_len, read_thread_num): + image_list_part = video_path[i:i + read_thread_num] + image_id_path_buf_list = [] + for k in range(len(image_list_part)): + image_id_path_buf_list.append([k, image_list_part[k], None]) + + with concurrent.futures.ThreadPoolExecutor( + max_workers=read_thread_num) as executor: + executor.map( + lambda image_id_path_buf: image_buf(image_id_path_buf), + image_id_path_buf_list) + imgs_seg_list = [x[2] for x in image_id_path_buf_list] + + # add the fault-tolerant for bad image + for k in range(len(image_id_path_buf_list)): + img_buf = image_id_path_buf_list[k][2] + pad_id = 1 + while pad_id < num_seg and img_buf is None: + img_buf = imgs_seg_list[(k + pad_id) % num_seg][2] + if img_buf is None: + print("read img erro from {} to {}".format( + i, i + read_thread_num)) + exit(0) + else: + imgs_seg_list[k] = img_buf + for pad_id in range(len(imgs_seg_list), num_seg): + imgs_seg_list.append(imgs_seg_list[-1]) + yield imgs_seg_list + + + def inference_imgs_transform(imgs_list, mode, num_seg, seglen, short_size,\ + target_size, img_mean, img_std): + """ + inference_imgs_transform + """ + imgs_ret = imgs_transform(imgs_list, mode, num_seg, seglen, + short_size, target_size, img_mean, + img_std) + label_ret = 0 + + return imgs_ret, label_ret + + mapper = functools.partial(inference_imgs_transform, + mode=mode, + num_seg=num_seg, + seglen=seglen, + short_size=short_size, + target_size=target_size, + img_mean=img_mean, + img_std=img_std) + + return paddle.reader.xmap_readers(mapper, + reader, + num_threads, + buf_size, + order=True) + + +def imgs_transform(imgs, + mode, + num_seg, + seglen, + short_size, + target_size, + img_mean, + img_std, + name=''): + """ + imgs_transform + """ + imgs = group_scale(imgs, short_size) + + if mode == 'train': + if name == "TSM": + imgs = group_multi_scale_crop(imgs, short_size) + imgs = group_random_crop(imgs, target_size) + imgs = group_random_flip(imgs) + else: + imgs = group_center_crop(imgs, target_size) + + np_imgs = (np.array(imgs[0]).astype('float32').transpose( + (2, 0, 1))).reshape(1, 3, target_size, target_size) / 255 + for i in range(len(imgs) - 1): + img = (np.array(imgs[i + 1]).astype('float32').transpose( + (2, 0, 1))).reshape(1, 3, target_size, target_size) / 255 + np_imgs = np.concatenate((np_imgs, img)) + imgs = np_imgs + imgs -= img_mean + imgs /= img_std + imgs = np.reshape(imgs, (num_seg, seglen * 3, target_size, target_size)) + + return imgs + +def group_multi_scale_crop(img_group, target_size, scales=None, \ + max_distort=1, fix_crop=True, more_fix_crop=True): + """ + group_multi_scale_crop + """ + scales = scales if scales is not None else [1, .875, .75, .66] + input_size = [target_size, target_size] + + im_size = img_group[0].size + + # get random crop offset + def _sample_crop_size(im_size): + """ + _sample_crop_size + """ + image_w, image_h = im_size[0], im_size[1] + + base_size = min(image_w, image_h) + crop_sizes = [int(base_size * x) for x in scales] + crop_h = [ + input_size[1] if abs(x - input_size[1]) < 3 else x + for x in crop_sizes + ] + crop_w = [ + input_size[0] if abs(x - input_size[0]) < 3 else x + for x in crop_sizes + ] + + pairs = [] + for i, h in enumerate(crop_h): + for j, w in enumerate(crop_w): + if abs(i - j) <= max_distort: + pairs.append((w, h)) + + crop_pair = random.choice(pairs) + if not fix_crop: + w_offset = random.randint(0, image_w - crop_pair[0]) + h_offset = random.randint(0, image_h - crop_pair[1]) + else: + w_step = (image_w - crop_pair[0]) / 4 + h_step = (image_h - crop_pair[1]) / 4 + + ret = list() + ret.append((0, 0)) # upper left + if w_step != 0: + ret.append((4 * w_step, 0)) # upper right + if h_step != 0: + ret.append((0, 4 * h_step)) # lower left + if h_step != 0 and w_step != 0: + ret.append((4 * w_step, 4 * h_step)) # lower right + if h_step != 0 or w_step != 0: + ret.append((2 * w_step, 2 * h_step)) # center + + if more_fix_crop: + ret.append((0, 2 * h_step)) # center left + ret.append((4 * w_step, 2 * h_step)) # center right + ret.append((2 * w_step, 4 * h_step)) # lower center + ret.append((2 * w_step, 0 * h_step)) # upper center + + ret.append((1 * w_step, 1 * h_step)) # upper left quarter + ret.append((3 * w_step, 1 * h_step)) # upper right quarter + ret.append((1 * w_step, 3 * h_step)) # lower left quarter + ret.append((3 * w_step, 3 * h_step)) # lower righ quarter + + w_offset, h_offset = random.choice(ret) + crop_info = { + 'crop_w': crop_pair[0], + 'crop_h': crop_pair[1], + 'offset_w': w_offset, + 'offset_h': h_offset + } + + return crop_info + + crop_info = _sample_crop_size(im_size) + crop_w = crop_info['crop_w'] + crop_h = crop_info['crop_h'] + offset_w = crop_info['offset_w'] + offset_h = crop_info['offset_h'] + crop_img_group = [ + img.crop((offset_w, offset_h, offset_w + crop_w, offset_h + crop_h)) + for img in img_group + ] + ret_img_group = [ + img.resize((input_size[0], input_size[1]), Image.BILINEAR) + for img in crop_img_group + ] + + return ret_img_group + + +def group_random_crop(img_group, target_size): + """ + group_random_crop + """ + w, h = img_group[0].size + th, tw = target_size, target_size + + assert (w >= target_size) and (h >= target_size), \ + "image width({}) and height({}) should be larger than crop size".format(w, h) + + out_images = [] + x1 = random.randint(0, w - tw) + y1 = random.randint(0, h - th) + + for img in img_group: + if w == tw and h == th: + out_images.append(img) + else: + out_images.append(img.crop((x1, y1, x1 + tw, y1 + th))) + + return out_images + + +def group_random_flip(img_group): + """ + group_random_flip + """ + v = random.random() + if v < 0.5: + ret = [img.transpose(Image.FLIP_LEFT_RIGHT) for img in img_group] + return ret + else: + return img_group + + +def group_center_crop(img_group, target_size): + """ + group_center_crop + """ + img_crop = [] + for img in img_group: + w, h = img.size + th, tw = target_size, target_size + assert (w >= target_size) and (h >= target_size), \ + "image width({}) and height({}) should be larger than crop size".format(w, h) + x1 = int(round((w - tw) / 2.)) + y1 = int(round((h - th) / 2.)) + img_crop.append(img.crop((x1, y1, x1 + tw, y1 + th))) + + return img_crop + + +def group_scale(imgs, target_size): + """ + group_scale + """ + resized_imgs = [] + for i in range(len(imgs)): + img = imgs[i] + w, h = img.size + if (w <= h and w == target_size) or (h <= w and h == target_size): + resized_imgs.append(img) + continue + + if w < h: + ow = target_size + oh = int(target_size * 4.0 / 3.0) + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + else: + oh = target_size + ow = int(target_size * 4.0 / 3.0) + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + + return resized_imgs diff --git a/applications/BasketballAction/predict/action_detect/utils/__init__.py b/applications/BasketballAction/predict/action_detect/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/BasketballAction/predict/action_detect/utils/config_utils.py b/applications/BasketballAction/predict/action_detect/utils/config_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e5db92b0d8f7ce5d5ff8d14635e53996dafb3ac3 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/utils/config_utils.py @@ -0,0 +1,80 @@ +""" +config_utils +""" +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import yaml +import ast + +import logger + +logger = logger.Logger() + +CONFIG_SECS = [ + 'train', + 'valid', + 'test', + 'infer', +] + +class AttrDict(dict): + """ + AttrDict + """ + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value + + +def parse_config(cfg_file): + """Load a config file into AttrDict""" + import yaml + with open(cfg_file, 'r') as fopen: + yaml_config = AttrDict(yaml.load(fopen, Loader=yaml.Loader)) + create_attr_dict(yaml_config) + return yaml_config + + +def create_attr_dict(yaml_config): + """create_attr_dict""" + for key, value in yaml_config.items(): + if isinstance(value, dict): + yaml_config[key] = value = AttrDict(value) + if isinstance(value, str): + try: + value = ast.literal_eval(value) + except BaseException: + pass + if isinstance(value, AttrDict): + create_attr_dict(yaml_config[key]) + else: + yaml_config[key] = value + return + + +def print_configs(cfg, mode): + """print_configs""" + logger.info("---------------- {:>5} Arguments ----------------".format( + mode)) + for sec, sec_items in cfg.items(): + logger.info("{}:".format(sec)) + for k, v in sec_items.items(): + logger.info(" {}:{}".format(k, v)) + logger.info("-------------------------------------------------") diff --git a/applications/BasketballAction/predict/action_detect/utils/preprocess.py b/applications/BasketballAction/predict/action_detect/utils/preprocess.py new file mode 100644 index 0000000000000000000000000000000000000000..d14aaf1eecaef7ab853fad2b2ef82ce9077c2677 --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/utils/preprocess.py @@ -0,0 +1,36 @@ +""" extract frames and pcm""" +import os +import sys +import shutil + + +def ffmpeg_frames(mp4_addr, frame_out_folder, fps=5): + """ffmpeg_frames""" + if os.path.exists(frame_out_folder): + shutil.rmtree(frame_out_folder) + os.makedirs(frame_out_folder) + cmd = './src/utils/ffmpeg -v 0 -i %s -r %d -q 0 %s/%s.jpg' % (mp4_addr, fps, frame_out_folder, '%08d') + os.system(cmd) + + +def ffmpeg_pcm(mp4_addr, save_file_name): + """ffmpeg_pcm""" + cmd = './src/utils/ffmpeg -y -i %s -acodec pcm_s16le -f s16le -ac 1 -ar 16000 %s -v 0' \ + % (mp4_addr, save_file_name) + os.system(cmd) + + +def ffmpeg_mp4(mp4_url, mp4_addr): + """ffmpeg_mp4""" + cmd = "wget %s -O %s -q" % (mp4_url, mp4_addr) + print ("cmd = ", cmd) + os.system(cmd) + + +def get_images(image_path): + """get_images""" + images = sorted(os.listdir(image_path)) + images = images + images_path_list = [image_path + '/' + im for im in images] + return images_path_list + diff --git a/applications/BasketballAction/predict/action_detect/utils/process_result.py b/applications/BasketballAction/predict/action_detect/utils/process_result.py new file mode 100644 index 0000000000000000000000000000000000000000..164869696cbbb95d0bbdbf697aec355f7e70b93b --- /dev/null +++ b/applications/BasketballAction/predict/action_detect/utils/process_result.py @@ -0,0 +1,144 @@ +""" +# @File : process_result.py +# @Author: macaihong +# @Date : 2019/12/15 +# @Desc : +""" + +import sys +import os +import re +import numpy as np +import pickle +import json +import logger + +logger = logger.Logger() + + +def get_data_res(label_map, data, topk): + """get_data_res""" + sum_vid = len(data) + video_result = [] + for i in range(sum_vid): + vid_name = data[i][0][0] + # true_label predict_start predict_end predict_score predict_len gt_iou gt_start gt_ioa + feature_start_id = float(data[i][0][1]['start']) + feature_end_id = float(data[i][0][1]['end']) + feature_stage1_score = data[i][0][1]['score'] + predict_res = [] + for k in range(topk): + score_top = data[i][1][k] + labelid_top = data[i][2][k] + label_iou = data[i][3] + labelname_top = label_map[str(labelid_top)] + video_result.append([feature_start_id, feature_end_id, labelid_top, labelname_top, score_top, label_iou]) + return video_result + + +def base_nms(bboxes, thresh, delta=0, nms_id=2): + """ + One-dimensional non-maximal suppression + :param bboxes: [[vid, label, st, ed, score, ...], ...] + :param thresh: + :return: + """ + """ + t1 = bboxes[:, 0] + t2 = bboxes[:, 1] + scores = bboxes[:, nms_id] + """ + + t1 = np.array([max(0, x[0] - delta) for x in bboxes]) + t2 = np.array([x[1] + delta for x in bboxes]) + scores = np.array([x[nms_id] for x in bboxes]) + + durations = t2 - t1 + order = scores.argsort()[::-1] + + keep = [] + while order.size > 0: + i = order[0] + keep.append(i) + tt1 = np.maximum(t1[i], t1[order[1:]]) + tt2 = np.minimum(t2[i], t2[order[1:]]) + intersection = tt2 - tt1 + IoU = intersection / (durations[i] + durations[order[1:]] - intersection).astype(float) + + inds = np.where(IoU <= thresh)[0] + order = order[inds + 1] + return [bboxes[i] for i in keep] + + +def process_proposal(source_prop_box, min_frame_thread=5, nms_thresh=0.7, score_thresh=0.01): + """process_video_prop""" + prop_box = [] + for items in source_prop_box: + start_frame = float(items[0]) + end_frame = float(items[1]) + score = float(items[2]) + if end_frame - start_frame < min_frame_thread or score < score_thresh: + continue + prop_box.append([start_frame, end_frame, score]) + + prop_box_keep = base_nms(prop_box, nms_thresh) + + prop_res = [] + for res in prop_box_keep: + prop_res.append({'start': res[0], 'end': res[1], 'score': res[2]}) + + return prop_res + + +def process_video_classify(video_prop, fps, score_thread, iou_thread, \ + nms_id=5, nms_thread=0.01, nms_delta=10, backgroundid=0): + """process_video_classify""" + prop_filter = [] + for item in video_prop: + if item[2] == backgroundid: + continue + prop_filter.append(item) + + # prop_filter = sorted(prop_filter, key=lambda x: x[nms_id], reverse=True) + prop_filter = base_nms(prop_filter, nms_thread, nms_delta, nms_id) + prop_filter = sorted(prop_filter, key=lambda x: x[0]) + + video_results = [] + for item in prop_filter: + start_sec = item[0] / fps + end_sec = item[1] / fps + + start_id_frame = item[0] + end_id_frame = item[1] + # start_time = "%02d:%02d:%02d" % ((start_id_frame / fps) / 3600, \ + # ((start_id_frame / fps) % 3600) / 60, (start_id_frame / fps) % 60) + # end_time = "%02d:%02d:%02d" % ((end_id_frame / fps) / 3600, \ + # ((end_id_frame / fps) % 3600) / 60, (end_id_frame / fps) % 60) + start_time = int(start_id_frame / fps) + end_time = int(end_id_frame / fps) + + label_id = item[2] + label_name = item[3] + label_classify_score = item[4] + label_iou_score = item[5] + if label_classify_score > score_thread and label_iou_score > iou_thread: + video_results.append({"start_time": start_time, + "end_time": end_time, + "label_id": label_id, + "label_name": label_name, + "classify_score": label_classify_score, + "iou_score": label_iou_score}) + + return video_results + + +def get_action_result(result_info, label_map_file, fps, score_thread=0, \ + iou_thread=0, nms_id=5, nms_thread=0.01, frame_offset=10, topk=1): + """get_action_result""" + + label_map = json.load(open(label_map_file, 'r', encoding='utf-8')) + + org_result = get_data_res(label_map, result_info, topk) + nms_result = process_video_classify(org_result, fps, score_thread, iou_thread, nms_id, nms_thread, frame_offset) + + return nms_result diff --git a/applications/BasketballAction/predict/checkpoints_basketball/download.txt b/applications/BasketballAction/predict/checkpoints_basketball/download.txt new file mode 100644 index 0000000000000000000000000000000000000000..e2da0e5d4986df28c4423d9039830d3a5f8e9394 --- /dev/null +++ b/applications/BasketballAction/predict/checkpoints_basketball/download.txt @@ -0,0 +1 @@ +https://bj.bcebos.com/v1/acg-algo/PaddleVideo_application/basketball/checkpoints_basketball.tar.gz diff --git a/applications/BasketballAction/predict/configs_basketball/configs_basketball.yaml b/applications/BasketballAction/predict/configs_basketball/configs_basketball.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9f01904f808389d6b694aaa8423cdfe331fa8e75 --- /dev/null +++ b/applications/BasketballAction/predict/configs_basketball/configs_basketball.yaml @@ -0,0 +1,62 @@ +COMMON: + fps: 5 + use_gpu: True + label_dic: 'configs_basketball/index_label_basketball_6.json' + # debug + PCM_ONLY: False + DEBUG: False + BMN_ONLY: False + LSTM_ONLY: False + +PPTSM: + name: "PPTSM" + model_file: "checkpoints_basketball/ppTSM/ppTSM.pdmodel" + params_file: "checkpoints_basketball/ppTSM/ppTSM.pdiparams" + gpu_mem: 8000 + device_id: 0 + num_seg: 8 + seglen: 1 + short_size: 256 + target_size: 224 + batch_size: 32 + image_mean: [0.485, 0.456, 0.406] + image_std: [0.229, 0.224, 0.225] + reader_threads: 12 + buf_size: 1024 + +AUDIO: + name: "AUDIO" + model_file: "checkpoints_basketball/AUDIO/__model__" + params_file: "checkpoints_basketball/AUDIO/__param__" + gpu_mem: 8000 + device_id: 0 + sample_rate: 16000 + batch_size: 32 + +BMN: + name: "BMN" + model_file: "checkpoints_basketball/BMN/__model__" + params_file: "checkpoints_basketball/BMN/__param__" + gpu_mem: 1000 + device_id: 0 + window_step: 200 # 200 + tscale: 200 + dscale: 200 + batch_size: 1 # 8 + nms_thread: 0.7 + score_thread: 0.03 # 0.05 + +ACTION: + name: "ACTION" + model_file: "checkpoints_basketball/LSTM/__model__" + params_file: "checkpoints_basketball/LSTM/__param__" + gpu_mem: 8000 + device_id: 0 + batch_size: 32 + topk: 1 + nms_thread: 0.01 + nms_offset: 10 + + classify_score_thread: 0.1 + iou_score_thread: 0.3 + diff --git a/applications/BasketballAction/predict/configs_basketball/index_label_basketball_6.json b/applications/BasketballAction/predict/configs_basketball/index_label_basketball_6.json new file mode 100644 index 0000000000000000000000000000000000000000..5b980115bfbd45bae9edca9ed5fc09cba6d2d1ed --- /dev/null +++ b/applications/BasketballAction/predict/configs_basketball/index_label_basketball_6.json @@ -0,0 +1,9 @@ +{ + "0": "背景", + "1": "回放", + "2": "进球-三分球", + "3": "进球-两分球", + "4": "进球-扣篮", + "5": "罚球", + "6": "跳球" +} diff --git a/applications/BasketballAction/predict/datasets/download.txt b/applications/BasketballAction/predict/datasets/download.txt new file mode 100644 index 0000000000000000000000000000000000000000..9c142b579a004524e6d6d0a0cdc9b07e859c5969 --- /dev/null +++ b/applications/BasketballAction/predict/datasets/download.txt @@ -0,0 +1 @@ +https://bj.bcebos.com/v1/acg-algo/PaddleVideo_application/basketball/datasets.tar.gz diff --git a/applications/BasketballAction/predict/eval.py b/applications/BasketballAction/predict/eval.py new file mode 100644 index 0000000000000000000000000000000000000000..f7fe5705be91c29dfc3d7ae69d08e07664585877 --- /dev/null +++ b/applications/BasketballAction/predict/eval.py @@ -0,0 +1,238 @@ +""" +get instance for lstm +根据gts计算每个proposal_bmn的iou、ioa、label等信息 +""" +import os +import sys +import json +import random +import pickle +import numpy as np + +import io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding = 'utf-8') + +dataset = "datasets/" + +label_index_file = './configs_basketball/index_label_basketball_6.json' +eval_datasets = ['EuroCup2016'] +label_files = {'train': 'label_cls6_train.json', + 'validation': 'label_cls6_val.json'} + +global fps, mode +label_index = json.load(open(label_index_file, 'rb')) + +def load_gts(): + global fps + gts_data = {'fps': 0, 'gts': {}} + for eval_data in eval_datasets: + for item, value in label_files.items(): + label_file = '{}/{}/{}'.format(dataset, eval_data, value) + gts = json.load(open(label_file, 'rb')) + gts_data['fps'] = gts['fps'] + fps = gts['fps'] + for gt in gts['gts']: + gt['mode'] = item + basename = '{}/{}/mp4/{}'.format(dataset, eval_data, os.path.basename(gt['url'])) + gts_data['gts'][basename] = gt + return gts_data['gts'] + + +def computeIoU(e1, e2): + """ + clc iou and ioa + """ + if not (e1['label'] == e2['label'] and e1['basename'] == e2['basename']): + return 0. + area1 = e1["end"] - e1["start"] + area2 = e2["end"] - e2["start"] + x1 = np.maximum(e1["start"], e2["start"]) + x2 = np.minimum(e1["end"], e2["end"]) + inter = np.maximum(0.0, x2 - x1) + iou = 0.0 if (area1 + area2 - inter) == 0 else inter * 1.0 / (area1 + area2 - inter) + if not mode == 'proposal': + iou = 0.0 if area2 == 0 else inter * 1.0 / area2 + return iou + + +def convert_proposal(boxes, basename, score_threshold=0.01): + boxes = sorted(boxes, key=lambda x:float(x['score']), reverse=True) + res = [] + for box in boxes: + if not float(box['score']) >= score_threshold: + continue + res.append({'basename': basename, + 'start': int(float(box['start']) / fps), + 'end': int(float(box['end']) / fps), + 'label': 0}) + return res + +def convert_classify(boxes, basename, iou_threshold, score_threshold): + boxes = sorted(boxes, key=lambda x:(float(x['classify_score']), float(x['iou_score'])), reverse=True) + def convert_time_to_frame(time_type): + return int(time_type) + h, m, s = time_type.split(':') + return int(h) * 3600 + int(m) * 60 + int(s) + res = [] + for box in boxes: + if not (box['iou_score'] >= iou_threshold and + box['classify_score'] >= score_threshold): + continue + res.append({'basename': basename, + 'start': convert_time_to_frame(box['start_time']), + 'end': convert_time_to_frame(box['end_time']), + 'label': box['label_id']}) + return res + +def convert_groundtruth(boxes, basename, phase=None): + res = [] + for box in boxes: + for item in box['label_ids']: + label = 0 if phase == 'proposal' else item + res.append({'basename': basename, + 'start': box['start_id'], + 'end': box['end_id'], + 'label': label}) + return res +def print_head(iou): + print("\nioa = {:.1f}".format(iou)) + res_str = '' + for item in ['label_name']: + res_str += '{:<12s}'.format(item) + for item in ['label_id', 'precision', 'recall', 'hit_prop', 'num_prop', 'hit_gts', 'num_gts']: + res_str += '{:<10s}'.format(item) + print(res_str) + +def print_result(res_dict, label='avg'): + if label == 'avg': + res_str = '{:<22s}'.format(str(label)) + else: + res_str = '{0:{2}<6s}{1:<10s}'.format(label_index[str(label)], str(label), chr(12288)) + + for item in ['prec', 'recall']: + res_str += '{:<10.4f}'.format(res_dict[item]) + for item in ['hit_prop', 'num_prop', 'hit_gts', 'num_gts']: + res_str += '{:<10d}'.format(res_dict[item]) + print(res_str) + +def evaluation(res_boxes, gts_boxes, label_range, iou_range, show_sub = False): + iou_map = [computeIoU(resId, gtsId) for resId in res_boxes \ + for gtsId in gts_boxes] + iou_map = np.array(iou_map).reshape((len(res_boxes), len(gts_boxes))) + hit_map_prop_total = np.max(iou_map, axis=1) + hit_map_index_total = np.argmax(iou_map, axis=1) + + res_dict = ['hit_prop', 'num_prop', 'hit_gts', 'num_gts'] + + for iou_threshold in iou_range: + if show_sub: + print_head(iou_threshold) + + iou_prop = np.array([k >= iou_threshold for k in hit_map_prop_total]) + average_results = {} + for label_id in label_range: + sub_results = {} + label_prop = np.array([k['label'] == label_id for k in res_boxes]) + label_gts = np.array([k['label'] == label_id for k in gts_boxes]) + sub_results['num_prop'] = sum(label_prop) + sub_results['num_gts'] = sum(label_gts) + if sub_results['num_prop'] == 0: + hit_prop_index = [] + else: + hit_prop_index = label_prop & iou_prop + sub_results['hit_prop'] = sum(hit_prop_index) + sub_results['hit_gts'] = len(set(hit_map_index_total[hit_prop_index])) + + sub_results['prec'] = 0.0 if sub_results['num_prop'] == 0 \ + else sub_results['hit_prop'] * 1.0 / sub_results['num_prop'] + sub_results['recall'] = 0.0 if sub_results['num_gts'] == 0 \ + else sub_results['hit_gts'] * 1.0 / sub_results['num_gts'] + if show_sub: + print_result(sub_results, label=label_id) + for item in res_dict: + if not item in average_results: + average_results[item] = 0 + average_results[item] += sub_results[item] + if len(label_range) == 1: # proposal 不需要输出average值 + continue + average_results['prec'] = 0.0 if average_results['num_prop'] == 0 \ + else average_results['hit_prop'] * 1.0 / average_results['num_prop'] + average_results['recall'] = 0.0 if average_results['num_gts'] == 0 \ + else average_results['hit_gts'] * 1.0 / average_results['num_gts'] + if show_sub: + print_result(average_results) + + average_results['F1'] = 0.0 if (average_results['prec'] + average_results['recall'] == 0) \ + else 2 * average_results['prec'] * average_results['recall'] / \ + (average_results['prec'] + average_results['recall']) + return average_results + +def get_eval_results(predicts, gts_data, phase, iou_threshold = 0.3, score_threshold = 0.3, show_sub = False): + global mode + mode = phase + res_boxes = [] + gts_boxes = [] + for ped_data in predicts: + basename = ped_data['video_name'] + + # eval sub data + such_eval = False + for eval_name in eval_datasets: + if eval_name in basename: + such_eval = True + break + if not such_eval: + continue + + gts = gts_data[basename]['actions'] + if phase == 'proposal': + res_boxes.extend(convert_proposal(ped_data['bmn_results'], basename, score_threshold)) + gts_boxes.extend(convert_groundtruth(gts, basename, phase='proposal')) + label_range = [0] + iou_range = np.arange(0.1, 1, 0.1) + else: + res_boxes.extend(convert_classify(ped_data['action_results'], basename, iou_threshold, score_threshold)) + gts_boxes.extend(convert_groundtruth(gts, basename)) + label_range = range(1, len(label_index)) + iou_range = np.arange(0.5, 0.6, 0.1) + + eval_results = evaluation(res_boxes, gts_boxes, label_range, iou_range, show_sub = show_sub) + + return eval_results + + +if __name__ == "__main__": + result_file = sys.argv[1] + predicts = json.load(open(result_file, 'r', encoding='utf-8')) + gts_data = load_gts() + + get_eval_results(predicts, gts_data, 'proposal', + score_threshold = 0.03, + show_sub = True) + #get_eval_results(predicts, gts_data, 'actions') + + best_F1 = -0.1 + best_res = {} + best_iou_threshold = 0. + best_score_threshold = 0. + for iou_threshold in np.arange(0.1, 0.9, 0.1): + for score_threshold in np.arange(0.1, 1, 0.1): + avg_res = get_eval_results(predicts, gts_data, 'actions', + iou_threshold = iou_threshold, + score_threshold = score_threshold, + show_sub = False) + if best_F1 < avg_res['F1']: + best_F1 = avg_res['F1'] + best_res = avg_res + best_iou_threshold = iou_threshold + best_score_threshold = score_threshold + print("best iou threshold = {:.1f}".format(best_iou_threshold)) + print("best score threshold = {:.1f}".format(best_score_threshold)) + print('best F1 score = {:.4f}'.format(best_F1)) + print_head(0.5) + print_result(best_res) + + get_eval_results(predicts, gts_data, 'actions', iou_threshold = best_iou_threshold, + score_threshold = best_score_threshold, + show_sub = True) + diff --git a/applications/BasketballAction/predict/predict.py b/applications/BasketballAction/predict/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..168d34125f9c5f0cf06b60da89d0c58e9011fbc6 --- /dev/null +++ b/applications/BasketballAction/predict/predict.py @@ -0,0 +1,35 @@ + +import os +import sys +import json + +sys.path.append('action_detect') +from action import ActionDetection + +if __name__ == '__main__': + dataset_dir = "datasets/" + + model_predict = ActionDetection(cfg_file="configs_basketball/configs_basketball.yaml") + model_predict.load_model() + + video_url = os.path.join(dataset_dir, 'mp4.list') + with open(video_url, 'r') as f: + lines = f.readlines() + lines = [os.path.join(dataset_dir, "mp4", os.path.basename(k.strip())) for k in lines] + + results = [] + for line in lines: + video_name = line + print(video_name) + + imgs_path = video_name.replace(".mp4", "").replace("mp4", "frames") + pcm_path = video_name.replace(".mp4", ".pcm").replace("mp4", "pcm") + + bmn_results, action_results = model_predict.infer(imgs_path, pcm_path) + results.append({'video_name': line, + 'bmn_results': bmn_results, + 'action_results': action_results}) + + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) diff --git a/applications/EIVideo/.github/ISSUE_TEMPLATE/------bug---------.md b/applications/EIVideo/.github/ISSUE_TEMPLATE/------bug---------.md new file mode 100644 index 0000000000000000000000000000000000000000..3ea2793a0713c85f5ecbb6bb0a308bb40646957e --- /dev/null +++ b/applications/EIVideo/.github/ISSUE_TEMPLATE/------bug---------.md @@ -0,0 +1,33 @@ +--- +name: 我可能遇到了Bug或在使用中遇到问题 +about: 详细描述事情发生的经过,以便解决该问题。 +title: "【可能的Bug】" +labels: Possible bugs +assignees: GT-ZhangAcer + +--- + + +- [x] 我已了解以上内容 + +## 发生问题的组件 +- [ ] EIVideo(CLI版本) +- [ ] QEIVideo(GUI版本) +- [x] 我不清楚是什么组件出现了问题 + +## 环境信息 + +1. 操作系统:例如Windows10 1608 +2. 解释器环境:例如Python 3.2 +3. (Q)EIVideo版本:例如0.0.1 + +## 报错信息与截图 + +``` +您的报错信息 +``` + +## 复现步骤&是在什么情况下出现的问题 + + +## 【可选】您期待的解决路径 or 想法或建议 diff --git a/applications/EIVideo/.github/ISSUE_TEMPLATE/--or--.md b/applications/EIVideo/.github/ISSUE_TEMPLATE/--or--.md new file mode 100644 index 0000000000000000000000000000000000000000..673bc4ba72e1646895e5031901f135a2265cb04e --- /dev/null +++ b/applications/EIVideo/.github/ISSUE_TEMPLATE/--or--.md @@ -0,0 +1,18 @@ +--- +name: 想法or建议 +about: 撰写关于EIVideo的产品提议 +title: "【提议】" +labels: Communication +assignees: GT-ZhangAcer + +--- + +## 提议来源 +- [ ] 工程应用 +- [ ] 学术研究 +- [ ] 作业/外包任务 +- [ ] 在此处撰写您的提议来源 + +## 提议主要内容 + +## 您推荐的解决方案 diff --git a/applications/EIVideo/.gitignore b/applications/EIVideo/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..dd4040f2b38be41489c229096d41fdad3cc9f6fa --- /dev/null +++ b/applications/EIVideo/.gitignore @@ -0,0 +1,133 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# our +.idea/ +.DS_Store diff --git a/applications/EIVideo/EIVideo/README.MD b/applications/EIVideo/EIVideo/README.MD new file mode 100644 index 0000000000000000000000000000000000000000..6dc0fe4b9083d8af179e9259c5ff9114d2b74e1a --- /dev/null +++ b/applications/EIVideo/EIVideo/README.MD @@ -0,0 +1,15 @@ +# 交互式视频智能标注工具 - CLI(Command Line Interface) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install scikit-image +``` + +## 推理运行方式 +```shell + +C:\Python\Python37\python.exe main.py --test -c E:/PaddlePaddle_Project/EIVideo/resources/backend/configs/manet.yaml -w E:/PaddlePaddle_Project/EIVideo/resources/backend/model/save_step_80000.pdparams +C:\Python\Python37\python.exe resources/backend/main.py --test -c E:/PaddlePaddle_Project/EIVideo/resources/backend/configs/manet.yaml -w E:/PaddlePaddle_Project/EIVideo/resources/backend/model/save_step_80000.pdparams +``` +## 参考文档 +[manet](docs/zh-CN/manet.md) \ No newline at end of file diff --git a/applications/EIVideo/EIVideo/__init__.py b/applications/EIVideo/EIVideo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..17f7630ec101e60f7fda69a4e82ab248acd4a351 --- /dev/null +++ b/applications/EIVideo/EIVideo/__init__.py @@ -0,0 +1,16 @@ +# Author: Acer Zhang +# Datetime: 2022/1/6 +# Copyright belongs to the author. +# Please indicate the source for reprinting. + +import os +from EIVideo.version import __version__ + +EI_VIDEO_ROOT = os.path.abspath(os.path.dirname(__file__)) +TEMP_IMG_SAVE_PATH = "./temp.png" +TEMP_JSON_SAVE_PATH = "./save.json" +TEMP_JSON_FINAL_PATH = "./final.json" + + +def join_root_path(path: str): + return os.path.join(EI_VIDEO_ROOT, path) diff --git a/applications/EIVideo/EIVideo/api.py b/applications/EIVideo/EIVideo/api.py new file mode 100644 index 0000000000000000000000000000000000000000..009673cba083c223fc47e6d95481f5707a059ae5 --- /dev/null +++ b/applications/EIVideo/EIVideo/api.py @@ -0,0 +1,134 @@ +# Author: AP-Kai +# Datetime: 2022/1/10 +# Copyright belongs to the author. +# Please indicate the source for reprinting. + + +import json +import os +from collections import OrderedDict +import cv2 +import numpy as np +from PIL import Image + +from EIVideo.paddlevideo.utils.manet_utils import overlay_davis +from EIVideo import TEMP_JSON_SAVE_PATH, TEMP_JSON_FINAL_PATH + + +def get_images(sequence='bike-packing'): + img_path = os.path.join('data', sequence.strip(), 'frame') + img_files = os.listdir(img_path) + img_files.sort() + files = [] + for img in img_files: + img_file = np.array(Image.open(os.path.join(img_path, img))) + files.append(img_file) + return np.array(files) + + +def json2frame(path): + print("now turn masks.json to frames", path) + with open(path, 'r', encoding='utf-8') as f: + res = f.read() + a = json.loads(res) + b = a.get('overlays') + b_array = np.array(b) + frame_list = [] + + for i in range(0, len(b_array)): + im = Image.fromarray(np.uint8(b_array[i])) + im = cv2.cvtColor(np.asarray(im), cv2.COLOR_RGB2BGR) + im = cv2.cvtColor(im, cv2.COLOR_RGB2BGR) + # im = np.array(b_array[i]).astype("uint8") + # im = im.transpose((2, 0, 1)) + # im = cv2.merge(im) + frame_list.append(im) + return frame_list + + +def png2json(image_path, sliderframenum, save_json_path): + image = Image.open(image_path) # 用PIL中的Image.open打开图像 + image = image.convert('P') + image_arr = np.array(image) # 转化成numpy数组 + image_arr = image_arr.astype("float32") + r1 = np.argwhere(image_arr == 1) # tuple + pframes = [] + # i -> object id + for i in range(1, len(np.unique(image_arr))): + pframe = OrderedDict() + pframe['path'] = [] + # Find object id in image_arr + r1 = np.argwhere(image_arr == i) # tuple + r1 = r1.astype("float32") + # Add path to pframe + for j in range(0, len(r1)): + r1[j][0] = r1[j][0] / 480.0 + r1[j][1] = r1[j][1] / 910.0 + # r1[j] = np.around(r1[j], decimals=16) + pframe['path'].append(r1[j].tolist()) + # Add object id, start_time, stop_time + pframe['object_id'] = i + pframe['start_time'] = sliderframenum + pframe['stop_time'] = sliderframenum + # Add pframe to pframes + pframes.append(pframe) + + dic = OrderedDict() + dic['scribbles'] = [] + for i in range(0, int(100)): + if i == sliderframenum: + # Add value to frame[] + dic['scribbles'].append(pframes) + else: + dic['scribbles'].append([]) + + json_str = json.dumps(dic) + with open(save_json_path, 'w') as json_file: + json_file.write(json_str) + + +def load_video(video_path, min_side=None): + frame_list = [] + # ToDo To AP-kai: 是不是轻松干掉了m.video_path? + cap = cv2.VideoCapture(video_path) + # ToDo To AP-kai: while (cap.isOpened()): -> 不必多写个括号哈 + while cap.isOpened(): + _, frame = cap.read() + if frame is None: + break + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + if min_side: + h, w = frame.shape[:2] + new_w = (w * min_side // min(w, h)) + new_h = (h * min_side // min(w, h)) + frame = cv2.resize(frame, (new_w, new_h), + interpolation=cv2.INTER_CUBIC) + # .transpose([2, 0, 1]) + frame_list.append(frame) + frames = np.stack(frame_list, axis=0) + return frames, frame_list + + +def get_scribbles(): + # os.makedirs(TEMP_JSON_SAVE_PATH, exist_ok=True) + with open(TEMP_JSON_SAVE_PATH) as f: + print("load TEMP_JSON_SAVE_PATH success") + scribbles = json.load(f) + first_scribble = True + yield scribbles, first_scribble + + +def submit_masks(save_path, masks, images): + overlays = [] + for img_name, (mask, image) in enumerate(zip(masks, images)): + overlay = overlay_davis(image, mask) + overlays.append(overlay.tolist()) + overlay = Image.fromarray(overlay) + img_name = str(img_name) + while len(img_name) < 5: + img_name = '0' + img_name + overlay.save(os.path.join(save_path, img_name + '.png')) + result = {'overlays': overlays} + # result = {'masks': masks.tolist()} + with open(TEMP_JSON_FINAL_PATH, 'w') as f: + json.dump(result, f) diff --git a/applications/EIVideo/EIVideo/configs/manet.yaml b/applications/EIVideo/EIVideo/configs/manet.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4c5a1e4aaf79544a9777f8b5559601fa39e4d660 --- /dev/null +++ b/applications/EIVideo/EIVideo/configs/manet.yaml @@ -0,0 +1,27 @@ +MODEL: #MODEL field + framework: "Manet" + backbone: + name: "DeepLab" + backbone: 'resnet' + output_stride: 16 + num_classes: 21 + freeze_bn: False + head: + name: "IntVOS" + train_bn_mom: 0.9997 + model_aspp_outdim: 256 + model_semantic_embedding_dim: 100 + model_head_embedding_dim: 256 + model_useintseg: False + test_mode: False + model_max_local_distance: 12 + +PIPELINE: #PIPELINE field + test: #Mandatory, indicate the pipeline to deal with the validing data. please refer to the 'paddlevideo/loader/pipelines/' + transform: #Mandatory, image transform operator. + - Resize_manet: + output_size: [ 480, 854 ] + - ToTensor_manet: + + +model_name: "Manet" #Mandatory, model name. diff --git a/applications/EIVideo/EIVideo/docs/en/manet.md b/applications/EIVideo/EIVideo/docs/en/manet.md new file mode 100644 index 0000000000000000000000000000000000000000..558f3e026f6e08ceef49aad207290d2ab6b41609 --- /dev/null +++ b/applications/EIVideo/EIVideo/docs/en/manet.md @@ -0,0 +1,122 @@ +[简体中文](../zh-CN/manet.md) | English + +# Ma-Net + +## Contents + +- [Introduction](#Introduction) + +- [Data](#Data) + +- [Train](#Train) + +- [Test](#Test) + +- [Inference](#Inference) + +Before getting started, you need to install additional dependencies as follows: +```bash +python -m pip install scikit-image +``` + +## Introduction + +This is the paddle implementation of the CVPR2020 paper "[Memory aggregation networks for efficient interactive video object segmentation](https://arxiv.org/abs/2003.13246)". + +![avatar](../../../images/1836-teaser.gif) + +This code currently supports model test and model training on DAVIS dataset, and model inference on any given video will be provided in few days. + + + +## Data + +Download [DAVIS2017](https://data.vision.ee.ethz.ch/csergi/share/davis/DAVIS-2017-trainval-480p.zip) and [scribbles](https://data.vision.ee.ethz.ch/csergi/share/DAVIS-Interactive/DAVIS-2017-scribbles-trainval.zip) into one folder. Please refer to [DAVIS](https://davischallenge.org/davis2017/code.html). + +If you need the file "DAVIS2017/ImageSets/2017/v_a_l_instances.txt", please refer to the link https://drive.google.com/file/d/1aLPaQ_5lyAi3Lk3d2fOc_xewSrfcrQlc/view?usp=sharing + + +## Train + +#### Download and add pre-trained models + +1. Download [deeplabV3+ model pretrained on COCO](https://drive.google.com/file/d/15temSaxnKmGPvNxrKPN6W2lSsyGfCtTB/view?usp=sharing) to this repo as the Backbone initialization parameter, or download it through wget + + ```bash + wget https://drive.google.com/file/d/15temSaxnKmGPvNxrKPN6W2lSsyGfCtTB/view?usp=sharing + ``` + +2. Open `PaddleVideo/configs/segmentationer/manet_stage1.yaml`, and fill in the downloaded weight storage path below `pretrained:` + + ```yaml + MODEL: #MODEL field + framework: "Manet" + backbone: + name: "DeepLab" + pretrained: fill in the path here + ``` + +#### Start training + +- Our training process contains two stage. + + - You can start training of stage one using one card by such command: + + ```bash + python main.py -c configs/segmentation/manet.yaml + ``` + + - Turn on multiple cards for training to speed up the training process. The training start command is as follows: + + ```bash + export CUDA_VISIBLE_DEVICES=0,1,2,3 + + python -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_manet_stage1 main.py -c configs/segmentation/manet.yaml + ``` + + - Turn on amp mixed-precision training to speed up the training process. The training start command is as follows: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + # frames data format + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4" --log_dir=log_manet_stage1 main.py --amp -c configs/segmentation/manet.yaml + ``` + +- Then you can start training of stage two using one card (other training method such as multiple cards or amp mixed-precision is similar to the above) by such command which depends on the model training result of stage one: + + ```bash + export CUDA_VISIBLE_DEVICES=0,1,2,3 + + python -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_manet_stage1 main.py --validate -c configs/segmentation/manet_stage2.yaml + ``` + +- In addition, you can customize and modify the parameter configuration to achieve the purpose of training/testing on different data sets. It is recommended that the naming method of the configuration file is `model_dataset name_file format_data format_sampling method.yaml` , Please refer to [config](../../tutorials/config.md) for parameter usage. + + + +## Test + +You can start testing by such command: + +```bash +python main.py --test -c configs/localization/bmn.yaml -w output/BMN/BMN_epoch_00009.pdparams -o DATASET.test_batch_size=1 +``` + +- You can download [our model](https://drive.google.com/file/d/1JjYNha40rtEYKKKFtDv06myvpxagl5dW/view?usp=sharing) decompress it and specify the path to `METRIC.ground_truth_filename` in config file. + +- Args `-w` is used to specifiy the model path,you can download [our model](https://drive.google.com/file/d/1JjYNha40rtEYKKKFtDv06myvpxagl5dW/view?usp=sharing) and decompress it for evaluation. + + +Test accuracy in DAVIS2017: + +| J@60 | AUC | +| :---: | :---: | +| 0.761 | 0.749 | + + +## Inference + +**Ongoing** diff --git a/applications/EIVideo/EIVideo/docs/zh-CN/manet.md b/applications/EIVideo/EIVideo/docs/zh-CN/manet.md new file mode 100644 index 0000000000000000000000000000000000000000..24a387175830b0d0ecd71c36da55bc085410fa96 --- /dev/null +++ b/applications/EIVideo/EIVideo/docs/zh-CN/manet.md @@ -0,0 +1,121 @@ +[English](../en/manet.md) | 简体中文 + +# Ma-Net视频切分模型 + +## 内容 + +- [模型简介](#模型简介) + +- [数据准备](#数据准备) + +- [模型训练](#模型训练) + +- [模型测试](#模型测试) + +- [模型推理](#模型推理) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install scikit-image +``` + +## 模型简介 + +这是CVPR2020论文"[Memory aggregation networks for efficient interactive video object segmentation](https://arxiv.org/abs/2003.13246)"的Paddle实现。 + +![avatar](../../../images/1836-teaser.gif) + +此代码目前支持在 DAVIS 数据集上进行模型测试和模型训练,并且将在之后提供对任何给定视频的模型推理。 + + +## 数据准备 + +下载 [DAVIS2017](https://data.vision.ee.ethz.ch/csergi/share/davis/DAVIS-2017-trainval-480p.zip) 和 [scribbles](https://data.vision.ee.ethz.ch/csergi/share/DAVIS-Interactive/DAVIS-2017-scribbles-trainval.zip) 到一个文件夹中。请参阅 [DAVIS](https://davischallenge.org/davis2017/code.html). + +如果您需要文件"DAVIS2017/ImageSets/2017/v_a_l_instances.txt",请参阅链接 https://drive.google.com/file/d/1aLPaQ_5lyAi3Lk3d2fOc_xewSrfcrQlc/view?usp=sharing + + +## 模型训练 + +#### 下载并添加预先训练的模型 + +1. 下载 [deeplabV3+ model pretrained on COCO](https://drive.google.com/file/d/15temSaxnKmGPvNxrKPN6W2lSsyGfCtTB/view?usp=sharing) 作为主干初始化参数,或通过 wget 下载 + + ```bash + wget https://drive.google.com/file/d/15temSaxnKmGPvNxrKPN6W2lSsyGfCtTB/view?usp=sharing + ``` + +2. 打开 `PaddleVideo/configs/segmentationer/manet_stage1.yaml,然后在`pretrained:`填写下载的模型权重存储路径 + + ```yaml + MODEL: #MODEL field + framework: "Manet" + backbone: + name: "DeepLab" + pretrained: fill in the path here + ``` + +#### Start training + +- 我们的训练过程包括两个阶段。 + + - 您可以通过以下命令使用一张卡开始第一阶段的训练: + + ```bash + python main.py -c configs/segmentation/manet.yaml + ``` + + - 使用多张卡进行训练,以加快训练过程。训练开始命令如下: + + ```bash + export CUDA_VISIBLE_DEVICES=0,1,2,3 + + python -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_manet_stage1 main.py -c configs/segmentation/manet.yaml + ``` + + - 使用混合精度训练以加快训练过程。训练开始命令如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + # frames data format + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4" --log_dir=log_manet_stage1 main.py --amp -c configs/segmentation/manet.yaml + ``` + +- 使用第一阶段的模型训练结果,您可以使用一张卡开始训练第二阶段(其他训练方法,如多张卡或混合精度类似于上述),命令如下: + + ```bash + export CUDA_VISIBLE_DEVICES=0,1,2,3 + + python -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_manet_stage1 main.py --validate -c configs/segmentation/manet_stage2.yaml + ``` + +- 此外,您可以自定义和修改参数配置,以达到在不同数据集上训练/测试的目的。建议配置文件的命名方法是 `model_dataset name_file format_data format_sampling method.yaml` ,请参考 [config](../../tutorials/config.md) 配置参数的方法。 + + + + +## 模型测试 + +您可以通过以下命令开始测试: + +```bash +python main.py --test -c configs/localization/bmn.yaml -w output/BMN/BMN_epoch_00009.pdparams -o DATASET.test_batch_size=1 +``` + +- 您可以下载[我们的模型](https://drive.google.com/file/d/1JjYNha40rtEYKKKFtDv06myvpxagl5dW/view?usp=sharing) 解压缩它,并在配置文件中指定`METRIC.ground_truth_filename` 的路径。 + +- 参数 `-w` 用于指定模型路径,您可以下载 [我们的模型](https://drive.google.com/file/d/1JjYNha40rtEYKKKFtDv06myvpxagl5dW/view?usp=sharing) 并解压缩以进行测试。 + + +测试精度在 DAVIS2017上: + +| J@60 | AUC | +| :---: | :---: | +| 0.761 | 0.749 | + + + +## 模型推理 diff --git a/applications/EIVideo/EIVideo/example/example.mp4 b/applications/EIVideo/EIVideo/example/example.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..283fe64cee927c2b1b4cca4c35756d9d09c0caca Binary files /dev/null and b/applications/EIVideo/EIVideo/example/example.mp4 differ diff --git a/applications/EIVideo/EIVideo/main.py b/applications/EIVideo/EIVideo/main.py new file mode 100644 index 0000000000000000000000000000000000000000..f691e7616f3b2116c2ea8bbfc6a59dc75d4b74f2 --- /dev/null +++ b/applications/EIVideo/EIVideo/main.py @@ -0,0 +1,116 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless requifFred by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import random + +import numpy as np +import paddle + +from EIVideo.paddlevideo.tasks import (test_model) +from EIVideo.paddlevideo.utils import get_config, get_dist_info +from EIVideo import EI_VIDEO_ROOT, join_root_path + +DEF_CONFIG_FILE_PATH = join_root_path("configs/manet.yaml") +DEF_PARAMS_FILE_PATH = join_root_path("model/default_manet.pdparams") + + +def parse_args(): + parser = argparse.ArgumentParser("PaddleVideo train script") + parser.add_argument('-c', + '--config', + type=str, + default=DEF_CONFIG_FILE_PATH, + help='config file path') + parser.add_argument('-o', + '--override', + action='append', + default=[], + help='config options to be overridden') + parser.add_argument('--test', + action='store_true', + help='whether to test a model') + parser.add_argument('--train_dali', + action='store_true', + help='whether to use dali to speed up training') + parser.add_argument('--multigrid', + action='store_true', + help='whether to use multigrid training') + parser.add_argument('-w', + '--weights', + type=str, + default=DEF_PARAMS_FILE_PATH, + help='weights for finetuning or testing') + parser.add_argument('--fleet', + action='store_true', + help='whether to use fleet run distributed training') + parser.add_argument('--amp', + action='store_true', + help='whether to open amp training.') + parser.add_argument( + '--validate', + action='store_true', + help='whether to evaluate the checkpoint during training') + parser.add_argument( + '--seed', + type=int, + default=None, + help='fixed all random seeds when the program is running') + parser.add_argument( + '--max_iters', + type=int, + default=None, + help='max iterations when training(this argonly used in test_tipc)') + parser.add_argument( + '-p', + '--profiler_options', + type=str, + default=None, + help='The option of profiler, which should be in format ' + '\"key1=value1;key2=value2;key3=value3\".') + parser.add_argument('--use_npu', + type=bool, + default=False, + help='whether use npu.') + + args = parser.parse_args() + return args + + +def main(**kwargs): + args = parse_args() + cfg = get_config(args.config, overrides=args.override) + # ToDo To AP-kai: 下面这行代码目的是更新配置,这样的话我们调用main(use_npu = Ture),这时cfg.use_npu就是Ture了 + for key, value in kwargs.items(): + cfg.__setattr__(key, value) + + # set seed if specified + seed = args.seed + if seed is not None: + assert isinstance( + seed, + int), f"seed must be a integer when specified, but got {seed}" + paddle.seed(seed) + np.random.seed(seed) + random.seed(seed) + + _, world_size = get_dist_info() + parallel = world_size != 1 + if parallel: + paddle.distributed.init_parallel_env() + final = test_model(cfg, weights=args.weights, parallel=parallel) + return final + + +if __name__ == '__main__': + main(video_path='example/example1.mp4', save_path='./output') diff --git a/applications/EIVideo/EIVideo/paddlevideo/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b03acf29696e71c21ed2d7bfc3a908b7f7c9c48 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .version import paddlevideo_version diff --git a/applications/EIVideo/EIVideo/paddlevideo/loader/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/loader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..232be145d3e2ad36bc7335ce60bd9f2bde3017fe --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/loader/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .builder import build_batch_pipeline +from .pipelines.compose import Compose + +__all__ = [ + 'build_batch_pipeline','Compose' +] + diff --git a/applications/EIVideo/EIVideo/paddlevideo/loader/builder.py b/applications/EIVideo/EIVideo/paddlevideo/loader/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..bce56b37567fd8cda93c90dcf327f18416ee4cbf --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/loader/builder.py @@ -0,0 +1,152 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import signal +import os +import paddle +from paddle.fluid.dataloader import BatchSampler +from paddle.io import DataLoader, DistributedBatchSampler + +from .pipelines.compose import Compose +from .registry import DATASETS, PIPELINES, DATALOADERS, BATCH_SAMPLERS, SAMPLERS +from ..utils import get_logger +from ..utils.build_utils import build +import numpy as np + +logger = get_logger("paddlevideo") + + +def build_pipeline(cfg): + """Build pipeline. + Args: + cfg (dict): root config dict. + """ + if cfg == None: + return + return Compose(cfg) + + +def build_dataset(cfg): + """Build dataset. + Args: + cfg (dict): root config dict. + + Returns: + dataset: dataset. + """ + # XXX: ugly code here! + cfg_dataset, cfg_pipeline = cfg + cfg_dataset.pipeline = build_pipeline(cfg_pipeline) + dataset = build(cfg_dataset, DATASETS, key="format") + return dataset + + +def build_sampler(cfg): + """Build batch_sampler. + Args: + cfg (dict): root config dict. + + Returns: + batch_sampler: batch_sampler. + """ + sampler = build(cfg, SAMPLERS) + return sampler + + +def build_batch_pipeline(cfg): + batch_pipeline = build(cfg, PIPELINES) + return batch_pipeline + + +def build_custom_dataloader(cfg): + custom_dataloader = build(cfg, DATALOADERS, key='dataloader') + return custom_dataloader + + +def build_dataloader(dataset, + batch_size, + num_workers, + places=None, + shuffle=True, + drop_last=True, + multigrid=False, + collate_fn_cfg=None, + **kwargs): + """Build Paddle Dataloader. + + XXX explain how the batch_sampler work! + + Args: + dataset (paddle.dataset): A PaddlePaddle dataset object. + batch_size (int): batch size on single card. + num_worker (int): num_worker + shuffle(bool): whether to shuffle the data at every epoch. + """ + + if not kwargs.get('sampler'): + batch_sampler = DistributedBatchSampler(dataset, + batch_size=batch_size, + shuffle=shuffle, + drop_last=drop_last) + else: + sampler = build_sampler(kwargs['sampler']) + batch_sampler = BatchSampler(dataset, + sampler=sampler, + batch_size=batch_size, + shuffle=shuffle, + drop_last=drop_last) + kwargs.update({'batch_sampler': batch_sampler}) + + # NOTE(shipping): when switch the mix operator on, such as: mixup, cutmix. + + # batch like: [[img, label, attibute, ...], [imgs, label, attribute, ...], ...] will recollate to: + # [[img, img, ...], [label, label, ...], [attribute, attribute, ...], ...] as using numpy.transpose. + + def mix_collate_fn(batch): + pipeline = build_batch_pipeline(collate_fn_cfg) + batch = pipeline(batch) + slots = [] + for items in batch: + for i, item in enumerate(items): + if len(slots) < len(items): + slots.append([item]) + else: + slots[i].append(item) + return [np.stack(slot, axis=0) for slot in slots] + + # if collate_fn_cfg is not None: + # ugly code here. collate_fn is mix op config + # collate_fn = mix_collate_fn(collate_fn_cfg) + + data_loader = DataLoader( + dataset, + places=places, + num_workers=num_workers, + collate_fn=mix_collate_fn if collate_fn_cfg is not None else None, + **kwargs) + + return data_loader + + +def term_mp(sig_num, frame): + """ kill all child processes + """ + pid = os.getpid() + pgid = os.getpgid(os.getpid()) + logger.info("main proc {} exit, kill process group " "{}".format(pid, pgid)) + os.killpg(pgid, signal.SIGKILL) + return + + +signal.signal(signal.SIGINT, term_mp) +signal.signal(signal.SIGTERM, term_mp) diff --git a/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6479891150e1e9a90344bcf086838d9e2da9f0e7 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .custom_transforms_f import Resize_manet, RandomCrop_manet, RandomHorizontalFlip_manet, ToTensor_manet, \ + RandomScale_manet + +__all__ = [ + 'Resize_manet', 'RandomCrop_manet', + 'RandomHorizontalFlip_manet', 'ToTensor_manet', 'RandomScale_manet', +] diff --git a/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/compose.py b/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/compose.py new file mode 100644 index 0000000000000000000000000000000000000000..76eb4ed4d436f692a25081dbe8efe9a9b9a11102 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/compose.py @@ -0,0 +1,76 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Sequence +from ..registry import PIPELINES +import traceback +from ...utils import build +from ...utils import get_logger + + +@PIPELINES.register() +class Compose(object): + """ + Composes several pipelines(include decode func, sample func, and transforms) together. + + Note: To deal with ```list``` type cfg temporaray, like: + + transform: + - Crop: # A list + attribute: 10 + - Resize: # A list + attribute: 20 + + every key of list will pass as the key name to build a module. + XXX: will be improved in the future. + + Args: + pipelines (list): List of transforms to compose. + Returns: + A compose object which is callable, __call__ for this Compose + object will call each given :attr:`transforms` sequencely. + """ + def __init__(self, pipelines): + #assert isinstance(pipelines, Sequence) + self.pipelines = [] + for p in pipelines.values(): + if isinstance(p, dict): + p = build(p, PIPELINES) + self.pipelines.append(p) + elif isinstance(p, list): + for t in p: + #XXX: to deal with old format cfg, ugly code here! + temp_dict = dict(name=list(t.keys())[0]) + for all_sub_t in t.values(): + if all_sub_t is not None: + temp_dict.update(all_sub_t) + + t = build(temp_dict, PIPELINES) + self.pipelines.append(t) + elif callable(p): + self.pipelines.append(p) + else: + raise TypeError(f'pipelines must be callable or a dict,' + f'but got {type(p)}') + def __call__(self, data): + for p in self.pipelines: + try: + data = p(data) + except Exception as e: + stack_info = traceback.format_exc() + logger = get_logger("paddlevideo") + logger.info("fail to perform transform [{}] with error: " + "{} and stack:\n{}".format(p, e, str(stack_info))) + raise e + return data diff --git a/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/custom_transforms_f.py b/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/custom_transforms_f.py new file mode 100644 index 0000000000000000000000000000000000000000..c2fc50633167f92a337aaa59be1cd17045856d74 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/loader/pipelines/custom_transforms_f.py @@ -0,0 +1,220 @@ +import os +import random +import cv2 +import numpy as np +import paddle +from PIL import Image +from davisinteractive.utils.operations import bresenham + +from ..registry import PIPELINES + +cv2.setNumThreads(0) +NEW_BRANCH = True + + +@PIPELINES.register() +class RandomScale_manet(object): + """Randomly resize the image and the ground truth to specified scales. + Args: + scales (list): the list of scales + """ + def __init__(self, scales=[0.75, 1, 1.25]): + self.scales = scales + + def __call__(self, sample): + + # Fixed range of scales + sc = self.scales[random.randint(0, len(self.scales) - 1)] + + for elem in sample.keys(): + if 'meta' in elem: + continue + tmp = sample[elem] + + if elem == 'img1' or elem == 'img2' or elem == 'ref_img': + flagval = cv2.INTER_CUBIC + else: + flagval = cv2.INTER_NEAREST + + tmp = cv2.resize(tmp, None, fx=sc, fy=sc, interpolation=flagval) + + sample[elem] = tmp + + return sample + + +@PIPELINES.register() +class Resize_manet(object): + """Rescale the image in a results to a given size. + + Args: + output_size (tuple or int): Desired output size. If tuple, output is + matched to output_size. If int, smaller of image edges is matched + to output_size keeping aspect ratio the same. + """ + def __init__(self, output_size): + assert isinstance(output_size, (int, list)) + if isinstance(output_size, int): + self.output_size = (output_size, output_size) + else: + self.output_size = output_size + + # self.seg_interpolation = cv2.INTER_CUBIC if is_continuous else cv2.INTER_NEAREST + # self.fix = fix + + def __call__(self, results): + img1 = results['img1'] + h, w = img1.shape[:2] + if self.output_size == (h, w): + return results + + else: + new_h, new_w = self.output_size + new_h, new_w = int(new_h), int(new_w) + for elem in results.keys(): + if 'meta' in elem: + continue + tmp = results[elem] + if elem == 'img1' or elem == 'img2' or elem == 'ref_img': + flagval = cv2.INTER_CUBIC + else: + flagval = cv2.INTER_NEAREST + + tmp = cv2.resize(tmp, dsize=(new_w, new_h), interpolation=flagval) + results[elem] = tmp + return results + + +@PIPELINES.register() +class RandomCrop_manet(object): + """Crop randomly the image in a results. + + Args: + output_size (tuple or int): Desired output size. If int, square crop + is made. + """ + def __init__(self, output_size, step=None): + assert isinstance(output_size, (int, list)) + if isinstance(output_size, int): + self.output_size = (output_size, output_size) + else: + assert len(output_size) == 2 + self.output_size = output_size + self.step = step + + def __call__(self, results): + + image = results['img1'] + h, w = image.shape[:2] + new_h, new_w = self.output_size + + new_h = h if new_h >= h else new_h + new_w = w if new_w >= w else new_w + is_contain_obj = False + + # while (not is_contain_obj) and (step < 5): + if self.step is None: + while not is_contain_obj: + # step += 1 + top = np.random.randint(0, h - new_h + 1) + left = np.random.randint(0, w - new_w + 1) + ref_scribble_label = results['ref_scribble_label'] + new_ref_scribble_label = ref_scribble_label[top:top + new_h, + left:left + new_w] + if len(np.unique(new_ref_scribble_label)) == 1: + continue + else: + + for elem in results.keys(): + if 'meta' in elem: + continue + + tmp = results[elem] + tmp = tmp[top:top + new_h, left:left + new_w] + results[elem] = tmp + break + else: + st = 0 + while not is_contain_obj and st < self.step: + st += 1 + top = np.random.randint(0, h - new_h + 1) + left = np.random.randint(0, w - new_w + 1) + ref_scribble_label = results['ref_scribble_label'] + new_ref_scribble_label = ref_scribble_label[top:top + new_h, + left:left + new_w] + if len(np.unique( + new_ref_scribble_label)) == 1 or st < self.step - 1: + continue + else: + + for elem in results.keys(): + if 'meta' in elem: + continue + + tmp = results[elem] + tmp = tmp[top:top + new_h, left:left + new_w] + results[elem] = tmp + break + + return results + + +@PIPELINES.register() +class RandomHorizontalFlip_manet(object): + """Horizontally flip the given image and ground truth randomly with a probability of 0.5.""" + def __init__(self, prob): + self.p = prob + + def __call__(self, results): + + if random.random() < self.p: + for elem in results.keys(): + if 'meta' in elem: + continue + tmp = results[elem] + tmp = cv2.flip(tmp, flipCode=1) + results[elem] = tmp + + return results + + +@PIPELINES.register() +class ToTensor_manet(object): + """Convert ndarrays in results to Tensors.""" + def __call__(self, results): + + for elem in results.keys(): + if 'meta' in elem: + continue + tmp = results[elem] + + if tmp.ndim == 2: + tmp = tmp[:, :, np.newaxis] + else: + tmp = tmp / 255. + tmp -= (0.485, 0.456, 0.406) + tmp /= (0.229, 0.224, 0.225) + tmp = tmp.transpose([2, 0, 1]) + results[elem] = paddle.to_tensor(tmp) + return results + + +def gt_from_scribble(scr, dilation=11, nocare_area=21): + # Compute foreground + if scr.max() == 1: + kernel_fg = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, + (dilation, dilation)) + fg = cv2.dilate(scr.astype(np.uint8), + kernel=kernel_fg).astype(scr.dtype) + else: + fg = scr + + # Compute nocare area + if nocare_area is None: + nocare = None + else: + kernel_nc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, + (nocare_area, nocare_area)) + nocare = cv2.dilate(fg, kernel=kernel_nc) - fg + + return fg, nocare diff --git a/applications/EIVideo/EIVideo/paddlevideo/loader/registry.py b/applications/EIVideo/EIVideo/paddlevideo/loader/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..0af97a1ef1dfd878fb01e1cc753a03758aa750fa --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/loader/registry.py @@ -0,0 +1,21 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..utils import Registry + +PIPELINES = Registry("pipeline") +DATASETS = Registry("datasets") +SAMPLERS = Registry("sampler") +BATCH_SAMPLERS = Registry("batch_sampler") +DATALOADERS = Registry("dataloader") diff --git a/applications/EIVideo/EIVideo/paddlevideo/metrics/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..844dc3d61f554d58bba71d5fe4ce9a63857eabaf --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/metrics/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .vos_metric import VOSMetric +from .build import build_metric + +__all__ = [ + 'VOSMetric', "build_metric" +] diff --git a/applications/EIVideo/EIVideo/paddlevideo/metrics/base.py b/applications/EIVideo/EIVideo/paddlevideo/metrics/base.py new file mode 100644 index 0000000000000000000000000000000000000000..06302597bfb2b2a74883d4a5f5e3d17354812300 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/metrics/base.py @@ -0,0 +1,30 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from abc import abstractmethod +from EIVideo.paddlevideo.utils import get_dist_info + + +class BaseMetric(object): + def __init__(self, data_size, batch_size, log_interval=1, **kwargs): + self.data_size = data_size + self.batch_size = batch_size + _, self.world_size = get_dist_info() + self.log_interval = log_interval + + @abstractmethod + def update(self): + raise NotImplemented + + @abstractmethod + def accumulate(self): + raise NotImplemented diff --git a/applications/EIVideo/EIVideo/paddlevideo/metrics/build.py b/applications/EIVideo/EIVideo/paddlevideo/metrics/build.py new file mode 100644 index 0000000000000000000000000000000000000000..82e4b502611f3190e5f2c67d9e70fd2201be8233 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/metrics/build.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .registry import METRIC +from ..utils import build + + +def build_metric(cfg): + return build(cfg, METRIC) diff --git a/applications/EIVideo/EIVideo/paddlevideo/metrics/registry.py b/applications/EIVideo/EIVideo/paddlevideo/metrics/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..221444023345011cfe6f0922fa939a635b46d738 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/metrics/registry.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..utils import Registry + +METRIC = Registry('metric') diff --git a/applications/EIVideo/EIVideo/paddlevideo/metrics/vos_metric.py b/applications/EIVideo/EIVideo/paddlevideo/metrics/vos_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..618b7d656b2ae9724b3705625b81cc5e20c25373 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/metrics/vos_metric.py @@ -0,0 +1,279 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +import paddle +import zipfile +import time +from PIL import Image + +from paddle.io import DataLoader + +from .registry import METRIC +from .base import BaseMetric +from EIVideo.paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@METRIC.register +class VOSMetric(BaseMetric): + def __init__(self, + data_size, + batch_size, + result_root, + zip_dir, + log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.video_num = 0 + self.total_time = 0 + self.total_frame = 0 + self.total_sfps = 0 + self.total_video_num = data_size + self.count = 0 + self.result_root = result_root + self.zip_dir = zip_dir + + def update(self, batch_id, data, model): + """update metrics during each iter + """ + self.video_num += 1 + seq_dataset = data + seq_name = seq_dataset.seq_name + + logger.info('Prcessing Seq {} [{}/{}]:'.format(seq_name, + self.video_num, + self.total_video_num)) + seq_dataloader = DataLoader(seq_dataset, + return_list=True, + batch_size=1, + shuffle=False, + num_workers=0) + seq_total_time = 0 + seq_total_frame = 0 + ref_embeddings = [] + ref_masks = [] + prev_embedding = [] + prev_mask = [] + with paddle.no_grad(): + for frame_idx, samples in enumerate(seq_dataloader): + time_start = time.time() + all_preds = [] + join_label = None + for aug_idx in range(len(samples)): + if len(ref_embeddings) <= aug_idx: + ref_embeddings.append([]) + ref_masks.append([]) + prev_embedding.append(None) + prev_mask.append(None) + + sample = samples[aug_idx] + ref_emb = ref_embeddings[aug_idx] + ref_m = ref_masks[aug_idx] + prev_emb = prev_embedding[aug_idx] + prev_m = prev_mask[aug_idx] + + current_img = sample['current_img'] + if 'current_label' in sample.keys(): + current_label = sample['current_label'] + current_label = paddle.to_tensor(current_label) + else: + current_label = None + + obj_num = sample['meta']['obj_num'] + imgname = sample['meta']['current_name'] + ori_height = sample['meta']['height'] + ori_width = sample['meta']['width'] + current_img = current_img + obj_num = obj_num + bs, _, h, w = current_img.shape + data_batch = [ + ref_emb, ref_m, prev_emb, prev_m, current_img, + [ori_height, ori_width], obj_num + ] + + all_pred, current_embedding = model(data_batch, + mode='test') + + if frame_idx == 0: + if current_label is None: + logger.info( + "No first frame label in Seq {}.".format( + seq_name)) + ref_embeddings[aug_idx].append(current_embedding) + ref_masks[aug_idx].append(current_label) + + prev_embedding[aug_idx] = current_embedding + prev_mask[aug_idx] = current_label + else: + if sample['meta']['flip']: #False + all_pred = self.flip_tensor(all_pred, 3) + # In YouTube-VOS, not all the objects appear in the first frame for the first time. Thus, we + # have to introduce new labels for new objects, if necessary. + if not sample['meta']['flip'] and not ( + current_label is None) and join_label is None: + join_label = paddle.cast(current_label, + dtype='int64') + all_preds.append(all_pred) + if current_label is not None: + ref_embeddings[aug_idx].append(current_embedding) + prev_embedding[aug_idx] = current_embedding + + if frame_idx > 0: + all_preds = paddle.concat(all_preds, axis=0) + all_preds = paddle.mean( + all_preds, axis=0) #average results if augmentation + pred_label = paddle.argmax(all_preds, axis=0) + if join_label is not None: + join_label = paddle.squeeze(paddle.squeeze(join_label, + axis=0), + axis=0) + keep = paddle.cast((join_label == 0), dtype="int64") + pred_label = pred_label * keep + join_label * (1 - + keep) + pred_label = pred_label + current_label = paddle.reshape( + pred_label, shape=[1, 1, ori_height, ori_width]) + flip_pred_label = self.flip_tensor(pred_label, 1) + flip_current_label = paddle.reshape( + flip_pred_label, shape=[1, 1, ori_height, ori_width]) + + for aug_idx in range(len(samples)): + if join_label is not None: + if samples[aug_idx]['meta']['flip']: + ref_masks[aug_idx].append(flip_current_label) + else: + ref_masks[aug_idx].append(current_label) + if samples[aug_idx]['meta']['flip']: + prev_mask[aug_idx] = flip_current_label + else: + prev_mask[ + aug_idx] = current_label #update prev_mask + + one_frametime = time.time() - time_start + seq_total_time += one_frametime + seq_total_frame += 1 + obj_num = obj_num.numpy()[0].item() + logger.info('Frame: {}, Obj Num: {}, Time: {}'.format( + imgname[0], obj_num, one_frametime)) + self.save_mask( + pred_label, + os.path.join(self.result_root, seq_name, + imgname[0].split('.')[0] + '.png')) + else: + one_frametime = time.time() - time_start + seq_total_time += one_frametime + logger.info('Ref Frame: {}, Time: {}'.format( + imgname[0], one_frametime)) + + del (ref_embeddings) + del (ref_masks) + del (prev_embedding) + del (prev_mask) + del (seq_dataset) + del (seq_dataloader) + + seq_avg_time_per_frame = seq_total_time / seq_total_frame + self.total_time += seq_total_time + self.total_frame += seq_total_frame + total_avg_time_per_frame = self.total_time / self.total_frame + self.total_sfps += seq_avg_time_per_frame + avg_sfps = self.total_sfps / (batch_id + 1) + logger.info("Seq {} FPS: {}, Total FPS: {}, FPS per Seq: {}".format( + seq_name, 1. / seq_avg_time_per_frame, + 1. / total_avg_time_per_frame, 1. / avg_sfps)) + + def flip_tensor(self, tensor, dim=0): + inv_idx = paddle.cast(paddle.arange(tensor.shape[dim] - 1, -1, -1), + dtype="int64") + tensor = paddle.index_select(x=tensor, index=inv_idx, axis=dim) + return tensor + + def save_mask(self, mask_tensor, path): + _palette = [ + 0, 0, 0, 128, 0, 0, 0, 128, 0, 128, 128, 0, 0, 0, 128, 128, 0, 128, + 0, 128, 128, 128, 128, 128, 64, 0, 0, 191, 0, 0, 64, 128, 0, 191, + 128, 0, 64, 0, 128, 191, 0, 128, 64, 128, 128, 191, 128, 128, 0, + 64, 0, 128, 64, 0, 0, 191, 0, 128, 191, 0, 0, 64, 128, 128, 64, + 128, 22, 22, 22, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 26, + 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 32, 32, + 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, + 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 43, + 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, + 49, 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, + 55, 55, 56, 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, + 61, 61, 61, 62, 62, 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, + 66, 67, 67, 67, 68, 68, 68, 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, + 72, 72, 73, 73, 73, 74, 74, 74, 75, 75, 75, 76, 76, 76, 77, 77, 77, + 78, 78, 78, 79, 79, 79, 80, 80, 80, 81, 81, 81, 82, 82, 82, 83, 83, + 83, 84, 84, 84, 85, 85, 85, 86, 86, 86, 87, 87, 87, 88, 88, 88, 89, + 89, 89, 90, 90, 90, 91, 91, 91, 92, 92, 92, 93, 93, 93, 94, 94, 94, + 95, 95, 95, 96, 96, 96, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, + 100, 100, 101, 101, 101, 102, 102, 102, 103, 103, 103, 104, 104, + 104, 105, 105, 105, 106, 106, 106, 107, 107, 107, 108, 108, 108, + 109, 109, 109, 110, 110, 110, 111, 111, 111, 112, 112, 112, 113, + 113, 113, 114, 114, 114, 115, 115, 115, 116, 116, 116, 117, 117, + 117, 118, 118, 118, 119, 119, 119, 120, 120, 120, 121, 121, 121, + 122, 122, 122, 123, 123, 123, 124, 124, 124, 125, 125, 125, 126, + 126, 126, 127, 127, 127, 128, 128, 128, 129, 129, 129, 130, 130, + 130, 131, 131, 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, + 135, 135, 135, 136, 136, 136, 137, 137, 137, 138, 138, 138, 139, + 139, 139, 140, 140, 140, 141, 141, 141, 142, 142, 142, 143, 143, + 143, 144, 144, 144, 145, 145, 145, 146, 146, 146, 147, 147, 147, + 148, 148, 148, 149, 149, 149, 150, 150, 150, 151, 151, 151, 152, + 152, 152, 153, 153, 153, 154, 154, 154, 155, 155, 155, 156, 156, + 156, 157, 157, 157, 158, 158, 158, 159, 159, 159, 160, 160, 160, + 161, 161, 161, 162, 162, 162, 163, 163, 163, 164, 164, 164, 165, + 165, 165, 166, 166, 166, 167, 167, 167, 168, 168, 168, 169, 169, + 169, 170, 170, 170, 171, 171, 171, 172, 172, 172, 173, 173, 173, + 174, 174, 174, 175, 175, 175, 176, 176, 176, 177, 177, 177, 178, + 178, 178, 179, 179, 179, 180, 180, 180, 181, 181, 181, 182, 182, + 182, 183, 183, 183, 184, 184, 184, 185, 185, 185, 186, 186, 186, + 187, 187, 187, 188, 188, 188, 189, 189, 189, 190, 190, 190, 191, + 191, 191, 192, 192, 192, 193, 193, 193, 194, 194, 194, 195, 195, + 195, 196, 196, 196, 197, 197, 197, 198, 198, 198, 199, 199, 199, + 200, 200, 200, 201, 201, 201, 202, 202, 202, 203, 203, 203, 204, + 204, 204, 205, 205, 205, 206, 206, 206, 207, 207, 207, 208, 208, + 208, 209, 209, 209, 210, 210, 210, 211, 211, 211, 212, 212, 212, + 213, 213, 213, 214, 214, 214, 215, 215, 215, 216, 216, 216, 217, + 217, 217, 218, 218, 218, 219, 219, 219, 220, 220, 220, 221, 221, + 221, 222, 222, 222, 223, 223, 223, 224, 224, 224, 225, 225, 225, + 226, 226, 226, 227, 227, 227, 228, 228, 228, 229, 229, 229, 230, + 230, 230, 231, 231, 231, 232, 232, 232, 233, 233, 233, 234, 234, + 234, 235, 235, 235, 236, 236, 236, 237, 237, 237, 238, 238, 238, + 239, 239, 239, 240, 240, 240, 241, 241, 241, 242, 242, 242, 243, + 243, 243, 244, 244, 244, 245, 245, 245, 246, 246, 246, 247, 247, + 247, 248, 248, 248, 249, 249, 249, 250, 250, 250, 251, 251, 251, + 252, 252, 252, 253, 253, 253, 254, 254, 254, 255, 255, 255 + ] + mask = mask_tensor.cpu().numpy().astype('uint8') + mask = Image.fromarray(mask).convert('P') + mask.putpalette(_palette) + mask.save(path) + + def zip_folder(self, source_folder, zip_dir): + f = zipfile.ZipFile(zip_dir, 'w', zipfile.ZIP_DEFLATED) + pre_len = len(os.path.dirname(source_folder)) + for dirpath, dirnames, filenames in os.walk(source_folder): + for filename in filenames: + pathfile = os.path.join(dirpath, filename) + arcname = pathfile[pre_len:].strip(os.path.sep) + f.write(pathfile, arcname) + f.close() + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + self.zip_folder(self.result_root, self.zip_dir) + logger.info('Save result to {}.'.format(self.zip_dir)) diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7d5ddcd63db615c7b1a8bae11e8b76e5f8999756 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/__init__.py @@ -0,0 +1,27 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .backbones import DeepLab +from .builder import (build_backbone, build_head, build_localizer, build_loss, + build_recognizer) +from .heads import IntVOS +from .registry import (BACKBONES, DETECTORS, HEADS, LOCALIZERS, LOSSES, + PARTITIONERS, RECOGNIZERS, ROI_EXTRACTORS) +from .weight_init import kaiming_normal_, trunc_normal_, weight_init_ + +__all__ = [ + 'BACKBONES', 'HEADS', 'RECOGNIZERS', 'LOCALIZERS', 'PARTITIONERS', + 'LOSSES', 'build_recognizer', 'build_localizer', 'build_head', + 'build_backbone', 'build_loss', 'DETECTORS', 'kaiming_normal_', 'trunc_normal_', + 'weight_init_', 'DeepLab', 'IntVOS' +] diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2a715feb3b19c51838b09ab888409db6a96fd566 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .deeplab_manet import DeepLab + +__all__ = ['DeepLab'] diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/aspp_manet.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/aspp_manet.py new file mode 100644 index 0000000000000000000000000000000000000000..819c43bc1bf9c5cc5e6b4a5184281aa88a8d69bd --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/aspp_manet.py @@ -0,0 +1,124 @@ +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from EIVideo.paddlevideo.utils.manet_utils import kaiming_normal_ + + +class _ASPPModule(nn.Layer): + def __init__(self, inplanes, planes, kernel_size, padding, dilation, + BatchNorm): + super(_ASPPModule, self).__init__() + self.atrous_conv = nn.Conv2D(inplanes, + planes, + kernel_size=kernel_size, + stride=1, + padding=padding, + dilation=dilation, + bias_attr=False) + self.bn = BatchNorm(planes) + self.relu = nn.ReLU(True) + + self._init_weight() + + def forward(self, x): + x = self.atrous_conv(x) + x = self.bn(x) + + return self.relu(x) + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + kaiming_normal_(m.weight) + elif isinstance(m, nn.BatchNorm2D): + from EIVideo.paddlevideo.utils.manet_utils import fill_ + fill_(m.weight, 1) + from EIVideo.paddlevideo.utils.manet_utils import zero_ + zero_(m.bias) + + +class ASPP(nn.Layer): + def __init__(self, backbone, output_stride, BatchNorm): + super(ASPP, self).__init__() + if backbone == 'drn': + inplanes = 512 + elif backbone == 'mobilenet': + inplanes = 320 + else: + inplanes = 2048 + if output_stride == 16: + dilations = [1, 6, 12, 18] + elif output_stride == 8: + dilations = [1, 12, 24, 36] + else: + raise NotImplementedError + + self.aspp1 = _ASPPModule(inplanes, + 256, + 1, + padding=0, + dilation=dilations[0], + BatchNorm=BatchNorm) + self.aspp2 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[1], + dilation=dilations[1], + BatchNorm=BatchNorm) + self.aspp3 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[2], + dilation=dilations[2], + BatchNorm=BatchNorm) + self.aspp4 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[3], + dilation=dilations[3], + BatchNorm=BatchNorm) + + self.global_avg_pool = nn.Sequential( + nn.AdaptiveAvgPool2D((1, 1)), + nn.Conv2D(inplanes, 256, 1, stride=1, bias_attr=False), + BatchNorm(256), nn.ReLU()) + self.conv1 = nn.Conv2D(1280, 256, 1, bias_attr=False) + self.bn1 = BatchNorm(256) + self.relu = nn.ReLU(True) + self.dropout = nn.Dropout(0.1) + self._init_weight() + + def forward(self, x): + x1 = self.aspp1(x) + x2 = self.aspp2(x) + x3 = self.aspp3(x) + x4 = self.aspp4(x) + x5 = self.global_avg_pool(x) + x5 = F.interpolate(x5, + size=x4.shape[2:], + mode='bilinear', + align_corners=True) + x = paddle.concat((x1, x2, x3, x4, x5), axis=1) + + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + return x + return self.dropout(x) + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + # n = m._kernel_size[0] * m._kernel_size[1] * m._out_channels + # m.weight.normal_(0, math.sqrt(2. / n)) + kaiming_normal_(m.weight) + elif isinstance(m, nn.BatchNorm2D): + from EIVideo.paddlevideo.utils.manet_utils import fill_ + fill_(m.weight, 1) + from EIVideo.paddlevideo.utils.manet_utils import zero_ + zero_(m.bias) + + +def build_aspp(backbone, output_stride, BatchNorm): + return ASPP(backbone, output_stride, BatchNorm) diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/decoder_manet.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/decoder_manet.py new file mode 100644 index 0000000000000000000000000000000000000000..9f80dd41dfeccb3f2ff62d385a9f895c9588facc --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/decoder_manet.py @@ -0,0 +1,65 @@ +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from EIVideo.paddlevideo.utils.manet_utils import kaiming_normal_ + + +class Decoder(nn.Layer): + def __init__(self, num_classes, backbone, BatchNorm): + super(Decoder, self).__init__() + if backbone == 'resnet' or backbone == 'drn' or backbone == 'resnet_edge': + low_level_inplanes = 256 + elif backbone == 'xception': + low_level_inplanes = 128 + elif backbone == 'mobilenet': + low_level_inplanes = 24 + else: + raise NotImplementedError + + self.conv1 = nn.Conv2D(low_level_inplanes, 48, 1, bias_attr=False) + self.bn1 = BatchNorm(48) + self.relu = nn.ReLU(True) + self.last_conv = nn.Sequential( + nn.Conv2D(304, + 256, + kernel_size=3, + stride=1, + padding=1, + bias_attr=False), BatchNorm(256), nn.ReLU(True), + nn.Sequential(), + nn.Conv2D(256, + 256, + kernel_size=3, + stride=1, + padding=1, + bias_attr=False), BatchNorm(256), nn.ReLU(True), + nn.Sequential()) + self._init_weight() + + def forward(self, x, low_level_feat): + low_level_feat = self.conv1(low_level_feat) + low_level_feat = self.bn1(low_level_feat) + low_level_feat = self.relu(low_level_feat) + + x = F.interpolate(x, + size=low_level_feat.shape[2:], + mode='bilinear', + align_corners=True) + x = paddle.concat((x, low_level_feat), axis=1) + x = self.last_conv(x) + + return x + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + kaiming_normal_(m.weight) + elif isinstance(m, nn.BatchNorm2D): + from EIVideo.paddlevideo.utils.manet_utils import fill_ + fill_(m.weight, 1) + from EIVideo.paddlevideo.utils.manet_utils import zero_ + zero_(m.bias) + + +def build_decoder(num_classes, backbone, BatchNorm): + return Decoder(num_classes, backbone, BatchNorm) diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/deeplab_manet.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/deeplab_manet.py new file mode 100644 index 0000000000000000000000000000000000000000..d188dc14304fd5ee8d38d3e6ff9a6309fe0547e7 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/deeplab_manet.py @@ -0,0 +1,90 @@ +import paddle +import paddle.nn as nn + +from ..registry import BACKBONES +from EIVideo.paddlevideo.modeling.backbones.aspp_manet import build_aspp +from EIVideo.paddlevideo.modeling.backbones.decoder_manet import build_decoder +from EIVideo.paddlevideo.modeling.backbones.resnet_manet import build_backbone + + +class FrozenBatchNorm2d(nn.Layer): + def __init__(self, n): + super(FrozenBatchNorm2d, self).__init__() + self.register_buffer("weight", paddle.ones(n)) + self.register_buffer("bias", paddle.zeros(n)) + self.register_buffer("running_mean", paddle.zeros(n)) + self.register_buffer("running_var", paddle.ones(n)) + + def forward(self, x): + if x.dtype == paddle.float16: + self.weight = self.weight.half() + self.bias = self.bias.half() + self.running_mean = self.running_mean.half() + self.running_var = self.running_var.half() + scale = self.weight * self.running_var.rsqrt() + bias = self.bias - self.running_mean * scale + scale = scale.reshape(1, -1, 1, 1) + bias = bias.reshape(1, -1, 1, 1) + return x * scale + bias + + +@BACKBONES.register() +class DeepLab(nn.Layer): + def __init__(self, + backbone='resnet', + output_stride=16, + num_classes=21, + freeze_bn=False, + pretrained=None): + super(DeepLab, self).__init__() + if backbone == 'drn': + output_stride = 8 + if freeze_bn == True: + print("Use frozen BN in DeepLab") + BatchNorm = FrozenBatchNorm2d + else: + BatchNorm = nn.BatchNorm2D + + self.backbone = build_backbone(output_stride, BatchNorm, pretrained) + self.aspp = build_aspp(backbone, output_stride, BatchNorm) + self.decoder = build_decoder(num_classes, backbone, BatchNorm) + + + def forward(self, input): + x, low_level_feat = self.backbone(input) + x = self.aspp(x) + x = self.decoder(x, low_level_feat) + return x + + def freeze_bn(self): + for m in self.sublayers(): + if isinstance(m, nn.BatchNorm2D): + m.eval() + + def get_1x_lr_params(self): + modules = [self.backbone] + for i in range(len(modules)): + for m in modules[i].named_modules(): + if isinstance(m[1], nn.Conv2D) or isinstance( + m[1], nn.BatchNorm2D): + for p in m[1].parameters(): + if p.requires_grad: + yield p + + def get_10x_lr_params(self): + modules = [self.aspp, self.decoder] + for i in range(len(modules)): + for m in modules[i].named_modules(): + if isinstance(m[1], nn.Conv2D) or isinstance( + m[1], nn.BatchNorm2D): + for p in m[1].parameters(): + if p.requires_grad: + yield p + + +if __name__ == "__main__": + model = DeepLab(backbone='resnet', output_stride=16) + model.eval() + input = paddle.rand([2, 3, 513, 513]) + output = model(input) + print(output.shape) diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/resnet_manet.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/resnet_manet.py new file mode 100644 index 0000000000000000000000000000000000000000..2a490956d68695b51423c55b072bf3c7c9051451 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/backbones/resnet_manet.py @@ -0,0 +1,245 @@ +import paddle.nn as nn +# from reprod_log.utils import paddle2np + +from EIVideo.paddlevideo.utils.manet_utils import fill_, zero_ + + +class Bottleneck(nn.Layer): + expansion = 4 + + def __init__(self, + inplanes, + planes, + stride=1, + dilation=1, + downsample=None, + BatchNorm=None): + super(Bottleneck, self).__init__() + self.conv1 = nn.Conv2D(inplanes, planes, kernel_size=1, bias_attr=False) + self.bn1 = BatchNorm(planes) + self.conv2 = nn.Conv2D(planes, + planes, + kernel_size=3, + stride=stride, + dilation=dilation, + padding=dilation, + bias_attr=False) + self.bn2 = BatchNorm(planes) + self.conv3 = nn.Conv2D(planes, + planes * 4, + kernel_size=1, + bias_attr=False) + self.bn3 = BatchNorm(planes * 4) + self.relu = nn.ReLU() + self.downsample = downsample + self.stride = stride + self.dilation = dilation + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class ResNet(nn.Layer): + def __init__(self, + block, + layers, + output_stride, + BatchNorm, + pretrained=None): + self.inplanes = 64 + super(ResNet, self).__init__() + blocks = [1, 2, 4] + if output_stride == 16: + strides = [1, 2, 2, 1] + dilations = [1, 1, 1, 2] + elif output_stride == 8: + strides = [1, 2, 1, 1] + dilations = [1, 1, 2, 4] + else: + raise NotImplementedError + + # Modules + self.conv1 = nn.Conv2D(3, + 64, + kernel_size=7, + stride=2, + padding=3, + bias_attr=False) + self.bn1 = BatchNorm(64) + self.relu = nn.ReLU() + self.maxpool = nn.MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.layer1 = self._make_layer(block, + 64, + layers[0], + stride=strides[0], + dilation=dilations[0], + BatchNorm=BatchNorm) + self.layer2 = self._make_layer(block, + 128, + layers[1], + stride=strides[1], + dilation=dilations[1], + BatchNorm=BatchNorm) + self.layer3 = self._make_layer(block, + 256, + layers[2], + stride=strides[2], + dilation=dilations[2], + BatchNorm=BatchNorm) + self.layer4 = self._make_MG_unit(block, + 512, + blocks=blocks, + stride=strides[3], + dilation=dilations[3], + BatchNorm=BatchNorm) + self.init_weight() + + + + def _make_layer(self, + block, + planes, + blocks, + stride=1, + dilation=1, + BatchNorm=None): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D(self.inplanes, + planes * block.expansion, + kernel_size=1, + stride=stride, + bias_attr=False), + BatchNorm(planes * block.expansion), + ) + + layers = [] + layers.append( + block(self.inplanes, planes, stride, dilation, downsample, + BatchNorm)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append( + block(self.inplanes, + planes, + dilation=dilation, + BatchNorm=BatchNorm)) + + return nn.Sequential(*layers) + + def _make_MG_unit(self, + block, + planes, + blocks, + stride=1, + dilation=1, + BatchNorm=None): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D(self.inplanes, + planes * block.expansion, + kernel_size=1, + stride=stride, + bias_attr=False), + BatchNorm(planes * block.expansion), + ) + + layers = [] + layers.append( + block(self.inplanes, + planes, + stride, + dilation=blocks[0] * dilation, + downsample=downsample, + BatchNorm=BatchNorm)) + self.inplanes = planes * block.expansion + for i in range(1, len(blocks)): + layers.append( + block(self.inplanes, + planes, + stride=1, + dilation=blocks[i] * dilation, + BatchNorm=BatchNorm)) + + return nn.Sequential(*layers) + + def forward(self, input): + x = self.conv1(input) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + + x = self.layer1(x) + low_level_feat = x + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + return x, low_level_feat + + def init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + n = m._kernel_size[0] * m._kernel_size[1] * m._out_channels + fill_(m.weight, 1) + elif isinstance(m, nn.BatchNorm2D): + fill_(m.weight, 1) + zero_(m.bias) + return self.sublayers() + + + + +def ResNet101(output_stride, BatchNorm, pretrained=None): + """Constructs a ResNet-101 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(Bottleneck, [3, 4, 23, 3], + output_stride, + BatchNorm, + pretrained=pretrained) + return model + + +def build_backbone(output_stride, BatchNorm, pretrained): + return ResNet101(output_stride, BatchNorm, pretrained) + + +if __name__ == "__main__": + import paddle + + model = ResNet101(BatchNorm=nn.BatchNorm2D, + pretrained=True, + output_stride=8) + input = paddle.rand([1, 3, 512, 512]) + output, low_level_feat = model(input) + print(output.shape) + print(low_level_feat.shape) + import json + + with open('output.txt', 'w') as f: + json.dump(output.tolist(), f) + with open('low_level_feat.txt', 'w') as f: + json.dump(low_level_feat.tolist(), f) diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/builder.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..d131ca48e7806aac96e81bee7962894ba7e1bfa0 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/builder.py @@ -0,0 +1,125 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .registry import BACKBONES, HEADS, LOSSES, RECOGNIZERS, LOCALIZERS, ROI_EXTRACTORS, DETECTORS, BBOX_ASSIGNERS, BBOX_SAMPLERS, BBOX_CODERS, PARTITIONERS, MULTIMODAL, SEGMENT +from ..utils import build +from .registry import (BACKBONES, BBOX_ASSIGNERS, BBOX_CODERS, BBOX_SAMPLERS, + DETECTORS, ESTIMATORS, HEADS, LOCALIZERS, LOSSES, + MULTIMODAL, PARTITIONERS, RECOGNIZERS, ROI_EXTRACTORS) + + +def build_backbone(cfg): + """Build backbone.""" + return build(cfg, BACKBONES) + + +def build_roi_extractor(cfg): + """Build roi extractor.""" + return build(cfg, ROI_EXTRACTORS) + + +def build_assigner(cfg, **default_args): + """Builder of box assigner.""" + return build(cfg, BBOX_ASSIGNERS) + + +def build_sampler(cfg, **default_args): + """Builder of box batch_sampler.""" + return build(cfg, BBOX_SAMPLERS) + + +def build_roi_extractor(cfg): + """Build roi extractor.""" + return build(cfg, ROI_EXTRACTORS) + + +def build_assigner(cfg, **default_args): + """Builder of box assigner.""" + return build(cfg, BBOX_ASSIGNERS) + + +def build_sampler(cfg, **default_args): + """Builder of box batch_sampler.""" + return build(cfg, BBOX_SAMPLERS) + + +def build_head(cfg): + """Build head.""" + return build(cfg, HEADS) + + +def build_loss(cfg): + """Build loss.""" + return build(cfg, LOSSES) + + +def build_recognizer(cfg): + """Build recognizer.""" + return build(cfg, RECOGNIZERS, key='framework') + + +def build_localizer(cfg): + """Build localizer.""" + return build(cfg, LOCALIZERS, key='framework') + + +def build_segmentationer(cfg): + """Build detector.""" + return build(cfg, SEGMENT, key='framework') + + +def build_partitioner(cfg): + """Build partitioner.""" + return build(cfg, PARTITIONERS, key='framework') + + +def build_estimator(cfg): + """Build estimator.""" + return build(cfg, ESTIMATORS, key='framework') + + +def build_multimodal(cfg): + """Build multimodal.""" + return build(cfg, MULTIMODAL, key='framework') + + +def build_detector(cfg): + """Build multimodal.""" + return build(cfg, DETECTORS, key='framework') + + +def build_segment(cfg): + """Build segment.""" + return build(cfg, SEGMENT, key='framework') + + +def build_model(cfg, key='framework'): + cfg_copy = cfg.copy() + framework_type = cfg_copy.get(key) + if framework_type in RECOGNIZERS: + return build_recognizer(cfg) + elif framework_type in LOCALIZERS: + return build_localizer(cfg) + elif framework_type in PARTITIONERS: + return build_partitioner(cfg) + elif framework_type in DETECTORS: + return build_detector(cfg) + elif framework_type in ESTIMATORS: + return build_estimator(cfg) + elif framework_type in MULTIMODAL: + return build_multimodal(cfg) + elif framework_type in SEGMENT: + return build_segment(cfg) + else: + raise NotImplementedError diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a7e528a803b20bf78c743508a39baa1d312a3671 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .segment import BaseSegment, Manet + +__all__ = ['BaseSegment', + 'Manet' +] diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8db3adf828b6d0ab9038b167fedc3aa1b5a65338 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from .base import BaseSegment +from .manet_stage1 import Manet + +__all__ = [ + 'BaseSegment', + 'Manet', +] diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/base.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/base.py new file mode 100644 index 0000000000000000000000000000000000000000..b5bb539455ea37844a69d091366528ea9c2b02d6 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/base.py @@ -0,0 +1,95 @@ + +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from abc import abstractmethod +from ... import builder +import paddle.nn as nn + + +class BaseSegment(nn.Layer): + """Base class for semi-Video Object Segmentation. + All subclass should overwrite: + + - Methods:``train_step``, supporting to forward when training. + - Methods:``valid_step``, supporting to forward when validating. + - Methods:``test_step``, supporting to forward when testing. + + Args: + backbone (dict): Backbone modules to extract feature. + head (dict): Head to process feature. + loss(dict): Loss function. + """ + def __init__(self, backbone=None, head=None, loss=None): + super().__init__() + if backbone != None: + self.backbone = builder.build_backbone(backbone) + if hasattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + self.backbone = None + if head != None: + self.head_name = head.name + if head.name == 'IntVOS': + head.update({'feature_extracter': self.backbone}) + self.head = builder.build_head(head) + else: + self.head = builder.build_head(head) + if hasattr(self.head, 'init_weights'): + self.head.init_weights() + else: + self.head = None + if loss != None: + self.loss = builder.build_loss(loss) + else: + self.loss = None + + def forward(self, data_batch, mode='infer', **kwargs): + """ + 1. Define how the model is going to run, from input to output. + 2. Console of train, valid, test or infer step + 3. Set mode='infer' is used for saving inference model, refer to tools/export_model.py + """ + if mode == 'train': + return self.train_step(data_batch, **kwargs) + elif mode == 'valid': + return self.val_step(data_batch, **kwargs) + elif mode == 'test': + return self.test_step(data_batch, **kwargs) + elif mode == 'infer': + return self.infer_step(data_batch, **kwargs) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating step. + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Test step. + """ + raise NotImplementedError + + @abstractmethod + def infer_step(self, data_batch, **kwargs): + """Infer step. + """ + raise NotImplementedError diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/manet_stage1.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/manet_stage1.py new file mode 100644 index 0000000000000000000000000000000000000000..875c508c19c5a06923d7e88701b34555f316174a --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/framework/segment/manet_stage1.py @@ -0,0 +1,417 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from EIVideo.paddlevideo.loader.builder import build_pipeline + +from EIVideo.paddlevideo.loader.pipelines import ToTensor_manet + +import os +import timeit +import paddle +from PIL import Image +from davisinteractive.utils.scribbles import scribbles2mask, annotated_frames +from paddle import nn + +from EIVideo.paddlevideo.utils import load +from EIVideo.paddlevideo.utils.manet_utils import float_, _palette, damage_masks, long_, write_dict, rough_ROI +from EIVideo.api import load_video, get_scribbles, submit_masks + +from ...builder import build_model +from ...registry import SEGMENT +from .base import BaseSegment + + +# if cfg.MODEL.framework == "Manet": +# cfg_helper = {"knns": 1, +# "is_save_image": True} +# cfg.update(cfg_helper) +# build_model(cfg['MODEL']).test_step(**cfg, +# weights=weights, +# parallel=False) +# return + + +@SEGMENT.register() +class Manet(BaseSegment): + def __init__(self, backbone=None, head=None, **cfg): + super().__init__(backbone, head, **cfg) + + def train_step(self, data_batch, step, **cfg): + pass + + def val_step(self, data_batch, **kwargs): + pass + + def infer_step(self, data_batch, **kwargs): + """Define how the model is going to test, from input to output.""" + pass + + def test_step(self, weights, parallel=True, is_save_image=True, **cfg): + # 1. Construct model. + cfg['MODEL'].head.pretrained = '' + cfg['MODEL'].head.test_mode = True + model = build_model(cfg['MODEL']) + if parallel: + model = paddle.DataParallel(model) + + # 2. Construct data. + sequence = cfg["video_path"].split('/')[-1].split('.')[0] + obj_nums = 1 + images, _ = load_video(cfg["video_path"], 480) + print("stage1 load_video success") + # [195, 389, 238, 47, 244, 374, 175, 399] + # .shape: (502, 480, 600, 3) + report_save_dir = cfg.get("output_dir", + f"./output/{cfg['model_name']}") + if not os.path.exists(report_save_dir): + os.makedirs(report_save_dir) + # Configuration used in the challenges + max_nb_interactions = 8 # Maximum number of interactions + # Interactive parameters + model.eval() + + state_dicts_ = load(weights)['state_dict'] + state_dicts = {} + for k, v in state_dicts_.items(): + if 'num_batches_tracked' not in k: + state_dicts['head.' + k] = v + if ('head.' + k) not in model.state_dict().keys(): + print(f'pretrained -----{k} -------is not in model') + write_dict(state_dicts, 'model_for_infer.txt', **cfg) + model.set_state_dict(state_dicts) + inter_file = open( + os.path.join( + cfg.get("output_dir", f"./output/{cfg['model_name']}"), + 'inter_file.txt'), 'w') + seen_seq = False + + with paddle.no_grad(): + + # Get the current iteration scribbles + for scribbles, first_scribble in get_scribbles(): + t_total = timeit.default_timer() + f, h, w = images.shape[:3] + if 'prev_label_storage' not in locals().keys(): + prev_label_storage = paddle.zeros([f, h, w]) + if len(annotated_frames(scribbles)) == 0: + final_masks = prev_label_storage + # ToDo To AP-kai: save_path传过来了 + submit_masks(cfg["save_path"], final_masks.numpy(), images) + continue + + # if no scribbles return, keep masks in previous round + start_annotated_frame = annotated_frames(scribbles)[0] + pred_masks = [] + pred_masks_reverse = [] + + if first_scribble: # If in the first round, initialize memories + n_interaction = 1 + eval_global_map_tmp_dic = {} + local_map_dics = ({}, {}) + total_frame_num = f + + else: + n_interaction += 1 + inter_file.write(sequence + ' ' + 'interaction' + + str(n_interaction) + ' ' + 'frame' + + str(start_annotated_frame) + '\n') + + if first_scribble: # if in the first round, extract pixel embbedings. + if not seen_seq: + seen_seq = True + inter_turn = 1 + embedding_memory = [] + places = paddle.set_device('cpu') + + for imgs in images: + if cfg['PIPELINE'].get('test'): + imgs = paddle.to_tensor([ + build_pipeline(cfg['PIPELINE'].test)({ + 'img1': + imgs + })['img1'] + ]) + else: + imgs = paddle.to_tensor([imgs]) + if parallel: + for c in model.children(): + frame_embedding = c.head.extract_feature( + imgs) + else: + frame_embedding = model.head.extract_feature( + imgs) + embedding_memory.append(frame_embedding) + + del frame_embedding + + embedding_memory = paddle.concat(embedding_memory, 0) + _, _, emb_h, emb_w = embedding_memory.shape + ref_frame_embedding = embedding_memory[ + start_annotated_frame] + ref_frame_embedding = ref_frame_embedding.unsqueeze(0) + else: + inter_turn += 1 + ref_frame_embedding = embedding_memory[ + start_annotated_frame] + ref_frame_embedding = ref_frame_embedding.unsqueeze(0) + + else: + ref_frame_embedding = embedding_memory[ + start_annotated_frame] + ref_frame_embedding = ref_frame_embedding.unsqueeze(0) + ######## + scribble_masks = scribbles2mask(scribbles, (emb_h, emb_w)) + scribble_label = scribble_masks[start_annotated_frame] + scribble_sample = {'scribble_label': scribble_label} + scribble_sample = ToTensor_manet()(scribble_sample) + # print(ref_frame_embedding, ref_frame_embedding.shape) + scribble_label = scribble_sample['scribble_label'] + + scribble_label = scribble_label.unsqueeze(0) + model_name = cfg['model_name'] + output_dir = cfg.get("output_dir", f"./output/{model_name}") + inter_file_path = os.path.join( + output_dir, sequence, 'interactive' + str(n_interaction), + 'turn' + str(inter_turn)) + if is_save_image: + ref_scribble_to_show = scribble_label.squeeze().numpy() + im_ = Image.fromarray( + ref_scribble_to_show.astype('uint8')).convert('P', ) + im_.putpalette(_palette) + ref_img_name = str(start_annotated_frame) + + if not os.path.exists(inter_file_path): + os.makedirs(inter_file_path) + im_.save( + os.path.join(inter_file_path, + 'inter_' + ref_img_name + '.png')) + if first_scribble: + prev_label = None + prev_label_storage = paddle.zeros([f, h, w]) + else: + prev_label = prev_label_storage[start_annotated_frame] + prev_label = prev_label.unsqueeze(0).unsqueeze(0) + # check if no scribbles. + if not first_scribble and paddle.unique( + scribble_label).shape[0] == 1: + print( + 'not first_scribble and paddle.unique(scribble_label).shape[0] == 1' + ) + print(paddle.unique(scribble_label)) + final_masks = prev_label_storage + submit_masks(cfg["save_path"], final_masks.numpy(), images) + continue + + ###inteaction segmentation head + if parallel: + for c in model.children(): + tmp_dic, local_map_dics = c.head.int_seghead( + ref_frame_embedding=ref_frame_embedding, + ref_scribble_label=scribble_label, + prev_round_label=prev_label, + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + frame_num=[start_annotated_frame], + first_inter=first_scribble) + else: + tmp_dic, local_map_dics = model.head.int_seghead( + ref_frame_embedding=ref_frame_embedding, + ref_scribble_label=scribble_label, + prev_round_label=prev_label, + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + frame_num=[start_annotated_frame], + first_inter=first_scribble) + pred_label = tmp_dic[sequence] + pred_label = nn.functional.interpolate(pred_label, + size=(h, w), + mode='bilinear', + align_corners=True) + pred_label = paddle.argmax(pred_label, axis=1) + pred_masks.append(float_(pred_label)) + # np.unique(pred_label) + # array([0], dtype=int64) + prev_label_storage[start_annotated_frame] = float_( + pred_label[0]) + + if is_save_image: # save image + pred_label_to_save = pred_label.squeeze(0).numpy() + im = Image.fromarray( + pred_label_to_save.astype('uint8')).convert('P', ) + im.putpalette(_palette) + imgname = str(start_annotated_frame) + while len(imgname) < 5: + imgname = '0' + imgname + if not os.path.exists(inter_file_path): + os.makedirs(inter_file_path) + im.save(os.path.join(inter_file_path, imgname + '.png')) + ####################################### + if first_scribble: + scribble_label = rough_ROI(scribble_label) + + ############################## + ref_prev_label = pred_label.unsqueeze(0) + prev_label = pred_label.unsqueeze(0) + prev_embedding = ref_frame_embedding + for ii in range(start_annotated_frame + 1, total_frame_num): + current_embedding = embedding_memory[ii] + current_embedding = current_embedding.unsqueeze(0) + prev_label = prev_label + if parallel: + for c in model.children(): + tmp_dic, eval_global_map_tmp_dic, local_map_dics = c.head.prop_seghead( + ref_frame_embedding, + prev_embedding, + current_embedding, + scribble_label, + prev_label, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + k_nearest_neighbors=cfg['knns'], + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + start_annotated_frame=start_annotated_frame, + frame_num=[ii], + dynamic_seghead=c.head.dynamic_seghead) + else: + tmp_dic, eval_global_map_tmp_dic, local_map_dics = model.head.prop_seghead( + ref_frame_embedding, + prev_embedding, + current_embedding, + scribble_label, + prev_label, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + k_nearest_neighbors=cfg['knns'], + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + start_annotated_frame=start_annotated_frame, + frame_num=[ii], + dynamic_seghead=model.head.dynamic_seghead) + pred_label = tmp_dic[sequence] + pred_label = nn.functional.interpolate(pred_label, + size=(h, w), + mode='bilinear', + align_corners=True) + pred_label = paddle.argmax(pred_label, axis=1) + pred_masks.append(float_(pred_label)) + prev_label = pred_label.unsqueeze(0) + prev_embedding = current_embedding + prev_label_storage[ii] = float_(pred_label[0]) + if is_save_image: + pred_label_to_save = pred_label.squeeze(0).numpy() + im = Image.fromarray( + pred_label_to_save.astype('uint8')).convert('P', ) + im.putpalette(_palette) + imgname = str(ii) + while len(imgname) < 5: + imgname = '0' + imgname + if not os.path.exists(inter_file_path): + os.makedirs(inter_file_path) + im.save(os.path.join(inter_file_path, + imgname + '.png')) + ####################################### + prev_label = ref_prev_label + prev_embedding = ref_frame_embedding + ####### + # Propagation <- + for ii in range(start_annotated_frame): + current_frame_num = start_annotated_frame - 1 - ii + current_embedding = embedding_memory[current_frame_num] + current_embedding = current_embedding.unsqueeze(0) + prev_label = prev_label + if parallel: + for c in model.children(): + tmp_dic, eval_global_map_tmp_dic, local_map_dics = c.head.prop_seghead( + ref_frame_embedding, + prev_embedding, + current_embedding, + scribble_label, + prev_label, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + k_nearest_neighbors=cfg['knns'], + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + start_annotated_frame=start_annotated_frame, + frame_num=[current_frame_num], + dynamic_seghead=c.head.dynamic_seghead) + else: + tmp_dic, eval_global_map_tmp_dic, local_map_dics = model.head.prop_seghead( + ref_frame_embedding, + prev_embedding, + current_embedding, + scribble_label, + prev_label, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + k_nearest_neighbors=cfg['knns'], + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + start_annotated_frame=start_annotated_frame, + frame_num=[current_frame_num], + dynamic_seghead=model.head.dynamic_seghead) + pred_label = tmp_dic[sequence] + pred_label = nn.functional.interpolate(pred_label, + size=(h, w), + mode='bilinear', + align_corners=True) + + pred_label = paddle.argmax(pred_label, axis=1) + pred_masks_reverse.append(float_(pred_label)) + prev_label = pred_label.unsqueeze(0) + prev_embedding = current_embedding + #### + prev_label_storage[current_frame_num] = float_( + pred_label[0]) + ### + if is_save_image: + pred_label_to_save = pred_label.squeeze(0).numpy() + im = Image.fromarray( + pred_label_to_save.astype('uint8')).convert('P', ) + im.putpalette(_palette) + imgname = str(current_frame_num) + while len(imgname) < 5: + imgname = '0' + imgname + if not os.path.exists(inter_file_path): + os.makedirs(inter_file_path) + im.save(os.path.join(inter_file_path, + imgname + '.png')) + pred_masks_reverse.reverse() + pred_masks_reverse.extend(pred_masks) + final_masks = paddle.concat(pred_masks_reverse, 0) + submit_masks(cfg["save_path"], final_masks.numpy(), images) + + t_end = timeit.default_timer() + print('Total time for single interaction: ' + + str(t_end - t_total)) + inter_file.close() + return None \ No newline at end of file diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/heads/IntVOS.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/heads/IntVOS.py new file mode 100644 index 0000000000000000000000000000000000000000..32dbe009627cef428a84544b49ef284d1eca748a --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/heads/IntVOS.py @@ -0,0 +1,893 @@ +import numpy as np +import paddle +import paddle.nn as nn + +import paddle.nn.functional as F +from EIVideo.paddlevideo.utils.manet_utils import int_, float_, long_, load +from EIVideo.paddlevideo.utils.manet_utils import kaiming_normal_ + +#############################################################GLOBAL_DIST_MAP + +MODEL_UNFOLD = True +WRONG_LABEL_PADDING_DISTANCE = 1e20 + + +def _pairwise_distances(x, y, ys=None): + """Computes pairwise squared l2 distances between tensors x and y. + Args: + x: Tensor of shape [n, feature_dim]. + y: Tensor of shape [m, feature_dim]. + Returns: + Float32 distances tensor of shape [n, m]. + """ + + xs = paddle.sum(x * x, 1) + xs = xs.unsqueeze(1) + if ys is None: + ys = paddle.sum(y * y, 1) + ys = ys.unsqueeze(0) + else: + ys = ys + d = xs + ys - 2. * paddle.matmul(x, paddle.t(y)) + return d, ys + + +################## +def _flattened_pairwise_distances(reference_embeddings, query_embeddings, ys): + """Calculates flattened tensor of pairwise distances between ref and query. + Args: + reference_embeddings: Tensor of shape [..., embedding_dim], + the embedding vectors for the reference frame + query_embeddings: Tensor of shape [n_query_images, height, width, + embedding_dim], the embedding vectors for the query frames. + Returns: + A distance tensor of shape [reference_embeddings.size / embedding_dim, + query_embeddings.size / embedding_dim] + """ + embedding_dim = query_embeddings.shape[-1] + reference_embeddings = reference_embeddings.reshape([-1, embedding_dim]) + first_dim = -1 + query_embeddings = query_embeddings.reshape([first_dim, embedding_dim]) + dists, ys = _pairwise_distances(query_embeddings, reference_embeddings, ys) + return dists, ys + + +def _nn_features_per_object_for_chunk(reference_embeddings, query_embeddings, + wrong_label_mask, k_nearest_neighbors, + ys): + """Extracts features for each object using nearest neighbor attention. + Args: + reference_embeddings: Tensor of shape [n_chunk, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings: Tensor of shape [m_chunk, embedding_dim], the embedding + vectors for the query frames. + wrong_label_mask: + k_nearest_neighbors: Integer, the number of nearest neighbors to use. + Returns: + nn_features: A float32 tensor of nearest neighbor features of shape + [m_chunk, n_objects, feature_dim]. + """ + # reference_embeddings_key = reference_embeddings + # query_embeddings_key = query_embeddings + dists, ys = _flattened_pairwise_distances(reference_embeddings, + query_embeddings, ys) + + dists = (paddle.unsqueeze(dists, 1) + + paddle.unsqueeze(float_(wrong_label_mask), 0) * + WRONG_LABEL_PADDING_DISTANCE) + if k_nearest_neighbors == 1: + features = paddle.min(dists, 2, keepdim=True) + else: + dists, _ = paddle.topk(-dists, k=k_nearest_neighbors, axis=2) + dists = -dists + valid_mask = (dists < WRONG_LABEL_PADDING_DISTANCE) + masked_dists = dists * valid_mask.float() + pad_dist = paddle.max(masked_dists, axis=2, keepdim=True)[0].tile( + (1, 1, masked_dists.shape[-1])) + dists = paddle.where(valid_mask, dists, pad_dist) + # take mean of distances + features = paddle.mean(dists, axis=2, keepdim=True) + + return features, ys + + +### +def _selected_pixel(ref_labels_flat, ref_emb_flat): + index_list = paddle.arange(len(ref_labels_flat)) + index_list = index_list + index_ = paddle.masked_select(index_list, ref_labels_flat != -1) + + index_ = long_(index_) + ref_labels_flat = paddle.index_select(ref_labels_flat, index_, 0) + ref_emb_flat = paddle.index_select(ref_emb_flat, index_, 0) + + return ref_labels_flat, ref_emb_flat + + +### + + +def _nearest_neighbor_features_per_object_in_chunks(reference_embeddings_flat, + query_embeddings_flat, + reference_labels_flat, + ref_obj_ids, + k_nearest_neighbors, + n_chunks, **cfg): + """Calculates the nearest neighbor features per object in chunks to save mem. + Uses chunking to bound the memory use. + Args: + reference_embeddings_flat: Tensor of shape [n, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings_flat: Tensor of shape [m, embedding_dim], the embedding + vectors for the query frames. + reference_labels_flat: Tensor of shape [n], the class labels of the + reference frame. + ref_obj_ids: int tensor of unique object ids in the reference labels. + k_nearest_neighbors: Integer, the number of nearest neighbors to use. + n_chunks: Integer, the number of chunks to use to save memory + (set to 1 for no chunking). + Returns: + nn_features: A float32 tensor of nearest neighbor features of shape + [m, n_objects, feature_dim]. + """ + + # reference_embeddings_flat = reference_embeddings_flat.cpu() + # query_embeddings_flat = query_embeddings_flat.cpu() + # reference_labels_flat = reference_labels_flat.cpu() + # ref_obj_ids = ref_obj_ids.cpu() + + chunk_size = int_( + np.ceil((float_(query_embeddings_flat.shape[0]) / n_chunks).numpy())) + if cfg.get('test_mode'): + reference_labels_flat, reference_embeddings_flat = _selected_pixel( + reference_labels_flat, reference_embeddings_flat) + wrong_label_mask = (reference_labels_flat != paddle.unsqueeze( + ref_obj_ids, 1)) + all_features = [] + for n in range(n_chunks): + if n == 0: + ys = None + if n_chunks == 1: + query_embeddings_flat_chunk = query_embeddings_flat + else: + chunk_start = n * chunk_size + chunk_end = (n + 1) * chunk_size + query_embeddings_flat_chunk = query_embeddings_flat[ + chunk_start:chunk_end] + features, ys = _nn_features_per_object_for_chunk( + reference_embeddings_flat, query_embeddings_flat_chunk, + wrong_label_mask, k_nearest_neighbors, ys) + all_features.append(features) + if n_chunks == 1: + nn_features = all_features[0] + else: + nn_features = paddle.concat(all_features, axis=0) + return nn_features + + +def nearest_neighbor_features_per_object(reference_embeddings, + query_embeddings, + reference_labels, + k_nearest_neighbors, + gt_ids=None, + n_chunks=100, + **cfg): + """Calculates the distance to the nearest neighbor per object. + For every pixel of query_embeddings calculate the distance to the + nearest neighbor in the (possibly subsampled) reference_embeddings per object. + Args: + reference_embeddings: Tensor of shape [height, width, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings: Tensor of shape [n_query_images, height, width, + embedding_dim], the embedding vectors for the query frames. + reference_labels: Tensor of shape [height, width, 1], the class labels of + the reference frame. + max_neighbors_per_object: Integer, the maximum number of candidates + for the nearest neighbor query per object after subsampling, + or 0 for no subsampling. + k_nearest_neighbors: Integer, the number of nearest neighbors to use. + gt_ids: Int tensor of shape [n_objs] of the sorted unique ground truth + ids in the first frame. If None, it will be derived from + reference_labels. + n_chunks: Integer, the number of chunks to use to save memory + (set to 1 for no chunking). + Returns: + nn_features: A float32 tensor of nearest neighbor features of shape + [n_query_images, height, width, n_objects, feature_dim]. + gt_ids: An int32 tensor of the unique sorted object ids present + in the reference labels. + """ + # reference_embeddings = reference_embeddings.detach().cpu() + # query_embeddings = query_embeddings.detach().cpu() + # reference_labels = reference_labels.detach().cpu() + + assert (reference_embeddings.shape[:2] == reference_labels.shape[:2]) + h, w, _ = query_embeddings.shape + reference_labels_flat = reference_labels.reshape([-1]) + if gt_ids is None: + ref_obj_ids = paddle.unique(reference_labels_flat)[-1] + ref_obj_ids = np.arange(0, ref_obj_ids + 1) + gt_ids = paddle.to_tensor(ref_obj_ids) + gt_ids = int_(gt_ids) + else: + gt_ids = int_(paddle.arange(0, gt_ids + 1)) + + embedding_dim = query_embeddings.shape[-1] + query_embeddings_flat = query_embeddings.reshape([-1, embedding_dim]) + reference_embeddings_flat = reference_embeddings.reshape( + [-1, embedding_dim]) + nn_features = _nearest_neighbor_features_per_object_in_chunks( + reference_embeddings_flat, query_embeddings_flat, + reference_labels_flat, gt_ids, k_nearest_neighbors, n_chunks, **cfg) + nn_features_dim = nn_features.shape[-1] + nn_features = nn_features.reshape( + [1, h, w, gt_ids.shape[0], nn_features_dim]) + return nn_features.cuda(), gt_ids + + +########################################################################LOCAL_DIST_MAP + + +def local_pairwise_distances2(x, y, max_distance=9): + """Computes pairwise squared l2 distances using a local search window. + Naive implementation using map_fn. + Used as a slow fallback for when correlation_cost is not available. + Args: + x: Float32 tensor of shape [height, width, feature_dim]. + y: Float32 tensor of shape [height, width, feature_dim]. + max_distance: Integer, the maximum distance in pixel coordinates + per dimension which is considered to be in the search window. + Returns: + Float32 distances tensor of shape + [height, width, (2 * max_distance + 1) ** 2]. + """ + ori_h, ori_w, _ = x.shape + x = paddle.transpose(x, [2, 0, 1]).unsqueeze(0) + x = F.avg_pool2d(x, (2, 2), (2, 2)) + y = paddle.transpose(y, [2, 0, 1]).unsqueeze(0) + y = F.avg_pool2d(y, (2, 2), (2, 2)) + + _, channels, height, width = x.shape + padding_val = 1e20 + padded_y = F.pad(y, + (max_distance, max_distance, max_distance, max_distance), + mode='constant', + value=padding_val) + offset_y = F.unfold(padded_y, kernel_sizes=[height, width]).reshape( + [1, channels, height, width, -1]) + x = x.reshape([1, channels, height, width, 1]) + minus = x - offset_y + dists = paddle.sum(paddle.multiply(minus, minus), + axis=1).reshape([1, height, width, + -1]).transpose([0, 3, 1, 2]) + dists = (paddle.nn.functional.sigmoid(dists) - 0.5) * 2 + dists = F.interpolate(dists, + size=[ori_h, ori_w], + mode='bilinear', + align_corners=True) + dists = dists.squeeze(0).transpose([1, 2, 0]) + return dists + + +def local_previous_frame_nearest_neighbor_features_per_object( + prev_frame_embedding, + query_embedding, + prev_frame_labels, + gt_ids, + max_distance=12): + """Computes nearest neighbor features while only allowing local matches. + Args: + prev_frame_embedding: Tensor of shape [height, width, embedding_dim], + the embedding vectors for the last frame. + query_embedding: Tensor of shape [height, width, embedding_dim], + the embedding vectors for the query frames. + prev_frame_labels: Tensor of shape [height, width, 1], the class labels of + the previous frame. + gt_ids: Int Tensor of shape [n_objs] of the sorted unique ground truth + ids in the first frame. + max_distance: Integer, the maximum distance allowed for local matching. + Returns: + nn_features: A float32 np.array of nearest neighbor features of shape + [1, height, width, n_objects, 1]. + """ + # print(query_embedding.shape, prev_frame_embedding.shape) + # print(query_embedding.place, prev_frame_embedding.place) + # query_embedding = query_embedding.cpu() + # prev_frame_embedding = prev_frame_embedding.cpu() + # prev_frame_labels = prev_frame_labels.cpu() + # print(prev_frame_labels.place, prev_frame_embedding.place, query_embedding.place) + + d = local_pairwise_distances2(query_embedding, + prev_frame_embedding, + max_distance=max_distance) + height, width = prev_frame_embedding.shape[:2] + + if MODEL_UNFOLD: + + labels = float_(prev_frame_labels).transpose([2, 0, 1]).unsqueeze(0) + padded_labels = F.pad(labels, ( + 2 * max_distance, + 2 * max_distance, + 2 * max_distance, + 2 * max_distance, + )) + offset_labels = F.unfold(padded_labels, + kernel_sizes=[height, width], + strides=[2, + 2]).reshape([height, width, -1, 1]) + offset_masks = paddle.equal( + offset_labels, + float_(gt_ids).unsqueeze(0).unsqueeze(0).unsqueeze(0)) + else: + + masks = paddle.equal(prev_frame_labels, + gt_ids.unsqueeze(0).unsqueeze(0)) + padded_masks = nn.functional.pad(masks, ( + 0, + 0, + max_distance, + max_distance, + max_distance, + max_distance, + )) + offset_masks = [] + for y_start in range(2 * max_distance + 1): + y_end = y_start + height + masks_slice = padded_masks[y_start:y_end] + for x_start in range(2 * max_distance + 1): + x_end = x_start + width + offset_mask = masks_slice[:, x_start:x_end] + offset_masks.append(offset_mask) + offset_masks = paddle.stack(offset_masks, axis=2) + + d_tiled = d.unsqueeze(-1).tile((1, 1, 1, gt_ids.shape[0])) + pad = paddle.ones_like(d_tiled) + d_masked = paddle.where(offset_masks, d_tiled, pad) + dists = paddle.min(d_masked, axis=2) + dists = dists.reshape([1, height, width, gt_ids.shape[0], 1]) + + return dists + + +############################################################## + + +################# +class _res_block(nn.Layer): + def __init__(self, in_dim, out_dim, **cfg): + super(_res_block, self).__init__() + self.conv1 = nn.Conv2D(in_dim, + out_dim, + kernel_size=3, + stride=1, + padding=1) + self.relu1 = nn.ReLU() + self.bn1 = paddle.nn.BatchNorm2D(out_dim, momentum=cfg['train_bn_mom']) + self.conv2 = nn.Conv2D(out_dim, + out_dim, + kernel_size=3, + stride=1, + padding=1) + self.relu2 = nn.ReLU() + self.bn2 = paddle.nn.BatchNorm2D(out_dim, momentum=cfg['train_bn_mom']) + + def forward(self, x): + res = x + x = self.conv1(x) + x = self.bn1(x) + x = self.relu1(x) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu2(x) + x += res + return x + + +#################### +class IntSegHead(nn.Layer): + def __init__(self, in_dim, emb_dim, **cfg): + super(IntSegHead, self).__init__() + self.conv1 = nn.Conv2D(in_dim, + emb_dim, + kernel_size=7, + stride=1, + padding=3) + self.bn1 = paddle.nn.BatchNorm2D(emb_dim, momentum=cfg['train_bn_mom']) + self.relu1 = nn.ReLU(True) + self.res1 = _res_block(emb_dim, emb_dim, **cfg) + self.res2 = _res_block(emb_dim, emb_dim, **cfg) + self.conv2 = nn.Conv2D(256, + emb_dim, + kernel_size=3, + stride=1, + padding=1) + self.bn2 = paddle.nn.BatchNorm2D(emb_dim, momentum=cfg['train_bn_mom']) + self.relu2 = nn.ReLU(True) + self.conv3 = nn.Conv2D(emb_dim, 1, 1, 1) + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu1(x) + x = self.res1(x) + x = self.res2(x) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu2(x) + x = self.conv3(x) + return x + + +class _split_separable_conv2d(nn.Layer): + def __init__(self, in_dim, out_dim, kernel_size=7, **cfg): + super(_split_separable_conv2d, self).__init__() + self.conv1 = nn.Conv2D(in_dim, + in_dim, + kernel_size=kernel_size, + stride=1, + padding=int((kernel_size - 1) / 2), + groups=in_dim) + self.relu1 = nn.ReLU(True) + self.bn1 = paddle.nn.BatchNorm2D(in_dim, momentum=cfg['train_bn_mom']) + self.conv2 = nn.Conv2D(in_dim, out_dim, kernel_size=1, stride=1) + self.relu2 = nn.ReLU(True) + self.bn2 = paddle.nn.BatchNorm2D(out_dim, momentum=cfg['train_bn_mom']) + kaiming_normal_(self.conv1.weight, mode='fan_out', nonlinearity='relu') + kaiming_normal_(self.conv2.weight, mode='fan_out', nonlinearity='relu') + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu1(x) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu2(x) + return x + + +class DynamicSegHead(nn.Layer): + def __init__(self, in_dim, embed_dim, **cfg): + super(DynamicSegHead, self).__init__() + self.layer1 = _split_separable_conv2d(in_dim, embed_dim, **cfg) + self.layer2 = _split_separable_conv2d(embed_dim, embed_dim, **cfg) + self.layer3 = _split_separable_conv2d(embed_dim, embed_dim, **cfg) + self.layer4 = _split_separable_conv2d(embed_dim, embed_dim, **cfg) + self.conv = nn.Conv2D(embed_dim, 1, 1, 1) + kaiming_normal_(self.conv.weight, mode='fan_out', nonlinearity='relu') + + def forward(self, x): + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + x = self.conv(x) + return x + + +from ..registry import HEADS +""" +覆盖原理 +class c1: + def __init__(self): + self.a = 1 + + +class c2(c1): + def __init__(self): + super(c2, self).__init__() + self.a = 2 + + +c = c2() +print(c.a) + +""" + + +@HEADS.register() +class IntVOS(nn.Layer): + def __init__(self, feature_extracter, **cfg): + super(IntVOS, self).__init__() + self.feature_extracter = feature_extracter ##embedding extractor + self.feature_extracter.cls_conv = nn.Sequential() + self.feature_extracter.upsample4 = nn.Sequential() + self.semantic_embedding = None + self.seperate_conv = nn.Conv2D(cfg['model_aspp_outdim'], + cfg['model_aspp_outdim'], + kernel_size=3, + stride=1, + padding=1, + groups=cfg['model_aspp_outdim']) + self.bn1 = paddle.nn.BatchNorm2D(cfg['model_aspp_outdim'], + momentum=cfg['train_bn_mom']) + self.relu1 = nn.ReLU(True) + self.embedding_conv = nn.Conv2D(cfg['model_aspp_outdim'], + cfg['model_semantic_embedding_dim'], 1, + 1) + self.relu2 = nn.ReLU(True) + self.bn2 = paddle.nn.BatchNorm2D(cfg['model_semantic_embedding_dim'], + momentum=cfg['train_bn_mom']) + self.semantic_embedding = nn.Sequential(*[ + self.seperate_conv, self.bn1, self.relu1, self.embedding_conv, + self.bn2, self.relu2 + ]) + + for m in self.semantic_embedding: + if isinstance(m, nn.Conv2D): + kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') + + self.dynamic_seghead = DynamicSegHead( + in_dim=cfg['model_semantic_embedding_dim'] + 3, + embed_dim=cfg['model_head_embedding_dim'], + **cfg) # propagation segm head + if cfg['model_useintseg']: + self.inter_seghead = IntSegHead( + in_dim=cfg['model_semantic_embedding_dim'] + 3, + emb_dim=cfg['model_head_embedding_dim'], + **cfg) + else: + self.inter_seghead = DynamicSegHead( + in_dim=cfg['model_semantic_embedding_dim'] + 2, + embed_dim=cfg['model_head_embedding_dim'], + **cfg) # interaction segm head + self.pretrained = cfg.get('pretrained', None) + self.cfg = cfg + + def init_weights(self): + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + self.set_state_dict(load(self.pretrained, self.state_dict())) + print('loaded pretrained model') + + def loss(self, **kwargs): + return self.loss_func(**kwargs) + + def forward(self, + x=None, + ref_scribble_label=None, + previous_frame_mask=None, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=None, + gt_ids=None, + k_nearest_neighbors=1, + global_map_tmp_dic=None, + local_map_dics=None, + interaction_num=None, + start_annotated_frame=None, + frame_num=None): + + x = self.extract_feature(x) + # print('extract_feature:', x.mean().item()) + ref_frame_embedding, previous_frame_embedding, current_frame_embedding = paddle.split( + x, num_or_sections=3, axis=0) + + if global_map_tmp_dic is None: + dic = self.prop_seghead( + ref_frame_embedding, + previous_frame_embedding, + current_frame_embedding, + ref_scribble_label, + previous_frame_mask, + normalize_nearest_neighbor_distances, + use_local_map, + seq_names, + gt_ids, + k_nearest_neighbors, + global_map_tmp_dic, + local_map_dics, + interaction_num, + start_annotated_frame, + frame_num, + self.dynamic_seghead, + ) + return dic + + else: + dic, global_map_tmp_dic = self.prop_seghead( + ref_frame_embedding, + previous_frame_embedding, + current_frame_embedding, + ref_scribble_label, + previous_frame_mask, + normalize_nearest_neighbor_distances, + use_local_map, + seq_names, + gt_ids, + k_nearest_neighbors, + global_map_tmp_dic, + local_map_dics, + interaction_num, + start_annotated_frame, + frame_num, + self.dynamic_seghead, + ) + return dic, global_map_tmp_dic + + def extract_feature(self, x): + x = self.feature_extracter(x) + x = self.semantic_embedding(x) + return x + + def prop_seghead( + self, + ref_frame_embedding=None, + previous_frame_embedding=None, + current_frame_embedding=None, + ref_scribble_label=None, + previous_frame_mask=None, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=None, + gt_ids=None, + k_nearest_neighbors=1, + global_map_tmp_dic=None, + local_map_dics=None, + interaction_num=None, + start_annotated_frame=None, + frame_num=None, + dynamic_seghead=None, + ): + """return: feature_embedding,global_match_map,local_match_map,previous_frame_mask""" + ############### + cfg = self.cfg + global_map_tmp_dic = global_map_tmp_dic + dic_tmp = {} + bs, c, h, w = current_frame_embedding.shape + if cfg.get('test_mode'): + scale_ref_scribble_label = float_(ref_scribble_label) + else: + scale_ref_scribble_label = paddle.nn.functional.interpolate( + float_(ref_scribble_label), size=(h, w), mode='nearest') + scale_ref_scribble_label = int_(scale_ref_scribble_label) + scale_previous_frame_label = paddle.nn.functional.interpolate( + float_(previous_frame_mask), size=(h, w), mode='nearest') + scale_previous_frame_label = int_(scale_previous_frame_label) + for n in range(bs): + seq_current_frame_embedding = current_frame_embedding[n] + seq_ref_frame_embedding = ref_frame_embedding[n] + seq_prev_frame_embedding = previous_frame_embedding[n] + seq_ref_frame_embedding = seq_ref_frame_embedding.transpose( + [1, 2, 0]) + seq_current_frame_embedding = seq_current_frame_embedding.transpose( + [1, 2, 0]) + seq_ref_scribble_label = scale_ref_scribble_label[n].transpose( + [1, 2, 0]) + #########Global Map + nn_features_n, ref_obj_ids = nearest_neighbor_features_per_object( + reference_embeddings=seq_ref_frame_embedding, + query_embeddings=seq_current_frame_embedding, + reference_labels=seq_ref_scribble_label, + k_nearest_neighbors=k_nearest_neighbors, + gt_ids=gt_ids[n], + n_chunks=10) + if normalize_nearest_neighbor_distances: + nn_features_n = (paddle.nn.functional.sigmoid(nn_features_n) - + 0.5) * 2 + + # print(nn_features_n) + + ### + if global_map_tmp_dic is not None: ###when testing, use global map memory + if seq_names[n] not in global_map_tmp_dic: + global_map_tmp_dic[seq_names[n]] = paddle.ones_like( + nn_features_n).tile([1000, 1, 1, 1, 1]) + nn_features_n = paddle.where( + nn_features_n <= global_map_tmp_dic[seq_names[n]][ + frame_num[n]].unsqueeze(0), nn_features_n, + global_map_tmp_dic[seq_names[n]][frame_num[n]].unsqueeze( + 0)) + + # print('detach 1') + # print(nn_features_n.shape) + # nn_features_n = nn_features_n.detach() + global_map_tmp_dic[seq_names[n]][ + frame_num[n]] = nn_features_n.detach()[0] + + #########################Local dist map + seq_prev_frame_embedding = seq_prev_frame_embedding.transpose( + [1, 2, 0]) + seq_previous_frame_label = scale_previous_frame_label[n].transpose( + [1, 2, 0]) + + if use_local_map: + prev_frame_nn_features_n = local_previous_frame_nearest_neighbor_features_per_object( + prev_frame_embedding=seq_prev_frame_embedding, + query_embedding=seq_current_frame_embedding, + prev_frame_labels=seq_previous_frame_label, + gt_ids=ref_obj_ids, + max_distance=cfg['model_max_local_distance']) + else: + prev_frame_nn_features_n, _ = nearest_neighbor_features_per_object( + reference_embeddings=seq_prev_frame_embedding, + query_embeddings=seq_current_frame_embedding, + reference_labels=seq_previous_frame_label, + k_nearest_neighbors=k_nearest_neighbors, + gt_ids=gt_ids[n], + n_chunks=20) + prev_frame_nn_features_n = ( + paddle.nn.functional.sigmoid(prev_frame_nn_features_n) - + 0.5) * 2 + + # print(prev_frame_nn_features_n.mean().item(), prev_frame_nn_features_n.shape, interaction_num) # o + ############# + if local_map_dics is not None: ##When testing, use local map memory + local_map_tmp_dic, local_map_dist_dic = local_map_dics + if seq_names[n] not in local_map_dist_dic: + print(seq_names[n], 'not in local_map_dist_dic') + local_map_dist_dic[seq_names[n]] = paddle.zeros(1000, 9) + if seq_names[n] not in local_map_tmp_dic: + print(seq_names[n], 'not in local_map_tmp_dic') + local_map_tmp_dic[seq_names[n]] = paddle.zeros_like( + prev_frame_nn_features_n).unsqueeze(0).tile( + [1000, 9, 1, 1, 1, 1]) + # print(local_map_dist_dic[seq_names[n]].shape) + # print('detach 2') + # prev_frame_nn_features_n = prev_frame_nn_features_n.detach() + local_map_dist_dic[seq_names[n]][ + frame_num[n], interaction_num - + 1] = 1.0 / (abs(frame_num[n] - start_annotated_frame) + ) # bugs fixed. + local_map_tmp_dic[seq_names[n]][ + frame_num[n], + interaction_num - 1] = prev_frame_nn_features_n.squeeze( + 0).detach() # bugs fixed. + if interaction_num == 1: + prev_frame_nn_features_n = local_map_tmp_dic[seq_names[n]][ + frame_num[n]][interaction_num - 1] + prev_frame_nn_features_n = prev_frame_nn_features_n.unsqueeze( + 0) + else: + if local_map_dist_dic[seq_names[n]][frame_num[n]][interaction_num - 1] > \ + local_map_dist_dic[seq_names[n]][frame_num[n]][interaction_num - 2]: + prev_frame_nn_features_n = local_map_tmp_dic[ + seq_names[n]][frame_num[n]][interaction_num - 1] + prev_frame_nn_features_n = prev_frame_nn_features_n.unsqueeze( + 0) + else: + prev_frame_nn_features_n = local_map_tmp_dic[ + seq_names[n]][frame_num[n]][interaction_num - 2] + prev_frame_nn_features_n = prev_frame_nn_features_n.unsqueeze( + 0) + + local_map_dics = (local_map_tmp_dic, local_map_dist_dic) + + to_cat_previous_frame = ( + float_(seq_previous_frame_label) == float_(ref_obj_ids) + ) # float comparision? + + to_cat_current_frame_embedding = current_frame_embedding[ + n].unsqueeze(0).tile((ref_obj_ids.shape[0], 1, 1, 1)) + + to_cat_nn_feature_n = nn_features_n.squeeze(0).transpose( + [2, 3, 0, 1]) + to_cat_previous_frame = float_( + to_cat_previous_frame.unsqueeze(-1).transpose([2, 3, 0, 1])) + to_cat_prev_frame_nn_feature_n = prev_frame_nn_features_n.squeeze( + 0).transpose([2, 3, 0, 1]) + to_cat = paddle.concat( + (to_cat_current_frame_embedding, to_cat_nn_feature_n, + to_cat_prev_frame_nn_feature_n, to_cat_previous_frame), 1) + pred_ = dynamic_seghead(to_cat) + pred_ = pred_.transpose([1, 0, 2, 3]) + dic_tmp[seq_names[n]] = pred_ + + if global_map_tmp_dic is None: + return dic_tmp + else: + if local_map_dics is None: + return dic_tmp, global_map_tmp_dic + else: + return dic_tmp, global_map_tmp_dic, local_map_dics + + def int_seghead(self, + ref_frame_embedding=None, + ref_scribble_label=None, + prev_round_label=None, + normalize_nearest_neighbor_distances=True, + global_map_tmp_dic=None, + local_map_dics=None, + interaction_num=None, + seq_names=None, + gt_ids=None, + k_nearest_neighbors=1, + frame_num=None, + first_inter=True): + dic_tmp = {} + bs, c, h, w = ref_frame_embedding.shape + scale_ref_scribble_label = paddle.nn.functional.interpolate( + float_(ref_scribble_label), size=(h, w), mode='nearest') + scale_ref_scribble_label = int_(scale_ref_scribble_label) + if not first_inter: + scale_prev_round_label = paddle.nn.functional.interpolate( + float_(prev_round_label), size=(h, w), mode='nearest') + scale_prev_round_label = int_(scale_prev_round_label) + n_chunks = 500 + for n in range(bs): + + gt_id = paddle.arange(0, gt_ids[n] + 1) + + gt_id = int_(gt_id) + + seq_ref_frame_embedding = ref_frame_embedding[n] + + ########################Local dist map + seq_ref_frame_embedding = paddle.transpose(seq_ref_frame_embedding, + [1, 2, 0]) + seq_ref_scribble_label = paddle.transpose( + scale_ref_scribble_label[n], [1, 2, 0]) + nn_features_n = local_previous_frame_nearest_neighbor_features_per_object( + prev_frame_embedding=seq_ref_frame_embedding, + query_embedding=seq_ref_frame_embedding, + prev_frame_labels=seq_ref_scribble_label, + gt_ids=gt_id, + max_distance=self.cfg['model_max_local_distance']) + + ####### + ######################Global map update + if seq_names[n] not in global_map_tmp_dic: + global_map_tmp_dic[seq_names[n]] = paddle.ones_like( + nn_features_n).tile([1000, 1, 1, 1, 1]) + nn_features_n_ = paddle.where( + nn_features_n <= + global_map_tmp_dic[seq_names[n]][frame_num[n]].unsqueeze(0), + nn_features_n, + global_map_tmp_dic[seq_names[n]][frame_num[n]].unsqueeze(0)) + + ### + + ### + # print('detach 3') + # nn_features_n_ = nn_features_n_.detach() + global_map_tmp_dic[seq_names[n]][ + frame_num[n]] = nn_features_n_.detach()[0] + ##################Local map update + if local_map_dics is not None: + local_map_tmp_dic, local_map_dist_dic = local_map_dics + if seq_names[n] not in local_map_dist_dic: + local_map_dist_dic[seq_names[n]] = paddle.zeros([1000, 9]) + if seq_names[n] not in local_map_tmp_dic: + local_map_tmp_dic[seq_names[n]] = paddle.ones_like( + nn_features_n).unsqueeze(0).tile([1000, 9, 1, 1, 1, 1]) + local_map_dist_dic[seq_names[n]][frame_num[n]][interaction_num + - 1] = 0 + + local_map_dics = (local_map_tmp_dic, local_map_dist_dic) + + ################## + to_cat_current_frame_embedding = ref_frame_embedding[n].unsqueeze( + 0).tile((gt_id.shape[0], 1, 1, 1)) + to_cat_nn_feature_n = nn_features_n.squeeze(0).transpose( + [2, 3, 0, 1]) + + to_cat_scribble_mask_to_cat = ( + float_(seq_ref_scribble_label) == float_(gt_id) + ) # float comparision? + to_cat_scribble_mask_to_cat = float_( + to_cat_scribble_mask_to_cat.unsqueeze(-1).transpose( + [2, 3, 0, 1])) + if not first_inter: + seq_prev_round_label = scale_prev_round_label[n].transpose( + [1, 2, 0]) + + to_cat_prev_round_to_cat = ( + float_(seq_prev_round_label) == float_(gt_id) + ) # float comparision? + to_cat_prev_round_to_cat = float_( + to_cat_prev_round_to_cat.unsqueeze(-1).transpose( + [2, 3, 0, 1])) + else: + to_cat_prev_round_to_cat = paddle.zeros_like( + to_cat_scribble_mask_to_cat) + to_cat_prev_round_to_cat[0] = 1. + + to_cat = paddle.concat( + (to_cat_current_frame_embedding, to_cat_scribble_mask_to_cat, + to_cat_prev_round_to_cat), 1) + + pred_ = self.inter_seghead(to_cat) + pred_ = pred_.transpose([1, 0, 2, 3]) + dic_tmp[seq_names[n]] = pred_ + if local_map_dics is None: + return dic_tmp + else: + return dic_tmp, local_map_dics diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/heads/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/heads/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5a98b64515069cbac23dc9725cd5eea4ed152006 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/heads/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .IntVOS import IntVOS + +__all__ = ['IntVOS' +] diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/registry.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..48babf5a3ceb180ef6227ee3d7f420b2991cf464 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/registry.py @@ -0,0 +1,31 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..utils import Registry + +BACKBONES = Registry('backbone') +HEADS = Registry('head') +RECOGNIZERS = Registry('recognizer') +LOCALIZERS = Registry('localizer') +PARTITIONERS = Registry('partitioner') +SEGMENT = Registry('segmentation') +LOSSES = Registry('loss') +ROI_EXTRACTORS = Registry('roi_extractor') +DETECTORS = Registry('detectors') +BBOX_ASSIGNERS = Registry('bbox_assigner') +BBOX_SAMPLERS = Registry('bbox_sampler') +BBOX_CODERS = Registry('bbox_coder') +ESTIMATORS = Registry('estimator') +MULTIMODAL = Registry('multimodal') +SEGMENT = Registry('segment') diff --git a/applications/EIVideo/EIVideo/paddlevideo/modeling/weight_init.py b/applications/EIVideo/EIVideo/paddlevideo/modeling/weight_init.py new file mode 100644 index 0000000000000000000000000000000000000000..479129eb6ab7d6a924db72c10e911180bb72af5d --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/modeling/weight_init.py @@ -0,0 +1,158 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import paddle +import paddle.nn.initializer as init +import numpy as np +from scipy import special + + +def weight_init_(layer, + func, + weight_name=None, + bias_name=None, + bias_value=0.0, + **kwargs): + """ + In-place params init function. + Usage: + .. code-block:: python + + import paddle + import numpy as np + + data = np.ones([3, 4], dtype='float32') + linear = paddle.nn.Linear(4, 4) + input = paddle.to_tensor(data) + print(linear.weight) + linear(input) + + weight_init_(linear, 'Normal', 'fc_w0', 'fc_b0', std=0.01, mean=0.1) + print(linear.weight) + """ + + if hasattr(layer, 'weight') and layer.weight is not None: + getattr(init, func)(**kwargs)(layer.weight) + if weight_name is not None: + # override weight name + layer.weight.name = weight_name + + if hasattr(layer, 'bias') and layer.bias is not None: + init.Constant(bias_value)(layer.bias) + if bias_name is not None: + # override bias name + layer.bias.name = bias_name + + +def _no_grad_trunc_normal_(tensor, mean, std, a, b): + def norm_cdf(x): + # Computes standard normal cumulative distribution function + return (1. + math.erf(x / math.sqrt(2.))) / 2. + + if (mean < a - 2 * std) or (mean > b + 2 * std): + print("mean is more than 2 std from [a, b] in nn.init.trunc_normal_. " + "The distribution of values may be incorrect.") + + with paddle.no_grad(): + # Values are generated by using a truncated uniform distribution and + # then using the inverse CDF for the normal distribution. + # Get upper and lower cdf values + l = norm_cdf((a - mean) / std) + u = norm_cdf((b - mean) / std) + + # Uniformly fill tensor with values from [l, u], then translate to [2l-1, 2u-1]. + tmp = np.random.uniform(2 * l - 1, 2 * u - 1, + size=list(tensor.shape)).astype(np.float32) + + # Use inverse cdf transform for normal distribution to get truncated + # standard normal + tmp = special.erfinv(tmp) + + # Transform to proper mean, std + tmp *= (std * math.sqrt(2.0)) + tmp += mean + + # Clamp to ensure it's in the proper range + tmp = np.clip(tmp, a, b) + tensor.set_value(paddle.to_tensor(tmp)) + + return tensor + + +def _calculate_fan_in_and_fan_out(tensor): + dimensions = tensor.dim() + if dimensions < 2: + raise ValueError( + "Fan in and fan out can not be computed for tensor with fewer than 2 dimensions" + ) + + num_input_fmaps = tensor.shape[1] + num_output_fmaps = tensor.shape[0] + receptive_field_size = 1 + if tensor.dim() > 2: + receptive_field_size = tensor[0][0].numel() + fan_in = num_input_fmaps * receptive_field_size + fan_out = num_output_fmaps * receptive_field_size + + return fan_in, fan_out + + +def trunc_normal_(tensor, mean=0., std=1., a=-2., b=2.): + return _no_grad_trunc_normal_(tensor, mean, std, a, b) + + +def kaiming_normal_(tensor, a=0., mode='fan_in', nonlinearity='leaky_relu'): + def _calculate_correct_fan(tensor, mode): + mode = mode.lower() + valid_modes = ['fan_in', 'fan_out'] + if mode not in valid_modes: + raise ValueError( + "Mode {} not supported, please use one of {}".format( + mode, valid_modes)) + + fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor) + return fan_in if mode == 'fan_in' else fan_out + + def calculate_gain(nonlinearity, param=None): + linear_fns = [ + 'linear', 'conv1d', 'conv2d', 'conv3d', 'conv_transpose1d', + 'conv_transpose2d', 'conv_transpose3d' + ] + if nonlinearity in linear_fns or nonlinearity == 'sigmoid': + return 1 + elif nonlinearity == 'tanh': + return 5.0 / 3 + elif nonlinearity == 'relu': + return math.sqrt(2.0) + elif nonlinearity == 'leaky_relu': + if param is None: + negative_slope = 0.01 + elif not isinstance(param, bool) and isinstance( + param, int) or isinstance(param, float): + negative_slope = param + else: + raise ValueError( + "negative_slope {} not a valid number".format(param)) + return math.sqrt(2.0 / (1 + negative_slope**2)) + else: + raise ValueError( + "Unsupported nonlinearity {}".format(nonlinearity)) + + fan = _calculate_correct_fan(tensor, mode) + gain = calculate_gain(nonlinearity, a) + std = gain / math.sqrt(fan) + with paddle.no_grad(): + paddle.nn.initializer.Normal(0, std)(tensor) + return tensor diff --git a/applications/EIVideo/EIVideo/paddlevideo/tasks/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/tasks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..45d1d0c090f1cb9b53e20a3959af9d361bf6e919 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/tasks/__init__.py @@ -0,0 +1,19 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .test import test_model + +__all__ = [ + 'test_model', +] diff --git a/applications/EIVideo/EIVideo/paddlevideo/tasks/test.py b/applications/EIVideo/EIVideo/paddlevideo/tasks/test.py new file mode 100644 index 0000000000000000000000000000000000000000..c92bb1293520483bb0eb3ac3b8d8f2f78e601383 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/tasks/test.py @@ -0,0 +1,39 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from EIVideo.paddlevideo.utils import get_logger, load +from ..loader.builder import build_dataloader, build_dataset +from ..metrics import build_metric +from ..modeling.builder import build_model +from ..modeling.framework import Manet + +logger = get_logger("paddlevideo") + + +@paddle.no_grad() +def test_model(cfg, weights, parallel=True): + """Test model entry + + Args: + cfg (dict): configuration. + weights (str): weights path to load. + parallel (bool): Whether to do multi-cards testing. Default: True. + + """ + if cfg.MODEL.framework == "Manet": + cfg_helper = {"knns": 1, "is_save_image": True} + cfg.update(cfg_helper) + final = Manet().test_step(**cfg, weights=weights, parallel=False) + return final diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/__init__.py b/applications/EIVideo/EIVideo/paddlevideo/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d18561d76e9424e06f596a2baa92ca2b7fe430cc --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .registry import Registry +from .build_utils import build +from .config import * +from .logger import setup_logger, coloring, get_logger +from .record import AverageMeter, build_record, log_batch, log_epoch +from .dist_utils import get_dist_info, main_only +from .save_load import save, load, load_ckpt, mkdir +from .precise_bn import do_preciseBN +from .profiler import add_profiler_step +__all__ = ['Registry', 'build'] diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/build_utils.py b/applications/EIVideo/EIVideo/paddlevideo/utils/build_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..73c0ca46bae477837411a010459a4ed08549d2ee --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/build_utils.py @@ -0,0 +1,35 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def build(cfg, registry, key='name'): + """Build a module from config dict. + Args: + cfg (dict): Config dict. It should at least contain the key. + registry (XXX): The registry to search the type from. + key (str): the key. + Returns: + obj: The constructed object. + """ + + assert isinstance(cfg, dict) and key in cfg + + cfg_copy = cfg.copy() + obj_type = cfg_copy.pop(key) + + obj_cls = registry.get(obj_type) + if obj_cls is None: + raise KeyError('{} is not in the {} registry'.format( + obj_type, registry.name)) + return obj_cls(**cfg_copy) diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/config.py b/applications/EIVideo/EIVideo/paddlevideo/utils/config.py new file mode 100644 index 0000000000000000000000000000000000000000..9db59bd5e5fcf827b59d0ace15b5fbd320b6ba2e --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/config.py @@ -0,0 +1,174 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import yaml +from EIVideo.paddlevideo.utils.logger import coloring, setup_logger + +__all__ = ['get_config'] + +logger = setup_logger("./", name="paddlevideo", level="INFO") + + +class AttrDict(dict): + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value + + +def create_attr_dict(yaml_config): + from ast import literal_eval + for key, value in yaml_config.items(): + if type(value) is dict: + yaml_config[key] = value = AttrDict(value) + if isinstance(value, str): + try: + value = literal_eval(value) + except BaseException: + pass + if isinstance(value, AttrDict): + create_attr_dict(yaml_config[key]) + else: + yaml_config[key] = value + + +def parse_config(cfg_file): + """Load a config file into AttrDict""" + with open(cfg_file, 'r') as fopen: + yaml_config = AttrDict(yaml.load(fopen, Loader=yaml.SafeLoader)) + create_attr_dict(yaml_config) + return yaml_config + + +def print_dict(d, delimiter=0): + """ + Recursively visualize a dict and + indenting acrrording by the relationship of keys. + """ + placeholder = "-" * 60 + for k, v in sorted(d.items()): + if isinstance(v, dict): + logger.info("{}{} : ".format(delimiter * " ", coloring(k, + "HEADER"))) + print_dict(v, delimiter + 4) + elif isinstance(v, list) and len(v) >= 1 and isinstance(v[0], dict): + logger.info("{}{} : ".format(delimiter * " ", + coloring(str(k), "HEADER"))) + for value in v: + print_dict(value, delimiter + 4) + else: + logger.info("{}{} : {}".format(delimiter * " ", + coloring(k, "HEADER"), + coloring(v, "OKGREEN"))) + + if k.isupper(): + logger.info(placeholder) + + +def print_config(config): + """ + visualize configs + Arguments: + config: configs + """ + print_dict(config) + + +def check_config(config): + """ + Check config + """ + pass + + +def override(dl, ks, v): + """ + Recursively replace dict of list + Args: + dl(dict or list): dict or list to be replaced + ks(list): list of keys + v(str): value to be replaced + """ + def str2num(v): + try: + return eval(v) + except Exception: + return v + + assert isinstance(dl, (list, dict)), ("{} should be a list or a dict") + assert len(ks) > 0, ('lenght of keys should larger than 0') + if isinstance(dl, list): + k = str2num(ks[0]) + if len(ks) == 1: + assert k < len(dl), ('index({}) out of range({})'.format(k, dl)) + dl[k] = str2num(v) + else: + override(dl[k], ks[1:], v) + else: + if len(ks) == 1: + #assert ks[0] in dl, ('{} is not exist in {}'.format(ks[0], dl)) + if not ks[0] in dl: + logger.warning('A new filed ({}) detected!'.format(ks[0], dl)) + dl[ks[0]] = str2num(v) + else: + assert ks[0] in dl, ( + '({}) doesn\'t exist in {}, a new dict field is invalid'.format( + ks[0], dl)) + override(dl[ks[0]], ks[1:], v) + + +def override_config(config, options=None): + """ + Recursively override the config + Args: + config(dict): dict to be replaced + options(list): list of pairs(key0.key1.idx.key2=value) + such as: [ + epochs=20', + 'PIPELINE.train.transform.1.ResizeImage.resize_short=300' + ] + Returns: + config(dict): replaced config + """ + if options is not None: + for opt in options: + assert isinstance(opt, + str), ("option({}) should be a str".format(opt)) + assert "=" in opt, ( + "option({}) should contain a =" + "to distinguish between key and value".format(opt)) + pair = opt.split('=') + assert len(pair) == 2, ("there can be only a = in the option") + key, value = pair + keys = key.split('.') + override(config, keys, value) + + return config + + +def get_config(fname, overrides=None, show=True): + """ + Read config from file + """ + assert os.path.exists(fname), ('config file({}) is not exist'.format(fname)) + config = parse_config(fname) + override_config(config, overrides) + if show: + print_config(config) + check_config(config) + return config diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/dist_utils.py b/applications/EIVideo/EIVideo/paddlevideo/utils/dist_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7659e88c127aa2312777c5cdd3d0afbdcdf07e6c --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/dist_utils.py @@ -0,0 +1,30 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import functools + +import paddle +import paddle.distributed as dist + +def get_dist_info(): + world_size = dist.get_world_size() + rank = dist.get_rank() + return rank, world_size + +def main_only(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + rank, _ = get_dist_info() + if rank == 0: + return func(*args, **kwargs) + return wrapper diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/logger.py b/applications/EIVideo/EIVideo/paddlevideo/utils/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..e9791b89b68ddd302e32f350ab7b96eda9f4ce36 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/logger.py @@ -0,0 +1,113 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +import sys +import datetime + +from paddle.distributed import ParallelEnv + + + +Color = { + 'RED': '\033[31m', + 'HEADER': '\033[35m', # deep purple + 'PURPLE': '\033[95m', # purple + 'OKBLUE': '\033[94m', + 'OKGREEN': '\033[92m', + 'WARNING': '\033[93m', + 'FAIL': '\033[91m', + 'ENDC': '\033[0m' +} + + +def coloring(message, color="OKGREEN"): + assert color in Color.keys() + if os.environ.get('COLORING', True): + return Color[color] + str(message) + Color["ENDC"] + else: + return message + + +logger_initialized = [] + + +def setup_logger(output=None, name="paddlevideo", level="INFO"): + """ + Initialize the paddlevideo logger and set its verbosity level to "INFO". + Args: + output (str): a file name or a directory to save log. If None, will not save log file. + If ends with ".txt" or ".log", assumed to be a file name. + Otherwise, logs will be saved to `output/log.txt`. + name (str): the root module name of this logger + Returns: + logging.Logger: a logger + """ + def time_zone(sec, fmt): + real_time = datetime.datetime.now() + return real_time.timetuple() + logging.Formatter.converter = time_zone + + logger = logging.getLogger(name) + if level == "INFO": + logger.setLevel(logging.INFO) + elif level=="DEBUG": + logger.setLevel(logging.DEBUG) + logger.propagate = False + + if level == "DEBUG": + plain_formatter = logging.Formatter( + "[%(asctime)s] %(name)s %(levelname)s: %(message)s", + datefmt="%m/%d %H:%M:%S") + else: + plain_formatter = logging.Formatter( + "[%(asctime)s] %(message)s", + datefmt="%m/%d %H:%M:%S") + # stdout logging: master only + local_rank = ParallelEnv().local_rank + if local_rank == 0: + ch = logging.StreamHandler(stream=sys.stdout) + ch.setLevel(logging.DEBUG) + formatter = plain_formatter + ch.setFormatter(formatter) + logger.addHandler(ch) + + # file logging: all workers + if output is not None: + if output.endswith(".txt") or output.endswith(".log"): + filename = output + else: + filename = os.path.join(output, ".log.txt") + if local_rank > 0: + filename = filename + ".rank{}".format(local_rank) + + # PathManager.mkdirs(os.path.dirname(filename)) + os.makedirs(os.path.dirname(filename), exist_ok=True) + + # fh = logging.StreamHandler(_cached_log_stream(filename) + fh = logging.FileHandler(filename, mode='a') + fh.setLevel(logging.DEBUG) + fh.setFormatter(plain_formatter) + logger.addHandler(fh) + logger_initialized.append(name) + return logger + + +def get_logger(name, output=None): + logger = logging.getLogger(name) + if name in logger_initialized: + return logger + + return setup_logger(name=name, output=name) diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/manet_utils.py b/applications/EIVideo/EIVideo/paddlevideo/utils/manet_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e9209641afbad81365eea2f6c82f26b3a6efd827 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/manet_utils.py @@ -0,0 +1,1295 @@ +from __future__ import absolute_import + +import json +import math +import os +import pickle +import warnings + +import numpy +import numpy as np +from numpy import inf +from paddle import Tensor, concat, reshape, nn +import paddle + +from typing import Union, Iterable + +# from reprod_log.compare import compute_diff +# from reprod_log.utils import check_print_diff, np2torch, np2paddle, torch2np, paddle2np + +_tensor_or_tensors = Union[paddle.Tensor, Iterable[paddle.Tensor]] +_palette = [ + 0, 0, 0, 128, 0, 0, 0, 128, 0, 128, 128, 0, 0, 0, 128, 128, 0, 128, 0, 128, + 128, 128, 128, 128, 64, 0, 0, 191, 0, 0, 64, 128, 0, 191, 128, 0, 64, 0, + 128, 191, 0, 128, 64, 128, 128, 191, 128, 128, 0, 64, 0, 128, 64, 0, 0, 191, + 0, 128, 191, 0, 0, 64, 128, 128, 64, 128, 22, 22, 22, 23, 23, 23, 24, 24, + 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, + 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, + 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, + 43, 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 49, + 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, + 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, 61, 61, 61, 62, 62, + 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 68, 68, 68, + 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, 73, 73, 74, 74, 74, 75, + 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 80, 80, 80, 81, 81, + 81, 82, 82, 82, 83, 83, 83, 84, 84, 84, 85, 85, 85, 86, 86, 86, 87, 87, 87, + 88, 88, 88, 89, 89, 89, 90, 90, 90, 91, 91, 91, 92, 92, 92, 93, 93, 93, 94, + 94, 94, 95, 95, 95, 96, 96, 96, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, + 100, 100, 101, 101, 101, 102, 102, 102, 103, 103, 103, 104, 104, 104, 105, + 105, 105, 106, 106, 106, 107, 107, 107, 108, 108, 108, 109, 109, 109, 110, + 110, 110, 111, 111, 111, 112, 112, 112, 113, 113, 113, 114, 114, 114, 115, + 115, 115, 116, 116, 116, 117, 117, 117, 118, 118, 118, 119, 119, 119, 120, + 120, 120, 121, 121, 121, 122, 122, 122, 123, 123, 123, 124, 124, 124, 125, + 125, 125, 126, 126, 126, 127, 127, 127, 128, 128, 128, 129, 129, 129, 130, + 130, 130, 131, 131, 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, 135, + 135, 135, 136, 136, 136, 137, 137, 137, 138, 138, 138, 139, 139, 139, 140, + 140, 140, 141, 141, 141, 142, 142, 142, 143, 143, 143, 144, 144, 144, 145, + 145, 145, 146, 146, 146, 147, 147, 147, 148, 148, 148, 149, 149, 149, 150, + 150, 150, 151, 151, 151, 152, 152, 152, 153, 153, 153, 154, 154, 154, 155, + 155, 155, 156, 156, 156, 157, 157, 157, 158, 158, 158, 159, 159, 159, 160, + 160, 160, 161, 161, 161, 162, 162, 162, 163, 163, 163, 164, 164, 164, 165, + 165, 165, 166, 166, 166, 167, 167, 167, 168, 168, 168, 169, 169, 169, 170, + 170, 170, 171, 171, 171, 172, 172, 172, 173, 173, 173, 174, 174, 174, 175, + 175, 175, 176, 176, 176, 177, 177, 177, 178, 178, 178, 179, 179, 179, 180, + 180, 180, 181, 181, 181, 182, 182, 182, 183, 183, 183, 184, 184, 184, 185, + 185, 185, 186, 186, 186, 187, 187, 187, 188, 188, 188, 189, 189, 189, 190, + 190, 190, 191, 191, 191, 192, 192, 192, 193, 193, 193, 194, 194, 194, 195, + 195, 195, 196, 196, 196, 197, 197, 197, 198, 198, 198, 199, 199, 199, 200, + 200, 200, 201, 201, 201, 202, 202, 202, 203, 203, 203, 204, 204, 204, 205, + 205, 205, 206, 206, 206, 207, 207, 207, 208, 208, 208, 209, 209, 209, 210, + 210, 210, 211, 211, 211, 212, 212, 212, 213, 213, 213, 214, 214, 214, 215, + 215, 215, 216, 216, 216, 217, 217, 217, 218, 218, 218, 219, 219, 219, 220, + 220, 220, 221, 221, 221, 222, 222, 222, 223, 223, 223, 224, 224, 224, 225, + 225, 225, 226, 226, 226, 227, 227, 227, 228, 228, 228, 229, 229, 229, 230, + 230, 230, 231, 231, 231, 232, 232, 232, 233, 233, 233, 234, 234, 234, 235, + 235, 235, 236, 236, 236, 237, 237, 237, 238, 238, 238, 239, 239, 239, 240, + 240, 240, 241, 241, 241, 242, 242, 242, 243, 243, 243, 244, 244, 244, 245, + 245, 245, 246, 246, 246, 247, 247, 247, 248, 248, 248, 249, 249, 249, 250, + 250, 250, 251, 251, 251, 252, 252, 252, 253, 253, 253, 254, 254, 254, 255, + 255, 255 +] + +# paddle.set_device('gpu') if paddle.is_compiled_with_cuda() else paddle.set_device('cpu') + +import paddle +import PIL +import numbers +import numpy as np +from PIL import Image +from paddle.vision.transforms import BaseTransform +from paddle.vision.transforms import functional as F + +import numpy as np +from scipy.ndimage import interpolation, binary_dilation +try: + from skimage import morphology, transform +except ImportError as e: + print( + f"{e}, [scikit-image] package and it's dependencies is required for EIVideo." + ) +import paddle +import cv2 +import random + + +#### +def mask_damager(labels=None, p_black=0.2): + scales = (0.8, 1.0, 1.2) + kernel_size = random.randint(10, 15) + kernel = np.ones((kernel_size, kernel_size), np.uint8) + if random.random() < p_black: + final_label = paddle.zeros_like(labels) + final_label = final_label.squeeze().numpy() + else: + prot = random.randint(5, 15) + nrot = random.randint(-15, -5) + rots = [prot, nrot, 0] + rot = rots[random.randint(0, 2)] + + sc = scales[random.randint(0, 2)] + _, _, h, w = labels.shape + tmp = labels.squeeze() + + tmp = tmp.unsqueeze(-1) + tmp = tmp.numpy().astype(np.uint8) + morph_p = random.random() + if morph_p < 0.5: + tmp = cv2.morphologyEx(tmp, cv2.MORPH_OPEN, kernel) + else: + tmp = cv2.morphologyEx(tmp, cv2.MORPH_CLOSE, kernel) + + tmp = tmp.astype(np.uint8) + center = (w / 2, h / 2) + M = cv2.getRotationMatrix2D(center, rot, sc) + final_label = cv2.warpAffine(tmp, M, (w, h), cv2.INTER_NEAREST) + + return final_label + + +color_map = [ + [0, 0, 0], + [255, 127, 0], + [30, 144, 255], + [186, 85, 211], + [255, 105, 180], + [192, 255, 62], + [255, 105, 180], + [50, 255, 255], +] + +color_map_np = np.array(color_map) + + +def overlay_davis(image, mask, alpha=0.5): + """ Overlay segmentation on top of RGB image. from davis official""" + im_overlay = image.copy() + mask = mask.astype('uint8') + colored_mask = color_map_np[mask] + foreground = image * alpha + (1 - alpha) * colored_mask + binary_mask = (mask > 0) + # Compose image + im_overlay[binary_mask] = foreground[binary_mask] + countours = binary_dilation(binary_mask) ^ binary_mask + im_overlay[countours, :] = 0 + return im_overlay.astype(image.dtype) + + +# TODO +def submit_masks(masks, images, inter_file_path): + overlays = [] + save_result_path = os.path.join(inter_file_path, 'result') + os.makedirs(save_result_path, exist_ok=True) + for imgname, (mask, image) in enumerate(zip(masks, images)): + overlay = overlay_davis(image, mask) + overlays.append(overlay.tolist()) + overlay = Image.fromarray(overlay) + imgname = str(imgname) + while len(imgname) < 5: + imgname = '0' + imgname + overlay.save(os.path.join(save_result_path, imgname + '.png')) + result = {'overlays': overlays} + # result = {'masks': masks.tolist()} + with open(os.path.join(save_result_path, 'masks.json'), 'w') as f: + json.dump(result, f) + + +def load_video(path, min_side=None): + frame_list = [] + cap = cv2.VideoCapture(path) + while (cap.isOpened()): + _, frame = cap.read() + if frame is None: + break + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + if min_side: + h, w = frame.shape[:2] + new_w = (w * min_side // min(w, h)) + new_h = (h * min_side // min(w, h)) + frame = cv2.resize(frame, (new_w, new_h), + interpolation=cv2.INTER_CUBIC) + # .transpose([2, 0, 1]) + frame_list.append(frame) + frames = np.stack(frame_list, axis=0) + return frames + + +def get_scribbles(): + for i in range(8): + with open(f'/home/lc/paddlevideo/data/bike-packing/lable/{i + 1}.json' + ) as f: + scribbles = json.load(f) + first_scribble = not i + yield scribbles, first_scribble + + +def get_images(sequence='bike-packing'): + img_path = os.path.join('/home/lc/paddlevideo/data', sequence.strip(), + 'frame') + img_files = os.listdir(img_path) + img_files.sort() + files = [] + for img in img_files: + img_file = np.array(Image.open(os.path.join(img_path, img))) + files.append(img_file) + return np.array(files) + + +def rough_ROI(ref_scribble_labels): + #### b*1*h*w + dist = 20 + b, _, h, w = ref_scribble_labels.shape + filter_ = paddle.zeros_like(ref_scribble_labels) + to_fill = paddle.zeros_like(ref_scribble_labels) + for i in range(b): + no_background = (ref_scribble_labels[i] != -1) + no_background = no_background.squeeze(0) + + no_b = no_background.nonzero() + (h_min, w_min) = paddle.min(no_b, 0) + (h_max, w_max) = paddle.max(no_b, 0) + filter_[i, 0, + max(h_min - dist, 0):min(h_max + dist, h - 1), + max(w_min - dist, 0):min(w_max + dist, w - 1)] = 1 + + final_scribble_labels = paddle.where(byte_(filter_), ref_scribble_labels, + to_fill) + return final_scribble_labels + + +import os.path as osp + + +def load(file_name, model, **cfg): + if not osp.isfile(file_name): + raise IOError(f'{file_name} not exist') + try: + state_dicts_ = paddle.load(file_name)['state_dict'] + except: + state_dicts_ = paddle.load(file_name) + state_dicts = {} + for k in model.keys(): + if 'num_batches_tracked' not in k: + if ('head.' + k) not in state_dicts_.keys(): + if k not in state_dicts_.keys(): + print(f'model -----{k} -------is not in pretrained') + else: + state_dicts[k] = state_dicts_[k] + else: + state_dicts[k] = state_dicts_['head.' + k] + write_dict(state_dicts, 'state_dicts.txt', **cfg) + write_dict(model, 'model.txt', **cfg) + return state_dicts + + +##### +def write_dict(state_dict, file_name, **cfg): + lines = [] + tot = 0 + for k, v in state_dict.items(): + # 目前只发现了torch和paddle模型参数命名的这三种不一致 + # 不一致1 + if 'num_batches_tracked' in k: + tot += 1 + continue + try: + line = str(k) + '\t' + str(v.cpu().detach().numpy().shape) + '\n' + except: + line = str(k) + '\t' + str(v.shape) + '\n' + lines.append(line) + # with open(cfg.get("output_dir", f"./output/{file_name}"), 'w') as f: + # f.writelines(lines) + # print('%d num_batches_tracked skipped' % tot) + + +def damage_masks(labels, shift=True, scale=True, rotate=True): + """ + Args: + labels: numpy array (batch_size * 1 * h * w) + """ + bs, _, h, w = labels.shape + labels = labels.transpose([0, 2, 3, 1]) + labels = labels.numpy() + final_label = [] + for i in range(bs): + label = labels[i] + damaged_label = damage_masks_np(label, shift, scale, rotate) + final_label.append(damaged_label) + final_label = np.array(final_label) + final_label = paddle.to_tensor(final_label) + final_label = final_label.transpose([0, 3, 1, 2]) + return final_label + + +def damage_masks_np(labels, shift=True, scale=True, rotate=True): + """Performs the actual mask damaging in numpy. + Args: + labels: Int32 numpy array of shape (height, width, 1). + shift: Boolean, whether to damage the masks by shifting. + scale: Boolean, whether to damage the masks by scaling. + rotate: Boolean, whether to damage the masks by rotation. + dilate: Boolean, whether to damage the masks by dilation. + Returns: + The damaged version of labels. + """ + unique_labels = np.unique(labels) + unique_labels = np.setdiff1d(unique_labels, [0]) + # Shuffle to get random depth ordering when combining together. + np.random.shuffle(unique_labels) + damaged_labels = np.zeros_like(labels) + for l in unique_labels: + obj_mask = (labels == l) + damaged_obj_mask = _damage_single_object_mask(obj_mask, shift, scale, + rotate) + damaged_labels[damaged_obj_mask] = l + return damaged_labels + + +def _damage_single_object_mask(mask, shift, scale, rotate): + """Performs mask damaging in numpy for a single object. + Args: + mask: Boolean numpy array of shape(height, width, 1). + shift: Boolean, whether to damage the masks by shifting. + scale: Boolean, whether to damage the masks by scaling. + rotate: Boolean, whether to damage the masks by rotation. + dilate: Boolean, whether to damage the masks by dilation. + Returns: + The damaged version of mask. + """ + if shift: + mask = _shift_mask(mask) + if scale: + mask = _scale_mask(mask) + if rotate: + mask = _rotate_mask(mask) + return mask + + +def _shift_mask(mask, max_shift_factor=0.05): + """Damages a mask for a single object by randomly shifting it in numpy. + Args: + mask: Boolean numpy array of shape(height, width, 1). + max_shift_factor: Float scalar, the maximum factor for random shifting. + Returns: + The shifted version of mask. + """ + nzy, nzx, _ = mask.nonzero() + h = nzy.max() - nzy.min() + w = nzx.max() - nzx.min() + size = np.sqrt(h * w) + offset = np.random.uniform(-size * max_shift_factor, + size * max_shift_factor, 2) + shifted_mask = interpolation.shift(np.squeeze(mask, axis=2), + offset, + order=0).astype('bool')[..., np.newaxis] + return shifted_mask + + +def _scale_mask(mask, scale_amount=0.025): + """Damages a mask for a single object by randomly scaling it in numpy. + Args: + mask: Boolean numpy array of shape(height, width, 1). + scale_amount: Float scalar, the maximum factor for random scaling. + Returns: + The scaled version of mask. + """ + nzy, nzx, _ = mask.nonzero() + cy = 0.5 * (nzy.max() - nzy.min()) + cx = 0.5 * (nzx.max() - nzx.min()) + scale_factor = np.random.uniform(1.0 - scale_amount, 1.0 + scale_amount) + shift = transform.SimilarityTransform(translation=[-cx, -cy]) + inv_shift = transform.SimilarityTransform(translation=[cx, cy]) + s = transform.SimilarityTransform(scale=[scale_factor, scale_factor]) + m = (shift + (s + inv_shift)).inverse + scaled_mask = transform.warp(mask, m) > 0.5 + return scaled_mask + + +def _rotate_mask(mask, max_rot_degrees=3.0): + """Damages a mask for a single object by randomly rotating it in numpy. + Args: + mask: Boolean numpy array of shape(height, width, 1). + max_rot_degrees: Float scalar, the maximum number of degrees to rotate. + Returns: + The scaled version of mask. + """ + cy = 0.5 * mask.shape[0] + cx = 0.5 * mask.shape[1] + rot_degrees = np.random.uniform(-max_rot_degrees, max_rot_degrees) + shift = transform.SimilarityTransform(translation=[-cx, -cy]) + inv_shift = transform.SimilarityTransform(translation=[cx, cy]) + r = transform.SimilarityTransform(rotation=np.deg2rad(rot_degrees)) + m = (shift + (r + inv_shift)).inverse + scaled_mask = transform.warp(mask, m) > 0.5 + return scaled_mask + + +class AverageMeter(object): + """Computes and stores the average and current value""" + def __init__(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + +import numpy as np + + +def label2colormap(label): + m = label.astype(np.uint8) + r, c = m.shape + cmap = np.zeros((r, c, 3), dtype=np.uint8) + cmap[:, :, 0] = (m & 1) << 7 | (m & 8) << 3 | (m & 64) >> 1 + cmap[:, :, 1] = (m & 2) << 6 | (m & 16) << 2 | (m & 128) >> 2 + cmap[:, :, 2] = (m & 4) << 5 | (m & 32) << 1 + return cmap + + +def torch2paddle(data): + try: + import torch + if isinstance(data, dict): + np_data = {} + for k, v in data.items(): + np_data[k] = paddle.to_tensor(v.detach().numpy()) + return np_data + else: + return paddle.to_tensor(data.detach().numpy()) + except: + pass + + +def fill_(tensor: Tensor, value): + return tensor.set_value(paddle.full_like(tensor, value)) + + +def zero_(tensor: Tensor): + return tensor.set_value(paddle.zeros_like(tensor)) + + +def float_(tensor: Tensor): + return paddle.to_tensor(tensor, dtype='float32') + + +def long_(tensor: Tensor): + return paddle.to_tensor(tensor, dtype='int64') + + +def int_(tensor: Tensor): + return paddle.to_tensor(tensor, dtype='int32') + + +def byte_(tensor: Tensor): + return paddle.to_tensor(tensor, dtype='bool') + + +class ToPILImage(BaseTransform): + def __init__(self, mode=None, keys=None): + super(ToPILImage, self).__init__(keys) + + def _apply_image(self, pic): + """ + Args: + pic (Tensor|np.ndarray): Image to be converted to PIL Image. + Returns: + PIL: Converted image. + """ + if not (isinstance(pic, paddle.Tensor) or isinstance(pic, np.ndarray)): + raise TypeError('pic should be Tensor or ndarray. Got {}.'.format( + type(pic))) + + elif isinstance(pic, paddle.Tensor): + if pic.ndimension() not in {2, 3}: + raise ValueError( + 'pic should be 2/3 dimensional. Got {} dimensions.'.format( + pic.ndimension())) + + elif pic.ndimension() == 2: + # if 2D image, add channel dimension (CHW) + pic = pic.unsqueeze(0) + + elif isinstance(pic, np.ndarray): + if pic.ndim not in {2, 3}: + raise ValueError( + 'pic should be 2/3 dimensional. Got {} dimensions.'.format( + pic.ndim)) + + elif pic.ndim == 2: + # if 2D image, add channel dimension (HWC) + pic = np.expand_dims(pic, 2) + + npimg = pic + if isinstance(pic, paddle.Tensor) and "float" in str( + pic.numpy().dtype) and self.mode != 'F': + pic = pic.mul(255).byte() + if isinstance(pic, paddle.Tensor): + npimg = np.transpose(pic.numpy(), (1, 2, 0)) + + if not isinstance(npimg, np.ndarray): + raise TypeError( + 'Input pic must be a paddle.Tensor or NumPy ndarray, ' + + 'not {}'.format(type(npimg))) + + if npimg.shape[2] == 1: + expected_mode = None + npimg = npimg[:, :, 0] + if npimg.dtype == np.uint8: + expected_mode = 'L' + elif npimg.dtype == np.int16: + expected_mode = 'I;16' + elif npimg.dtype == np.int32: + expected_mode = 'I' + elif npimg.dtype == np.float32: + expected_mode = 'F' + if self.mode is not None and self.mode != expected_mode: + raise ValueError( + "Incorrect self.mode ({}) supplied for input type {}. Should be {}" + .format(self.mode, np.dtype, expected_mode)) + self.mode = expected_mode + + elif npimg.shape[2] == 2: + permitted_2_channel_modes = ['LA'] + if self.mode is not None and self.mode not in permitted_2_channel_modes: + raise ValueError( + "Only self.modes {} are supported for 2D inputs".format( + permitted_2_channel_modes)) + + if self.mode is None and npimg.dtype == np.uint8: + self.mode = 'LA' + + elif npimg.shape[2] == 4: + permitted_4_channel_modes = ['RGBA', 'CMYK', 'RGBX'] + if self.mode is not None and self.mode not in permitted_4_channel_modes: + raise ValueError( + "Only self.modes {} are supported for 4D inputs".format( + permitted_4_channel_modes)) + + if self.mode is None and npimg.dtype == np.uint8: + self.mode = 'RGBA' + else: + permitted_3_channel_modes = ['RGB', 'YCbCr', 'HSV'] + if self.mode is not None and self.mode not in permitted_3_channel_modes: + raise ValueError( + "Only self.modes {} are supported for 3D inputs".format( + permitted_3_channel_modes)) + if self.mode is None and npimg.dtype == np.uint8: + self.mode = 'RGB' + + if self.mode is None: + raise TypeError('Input type {} is not supported'.format( + npimg.dtype)) + + return Image.fromarray(npimg, mode=self.mode) + + +class Identity(nn.Layer): + r"""A placeholder identity operator that is argument-insensitive. + + Args: + args: any argument (unused) + kwargs: any keyword argument (unused) + """ + def __init__(self, *args, **kwargs): + super(Identity, self).__init__() + + def forward(self, input): + return input + + +def convert(data: dict, to, dtype=None): + assert isinstance(data, dict) + input = {} + for k, v in data.items(): + + if 'paddle' == to: + if isinstance(v, np.ndarray): + if dtype is not None: + input[k] = paddle.to_tensor(v.astype(dtype)) + else: + input[k] = paddle.to_tensor(v) + else: + input[k] = v + elif 'torch' == to: + try: + import torch + if isinstance(v, np.ndarray): + if dtype is not None: + input[k] = torch.tensor(v.astype(dtype)) + else: + input[k] = torch.tensor(v) + else: + input[k] = v + except: + pass + else: + if isinstance(v, np.ndarray): + input[k] = v.astype(to) + else: + input[k] = v + return input + + +def clip_grad_norm_(parameters: _tensor_or_tensors, + max_norm: float, + norm_type: float = 2.0, + error_if_nonfinite: bool = False) -> paddle.Tensor: + r"""Clips gradient norm of an iterable of parameters. + + The norm is computed over all gradients together, as if they were + concatenated into a single vector. Gradients are modified in-place. + + Args: + parameters (Iterable[Tensor] or Tensor): an iterable of Tensors or a + single Tensor that will have gradients normalized + max_norm (float or int): max norm of the gradients + norm_type (float or int): type of the used p-norm. Can be ``'inf'`` for + infinity norm. + error_if_nonfinite (bool): if True, an error is thrown if the total + norm of the gradients from :attr:``parameters`` is ``nan``, + ``inf``, or ``-inf``. Default: False (will switch to True in the future) + + Returns: + Total norm of the parameters (viewed as a single vector). + """ + import time + if isinstance(parameters, paddle.Tensor): + parameters = [parameters] + parameters = [p for p in parameters if p.grad is not None] + detached_grads = [p.grad.detach() for p in parameters] + + max_norm = float(max_norm) + norm_type = float(norm_type) + if len(parameters) == 0: + return paddle.to_tensor(0.) + # device = paddle.get_device() # parameters[0].grad.device + if norm_type == inf: + norms = [p.abs().max() for p in parameters] + total_norm = norms[0] if len(norms) == 1 else paddle.max( + paddle.stack(norms)) + else: + # tik = time.time() + total_norm = paddle.norm( + paddle.stack([paddle.norm(g, norm_type) for g in detached_grads]), + norm_type) + # total_norm = paddle.norm(paddle.stack([paddle.sqrt(paddle.sum(g*g)) for g in detached_grads]), norm_type) # fixed. + # print(time.time() - tik) + if error_if_nonfinite and paddle.logical_or(total_norm.isnan(), + total_norm.isinf()): + raise RuntimeError( + f'The total norm of order {norm_type} for gradients from ' + '`parameters` is non-finite, so it cannot be clipped. To disable ' + 'this error and scale the gradients by the non-finite norm anyway, ' + 'set `error_if_nonfinite=False`') + clip_coef = max_norm / (total_norm + 1e-6) + # Note: multiplying by the clamped coef is redundant when the coef is clamped to 1, but doing so + # avoids a `if clip_coef < 1:` conditional which can require a CPU <=> device synchronization + # when the gradients do not reside in CPU memory. + clip_coef_clamped = paddle.clip(clip_coef, max=1.0) + for i, p in enumerate(parameters): + # p.set_value(paddle.multiply(p, clip_coef_clamped)) + p.grad.set_value(detached_grads[i] * clip_coef_clamped) # fixed + # p.grad.detach().mul_(clip_coef_clamped + return total_norm + + +# def max(a: paddle.Tensor, axis=0, keepdim=True): +# """ndarray=numpy.array([[1, 2, 3, 4], +# [4, 3, 2, 1], +# [5, 6, 7, 8], +# [8, 7, 6, 5]]) +# np.where(ndarray == np.max(ndarray)) +# (array([2, 3]), array([3, 0])) +# ndarray[np.where(ndarray == np.max(ndarray))] +# array([8, 8]) +# """ +# max_ = a.max(axis).unsqueeze(-1) +# index = paddle.argmax(a, axis=axis, keepdim=keepdim) +# max_ = max_.numpy() +# index = index.numpy() +# # index = paddle.argmax(a, axis=axis, keepdim=keepdim)[-1].flatten() +# return max_, index + + +def gather(tmp: paddle.Tensor, ind: paddle.Tensor): + shape = tmp.shape + tmp = paddle.to_tensor(tmp) + ind = paddle.to_tensor(ind) + if len(shape) == 2: + b = shape[0] + return concat([ + reshape(paddle.gather(tmp[i, :], ind[i, :]), [1, -1]) + for i in range(b) + ], + axis=0) + elif len(shape) == 3: + out = [] + for i in range(tmp.shape[0]): + _ = paddle.index_sample(tmp[i], ind[i]) + out.append(_) + return paddle.to_tensor(out) + elif len(shape) == 4: + b, c, d = shape[:3] + return concat([ + reshape( + concat([ + reshape( + concat([ + reshape( + paddle.gather(tmp[i, j, k, :], ind[i, j, k, :]), + [1, -1]) for k in range(d) + ], + axis=0), [1, d, -1]) for j in range(c) + ], + axis=0), [1, c, d, -1]) for i in range(b) + ], + axis=0) + else: + pass + + +# These no_grad_* functions are necessary as wrappers around the parts of these +# functions that use `with torch.no_grad()`. The JIT doesn't support context +# managers, so these need to be implemented as builtins. Using these wrappers +# lets us keep those builtins small and re-usable. +def _no_grad_uniform_(tensor, a, b): + with paddle.no_grad(): + tensor.set_value(paddle.uniform(tensor.shape, min=a, max=b)) + return tensor + + +def _no_grad_normal_(tensor, mean, std): + with paddle.no_grad(): + tensor.set_value(paddle.normal(shape=tensor.shape, mean=mean, std=std)) + return tensor + + +def _no_grad_trunc_normal_(tensor, mean, std, a, b): + from scipy import special + + # Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf + def norm_cdf(x): + # Computes standard normal cumulative distribution function + return (1. + math.erf(x / math.sqrt(2.))) / 2. + + if (mean < a - 2 * std) or (mean > b + 2 * std): + warnings.warn( + "mean is more than 2 std from [a, b] in nn.init.trunc_normal_. " + "The distribution of values may be incorrect.", + stacklevel=2) + + with paddle.no_grad(): + # Values are generated by using a truncated uniform distribution and + # then using the inverse CDF for the normal distribution. + # Get upper and lower cdf values + l = norm_cdf((a - mean) / std) + u = norm_cdf((b - mean) / std) + + # Uniformly fill tensor with values from [l, u], then translate to + # [2l-1, 2u-1]. + tensor.set_value( + paddle.uniform(tensor.shape, min=2 * l - 1, max=2 * u - 1)) + # tensor.uniform_(2 * l - 1, 2 * u - 1) + + # Use inverse cdf transform for normal distribution to get truncated + # standard normal + # tensor.erfinv_() # paddle 无 + tensor.set_value(special.erfinv(tensor)) + + # Transform to proper mean, std + # tensor.mul_(std * math.sqrt(2.)) + tensor.set_value(tensor.multiply(paddle.to_tensor(std * math.sqrt(2.)))) + tensor.add_(mean) + + # Clamp to ensure it's in the proper range + tensor.clip_(min=a, max=b) + return tensor + + +def _no_grad_fill_(tensor, val): + with paddle.no_grad(): + tensor.set_value(paddle.full_like(tensor, fill_value=val)) + return tensor + + +def _no_grad_zero_(tensor): + with paddle.no_grad(): + tensor.set_value(paddle.zeros_like(tensor)) + return tensor + + +def calculate_gain(nonlinearity, param=None): + r"""Return the recommended gain value for the given nonlinearity function. + The values are as follows: + + ================= ==================================================== + nonlinearity gain + ================= ==================================================== + Linear / Identity :math:`1` + Conv{1,2,3}D :math:`1` + Sigmoid :math:`1` + Tanh :math:`\frac{5}{3}` + ReLU :math:`\sqrt{2}` + Leaky Relu :math:`\sqrt{\frac{2}{1 + \text{negative\_slope}^2}}` + SELU :math:`\frac{3}{4}` + ================= ==================================================== + + Args: + nonlinearity: the non-linear function (`nn.functional` name) + param: optional parameter for the non-linear function + + Examples: + >>> gain = nn.init.calculate_gain('leaky_relu', 0.2) # leaky_relu with negative_slope=0.2 + """ + linear_fns = [ + 'linear', 'conv1d', 'conv2d', 'conv3d', 'conv_transpose1d', + 'conv_transpose2d', 'conv_transpose3d' + ] + if nonlinearity in linear_fns or nonlinearity == 'sigmoid': + return 1 + elif nonlinearity == 'tanh': + return 5.0 / 3 + elif nonlinearity == 'relu': + return math.sqrt(2.0) + elif nonlinearity == 'leaky_relu': + if param is None: + negative_slope = 0.01 + elif not isinstance(param, bool) and isinstance( + param, int) or isinstance(param, float): + # True/False are instances of int, hence check above + negative_slope = param + else: + raise ValueError( + "negative_slope {} not a valid number".format(param)) + return math.sqrt(2.0 / (1 + negative_slope**2)) + elif nonlinearity == 'selu': + return 3.0 / 4 # Value found empirically (https://github.com/pytorch/pytorch/pull/50664) + else: + raise ValueError("Unsupported nonlinearity {}".format(nonlinearity)) + + +def uniform_(tensor: Tensor, a: float = 0., b: float = 1.) -> Tensor: + r"""Fills the input Tensor with values drawn from the uniform + distribution :math:`\mathcal{U}(a, b)`. + + Args: + tensor: an n-dimensional `torch.Tensor` + a: the lower bound of the uniform distribution + b: the upper bound of the uniform distribution + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.uniform_(w) + """ + return _no_grad_uniform_(tensor, a, b) + + +def normal_(tensor: Tensor, mean: float = 0., std: float = 1.) -> Tensor: + r"""Fills the input Tensor with values drawn from the normal + distribution :math:`\mathcal{N}(\text{mean}, \text{std}^2)`. + + Args: + tensor: an n-dimensional `torch.Tensor` + mean: the mean of the normal distribution + std: the standard deviation of the normal distribution + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.normal_(w) + """ + return _no_grad_normal_(tensor, mean, std) + + +def trunc_normal_(tensor: Tensor, + mean: float = 0., + std: float = 1., + a: float = -2., + b: float = 2.) -> Tensor: + r"""Fills the input Tensor with values drawn from a truncated + normal distribution. The values are effectively drawn from the + normal distribution :math:`\mathcal{N}(\text{mean}, \text{std}^2)` + with values outside :math:`[a, b]` redrawn until they are within + the bounds. The method used for generating the random values works + best when :math:`a \leq \text{mean} \leq b`. + + Args: + tensor: an n-dimensional `torch.Tensor` + mean: the mean of the normal distribution + std: the standard deviation of the normal distribution + a: the minimum cutoff value + b: the maximum cutoff value + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.trunc_normal_(w) + """ + return _no_grad_trunc_normal_(tensor, mean, std, a, b) + + +def constant_(tensor: Tensor, val: float) -> Tensor: + r"""Fills the input Tensor with the value :math:`\text{val}`. + + Args: + tensor: an n-dimensional `torch.Tensor` + val: the value to fill the tensor with + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.constant_(w, 0.3) + """ + return _no_grad_fill_(tensor, val) + + +def ones_(tensor: Tensor) -> Tensor: + r"""Fills the input Tensor with the scalar value `1`. + + Args: + tensor: an n-dimensional `torch.Tensor` + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.ones_(w) + """ + return _no_grad_fill_(tensor, 1.) + + +def zeros_(tensor: Tensor) -> Tensor: + r"""Fills the input Tensor with the scalar value `0`. + + Args: + tensor: an n-dimensional `torch.Tensor` + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.zeros_(w) + """ + return _no_grad_zero_(tensor) + + +def eye_(tensor): + r"""Fills the 2-dimensional input `Tensor` with the identity + matrix. Preserves the identity of the inputs in `Linear` layers, where as + many inputs are preserved as possible. + + Args: + tensor: a 2-dimensional `torch.Tensor` + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.eye_(w) + """ + if tensor.ndimension() != 2: + raise ValueError("Only tensors with 2 dimensions are supported") + + with paddle.no_grad(): + tensor.set_value(paddle.eye(*tensor.shape)) + return tensor + + +def dirac_(tensor, groups=1): + r"""Fills the {3, 4, 5}-dimensional input `Tensor` with the Dirac + delta function. Preserves the identity of the inputs in `Convolutional` + layers, where as many input channels are preserved as possible. In case + of groups>1, each group of channels preserves identity + + Args: + tensor: a {3, 4, 5}-dimensional `torch.Tensor` + groups (optional): number of groups in the conv layer (default: 1) + Examples: + >>> w = torch.empty(3, 16, 5, 5) + >>> nn.init.dirac_(w) + >>> w = torch.empty(3, 24, 5, 5) + >>> nn.init.dirac_(w, 3) + """ + dimensions = tensor.ndimension() + if dimensions not in [3, 4, 5]: + raise ValueError( + "Only tensors with 3, 4, or 5 dimensions are supported") + + sizes = tensor.shape + + if sizes[0] % groups != 0: + raise ValueError('dim 0 must be divisible by groups') + + out_chans_per_grp = sizes[0] // groups + min_dim = min(out_chans_per_grp, sizes[1]) + + with paddle.no_grad(): + tensor.zero_() + + for g in range(groups): + for d in range(min_dim): + if dimensions == 3: # Temporal convolution + tensor[g * out_chans_per_grp + d, d, + tensor.shape[2] // 2] = 1 + elif dimensions == 4: # Spatial convolution + tensor[g * out_chans_per_grp + d, d, tensor.shape[2] // 2, + tensor.shape[3] // 2] = 1 + else: # Volumetric convolution + tensor[g * out_chans_per_grp + d, d, tensor.shape[2] // 2, + tensor.shape[3] // 2, tensor.shape[4] // 2] = 1 + return tensor + + +def _calculate_fan_in_and_fan_out(tensor): + dimensions = tensor.dim() + if dimensions < 2: + raise ValueError( + "Fan in and fan out can not be computed for tensor with fewer than 2 dimensions" + ) + + num_input_fmaps = tensor.shape[1] # .size(1) + num_output_fmaps = tensor.shape[0] # .size(0) + receptive_field_size = 1 + if tensor.dim() > 2: + for s in tensor.shape[2:]: + receptive_field_size *= s # fixed + fan_in = num_input_fmaps * receptive_field_size + fan_out = num_output_fmaps * receptive_field_size + + return fan_in, fan_out + + +def LongTensor(x): + return paddle.to_tensor(x, dtype='int64') + + +def IntTensor(x): + return paddle.to_tensor(x, dtype='int32') + + +def xavier_uniform_(tensor: Tensor, gain: float = 1.) -> Tensor: + r"""Fills the input `Tensor` with values according to the method + described in `Understanding the difficulty of training deep feedforward + neural networks` - Glorot, X. & Bengio, Y. (2010), using a uniform + distribution. The resulting tensor will have values sampled from + :math:`\mathcal{U}(-a, a)` where + + .. math:: + a = \text{gain} \times \sqrt{\frac{6}{\text{fan\_in} + \text{fan\_out}}} + + Also known as Glorot initialization. + + Args: + tensor: an n-dimensional `torch.Tensor` + gain: an optional scaling factor + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.xavier_uniform_(w, gain=nn.init.calculate_gain('relu')) + """ + fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor) + std = gain * math.sqrt(2.0 / float(fan_in + fan_out)) + a = math.sqrt(3.0) * std # Calculate uniform bounds from standard deviation + + return _no_grad_uniform_(tensor, -a, a) + + +def xavier_normal_(tensor: Tensor, gain: float = 1.) -> Tensor: + r"""Fills the input `Tensor` with values according to the method + described in `Understanding the difficulty of training deep feedforward + neural networks` - Glorot, X. & Bengio, Y. (2010), using a normal + distribution. The resulting tensor will have values sampled from + :math:`\mathcal{N}(0, \text{std}^2)` where + + .. math:: + \text{std} = \text{gain} \times \sqrt{\frac{2}{\text{fan\_in} + \text{fan\_out}}} + + Also known as Glorot initialization. + + Args: + tensor: an n-dimensional `torch.Tensor` + gain: an optional scaling factor + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.xavier_normal_(w) + """ + fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor) + std = gain * math.sqrt(2.0 / float(fan_in + fan_out)) + + return _no_grad_normal_(tensor, 0., std) + + +def _calculate_correct_fan(tensor, mode): + mode = mode.lower() + valid_modes = ['fan_in', 'fan_out'] + if mode not in valid_modes: + raise ValueError("Mode {} not supported, please use one of {}".format( + mode, valid_modes)) + + fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor) + return fan_in if mode == 'fan_in' else fan_out + + +def kaiming_uniform_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'): + r"""Fills the input `Tensor` with values according to the method + described in `Delving deep into rectifiers: Surpassing human-level + performance on ImageNet classification` - He, K. et al. (2015), using a + uniform distribution. The resulting tensor will have values sampled from + :math:`\mathcal{U}(-\text{bound}, \text{bound})` where + + .. math:: + \text{bound} = \text{gain} \times \sqrt{\frac{3}{\text{fan\_mode}}} + + Also known as He initialization. + + Args: + tensor: an n-dimensional `torch.Tensor` + a: the negative slope of the rectifier used after this layer (only + used with ``'leaky_relu'``) + mode: either ``'fan_in'`` (default) or ``'fan_out'``. Choosing ``'fan_in'`` + preserves the magnitude of the variance of the weights in the + forward pass. Choosing ``'fan_out'`` preserves the magnitudes in the + backwards pass. + nonlinearity: the non-linear function (`nn.functional` name), + recommended to use only with ``'relu'`` or ``'leaky_relu'`` (default). + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.kaiming_uniform_(w, mode='fan_in', nonlinearity='relu') + """ + fan = _calculate_correct_fan(tensor, mode) + gain = calculate_gain(nonlinearity, a) + std = gain / math.sqrt(fan) + bound = math.sqrt( + 3.0) * std # Calculate uniform bounds from standard deviation + with paddle.no_grad(): + tensor.set_value(paddle.uniform(tensor.shape, min=-bound, max=bound)) + return tensor + + +def kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'): + r"""Fills the input `Tensor` with values according to the method + described in `Delving deep into rectifiers: Surpassing human-level + performance on ImageNet classification` - He, K. et al. (2015), using a + normal distribution. The resulting tensor will have values sampled from + :math:`\mathcal{N}(0, \text{std}^2)` where + + .. math:: + \text{std} = \frac{\text{gain}}{\sqrt{\text{fan\_mode}}} + + Also known as He initialization. + + Args: + tensor: an n-dimensional `torch.Tensor` + a: the negative slope of the rectifier used after this layer (only + used with ``'leaky_relu'``) + mode: either ``'fan_in'`` (default) or ``'fan_out'``. Choosing ``'fan_in'`` + preserves the magnitude of the variance of the weights in the + forward pass. Choosing ``'fan_out'`` preserves the magnitudes in the + backwards pass. + nonlinearity: the non-linear function (`nn.functional` name), + recommended to use only with ``'relu'`` or ``'leaky_relu'`` (default). + + Examples: + >>> w = torch.empty(3, 5) + >>> kaiming_normal_(w, mode='fan_out', nonlinearity='relu') + """ + fan = _calculate_correct_fan(tensor, mode) + gain = calculate_gain(nonlinearity, a) + std = gain / math.sqrt(fan) + with paddle.no_grad(): + tensor.set_value(paddle.normal(shape=tensor.shape, mean=0, std=std)) + return tensor + + +def orthogonal_(tensor, gain=1): + r"""Fills the input `Tensor` with a (semi) orthogonal matrix, as + described in `Exact solutions to the nonlinear dynamics of learning in deep + linear neural networks` - Saxe, A. et al. (2013). The input tensor must have + at least 2 dimensions, and for tensors with more than 2 dimensions the + trailing dimensions are flattened. + + Args: + tensor: an n-dimensional `torch.Tensor`, where :math:`n \geq 2` + gain: optional scaling factor + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.orthogonal_(w) + """ + if tensor.ndimension() < 2: + raise ValueError("Only tensors with 2 or more dimensions are supported") + + rows = tensor.shape[0] # .size(0) + cols = tensor.numel() // rows + flattened = tensor.new(rows, cols).normal_(0, 1) + + if rows < cols: + flattened.t_() + + # Compute the qr factorization + q, r = paddle.to_tensor(np.linalg.qr(flattened.numpy())) + # q, r = torch.qr(flattened) + # Make Q uniform according to https://arxiv.org/pdf/math-ph/0609050.pdf + d = paddle.diag(r, 0) + ph = d.sign() + q *= ph + + if rows < cols: + q.t_() + + with paddle.no_grad(): + tensor.view_as(q).copy_(q) + tensor.mul_(gain) + return tensor + + +def sparse_(tensor, sparsity, std=0.01): + r"""Fills the 2D input `Tensor` as a sparse matrix, where the + non-zero elements will be drawn from the normal distribution + :math:`\mathcal{N}(0, 0.01)`, as described in `Deep learning via + Hessian-free optimization` - Martens, J. (2010). + + Args: + tensor: an n-dimensional `torch.Tensor` + sparsity: The fraction of elements in each column to be set to zero + std: the standard deviation of the normal distribution used to generate + the non-zero values + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.sparse_(w, sparsity=0.1) + """ + if tensor.ndimension() != 2: + raise ValueError("Only tensors with 2 dimensions are supported") + + rows, cols = tensor.shape + num_zeros = int(math.ceil(sparsity * rows)) + + with paddle.no_grad(): + tensor.normal_(0, std) + for col_idx in range(cols): + row_indices = paddle.randperm(rows) + zero_indices = row_indices[:num_zeros] + tensor[zero_indices, col_idx] = 0 + return tensor + + +# for backward compatibility +def _make_deprecate(meth): + new_name = meth.__name__ + old_name = new_name[:-1] + + def deprecated_init(*args, **kwargs): + warnings.warn( + "nn.init.{} is now deprecated in favor of nn.init.{}.".format( + old_name, new_name), + stacklevel=2) + return meth(*args, **kwargs) + + deprecated_init.__doc__ = r""" + {old_name}(...) + + .. warning:: + This method is now deprecated in favor of :func:`torch.nn.init.{new_name}`. + + See :func:`~torch.nn.init.{new_name}` for details.""".format( + old_name=old_name, new_name=new_name) + deprecated_init.__name__ = old_name + return deprecated_init + + +# uniform = _make_deprecate(uniform_) +# normal = _make_deprecate(normal_) +# constant = _make_deprecate(constant_) +# eye = _make_deprecate(eye_) +# dirac = _make_deprecate(dirac_) +# xavier_uniform = _make_deprecate(xavier_uniform_) +# xavier_normal = _make_deprecate(xavier_normal_) +# kaiming_uniform = _make_deprecate(kaiming_uniform_) +# kaiming_normal = _make_deprecate(kaiming_normal_) +# orthogonal = _make_deprecate(orthogonal_) +# sparse = _make_deprecate(sparse_) diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/precise_bn.py b/applications/EIVideo/EIVideo/paddlevideo/utils/precise_bn.py new file mode 100644 index 0000000000000000000000000000000000000000..7bb8de0437c664fb26793867f1030cfdac0db8c1 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/precise_bn.py @@ -0,0 +1,84 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import itertools + +from EIVideo.paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") +""" +Implement precise bn, which is useful for improving accuracy. +""" + + +@paddle.no_grad() # speed up and save CUDA memory +def do_preciseBN(model, data_loader, parallel, num_iters=200): + """ + Recompute and update the batch norm stats to make them more precise. During + training both BN stats and the weight are changing after every iteration, so + the running average can not precisely reflect the actual stats of the + current model. + In this function, the BN stats are recomputed with fixed weights, to make + the running average more precise. Specifically, it computes the true average + of per-batch mean/variance instead of the running average. + This is useful to improve validation accuracy. + Args: + model: the model whose bn stats will be recomputed + data_loader: an iterator. Produce data as input to the model + num_iters: number of iterations to compute the stats. + Return: + the model with precise mean and variance in bn layers. + """ + bn_layers_list = [ + m for m in model.sublayers() + if any((isinstance(m, bn_type) + for bn_type in (paddle.nn.BatchNorm1D, paddle.nn.BatchNorm2D, + paddle.nn.BatchNorm3D))) and m.training + ] + if len(bn_layers_list) == 0: + return + + # moving_mean=moving_mean*momentum+batch_mean*(1.−momentum) + # we set momentum=0. to get the true mean and variance during forward + momentum_actual = [bn._momentum for bn in bn_layers_list] + for bn in bn_layers_list: + bn._momentum = 0. + + running_mean = [paddle.zeros_like(bn._mean) + for bn in bn_layers_list] #pre-ignore + running_var = [paddle.zeros_like(bn._variance) for bn in bn_layers_list] + + ind = -1 + for ind, data in enumerate(itertools.islice(data_loader, num_iters)): + logger.info("doing precise BN {} / {}...".format(ind + 1, num_iters)) + if parallel: + model._layers.train_step(data) + else: + model.train_step(data) + + for i, bn in enumerate(bn_layers_list): + # Accumulates the bn stats. + running_mean[i] += (bn._mean - running_mean[i]) / (ind + 1) + running_var[i] += (bn._variance - running_var[i]) / (ind + 1) + + assert ind == num_iters - 1, ( + "update_bn_stats is meant to run for {} iterations, but the batch_sampler stops at {} iterations." + .format(num_iters, ind)) + + # Sets the precise bn stats. + for i, bn in enumerate(bn_layers_list): + bn._mean.set_value(running_mean[i]) + bn._variance.set_value(running_var[i]) + bn._momentum = momentum_actual[i] diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/profiler.py b/applications/EIVideo/EIVideo/paddlevideo/utils/profiler.py new file mode 100644 index 0000000000000000000000000000000000000000..04201aa26738baebcd3691c1cd24c39fad5ab848 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/profiler.py @@ -0,0 +1,110 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import paddle + +# A global variable to record the number of calling times for profiler +# functions. It is used to specify the tracing range of training steps. +_profiler_step_id = 0 + +# A global variable to avoid parsing from string every time. +_profiler_options = None + + +class ProfilerOptions(object): + """ + Use a string to initialize a ProfilerOptions. + The string should be in the format: "key1=value1;key2=value;key3=value3". + For example: + "profile_path=model.profile" + "batch_range=[50, 60]; profile_path=model.profile" + "batch_range=[50, 60]; tracer_option=OpDetail; profile_path=model.profile" + + ProfilerOptions supports following key-value pair: + batch_range - a integer list, e.g. [100, 110]. + state - a string, the optional values are 'CPU', 'GPU' or 'All'. + sorted_key - a string, the optional values are 'calls', 'total', + 'max', 'min' or 'ave. + tracer_option - a string, the optional values are 'Default', 'OpDetail', + 'AllOpDetail'. + profile_path - a string, the path to save the serialized profile data, + which can be used to generate a timeline. + exit_on_finished - a boolean. + """ + def __init__(self, options_str): + assert isinstance(options_str, str) + + self._options = { + 'batch_range': [10, 20], + 'state': 'All', + 'sorted_key': 'total', + 'tracer_option': 'Default', + 'profile_path': '/tmp/profile', + 'exit_on_finished': True + } + self._parse_from_string(options_str) + + def _parse_from_string(self, options_str): + for kv in options_str.replace(' ', '').split(';'): + key, value = kv.split('=') + if key == 'batch_range': + value_list = value.replace('[', '').replace(']', '').split(',') + value_list = list(map(int, value_list)) + if len(value_list) >= 2 and value_list[0] >= 0 and value_list[ + 1] > value_list[0]: + self._options[key] = value_list + elif key == 'exit_on_finished': + self._options[key] = value.lower() in ("yes", "true", "t", "1") + elif key in [ + 'state', 'sorted_key', 'tracer_option', 'profile_path' + ]: + self._options[key] = value + + def __getitem__(self, name): + if self._options.get(name, None) is None: + raise ValueError( + "ProfilerOptions does not have an option named %s." % name) + return self._options[name] + + +def add_profiler_step(options_str=None): + """ + Enable the operator-level timing using PaddlePaddle's profiler. + The profiler uses a independent variable to count the profiler steps. + One call of this function is treated as a profiler step. + + Args: + profiler_options - a string to initialize the ProfilerOptions. + Default is None, and the profiler is disabled. + """ + if options_str is None: + return + + global _profiler_step_id + global _profiler_options + + if _profiler_options is None: + _profiler_options = ProfilerOptions(options_str) + + if _profiler_step_id == _profiler_options['batch_range'][0]: + paddle.utils.profiler.start_profiler(_profiler_options['state'], + _profiler_options['tracer_option']) + elif _profiler_step_id == _profiler_options['batch_range'][1]: + paddle.utils.profiler.stop_profiler(_profiler_options['sorted_key'], + _profiler_options['profile_path']) + if _profiler_options['exit_on_finished']: + sys.exit(0) + + _profiler_step_id += 1 diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/record.py b/applications/EIVideo/EIVideo/paddlevideo/utils/record.py new file mode 100644 index 0000000000000000000000000000000000000000..812eb68083d0c347bba80912633d7055213f4b9d --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/record.py @@ -0,0 +1,157 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import OrderedDict + +import paddle + +from .logger import coloring, get_logger + +logger = get_logger("paddlevideo") + +__all__ = ['AverageMeter', 'build_record', 'log_batch', 'log_epoch'] + + +def build_record(cfg): + framework_type = cfg.get('framework', '') + record_list = [ + ("loss", AverageMeter('loss', '7.5f')), + ("lr", AverageMeter('lr', 'f', need_avg=False)), + ] + if 'Recognizer1D' in framework_type: #TODO: required specify str in framework + record_list.append(("hit_at_one", AverageMeter("hit_at_one", '.5f'))) + record_list.append(("perr", AverageMeter("perr", '.5f'))) + record_list.append(("gap", AverageMeter("gap", '.5f'))) + elif 'Recognizer' in framework_type: + record_list.append(("top1", AverageMeter("top1", '.5f'))) + record_list.append(("top5", AverageMeter("top5", '.5f'))) + elif 'FastRCNN' in framework_type: + record_list.append( + ("recall@thr=0.5", AverageMeter("recall@thr=0.5", '.5f'))) + record_list.append( + ("prec@thr=0.5", AverageMeter("prec@thr=0.5", '.5f'))) + record_list.append(("recall@top3", AverageMeter("recall@top3", '.5f'))) + record_list.append(("prec@top3", AverageMeter("prec@top3", '.5f'))) + record_list.append(("recall@top5", AverageMeter("recall@top5", '.5f'))) + record_list.append(("prec@top5", AverageMeter("prec@top5", '.5f'))) + record_list.append(("mAP@0.5IOU", AverageMeter("mAP@0.5IOU", '.5f'))) + elif 'DepthEstimator' in cfg.framework: + record_list.append(("abs_rel", AverageMeter("abs_rel", '.5f'))) + record_list.append(("sq_rel", AverageMeter("sq_rel", '.5f'))) + record_list.append(("rmse", AverageMeter("rmse", '.5f'))) + record_list.append(("rmse_log", AverageMeter("rmse_log", '.5f'))) + record_list.append(("a1", AverageMeter("a1", '.5f'))) + record_list.append(("a2", AverageMeter("a2", '.5f'))) + record_list.append(("a3", AverageMeter("a3", '.5f'))) + record_list.append(("losses_day", AverageMeter("losses_day", '.5f'))) + record_list.append( + ("losses_night", AverageMeter("losses_night", '.5f'))) + + record_list.append(("batch_time", AverageMeter('batch_cost', '.5f'))) + record_list.append(("reader_time", AverageMeter('reader_cost', '.5f'))) + record_list = OrderedDict(record_list) + return record_list + + +class AverageMeter(object): + """ + Computes and stores the average and current value + """ + def __init__(self, name='', fmt='f', need_avg=True): + self.name = name + self.fmt = fmt + self.need_avg = need_avg + self.reset() + + def reset(self): + """ reset """ + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + """ update """ + if isinstance(val, paddle.Tensor): + val = val.numpy()[0] + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + @property + def total(self): + return '{self.name}_sum: {self.sum:{self.fmt}}'.format(self=self) + + @property + def total_minute(self): + return '{self.name}_sum: {s:{self.fmt}} min'.format(s=self.sum / 60, + self=self) + + @property + def mean(self): + return '{self.name}_avg: {self.avg:{self.fmt}}'.format( + self=self) if self.need_avg else '' + + @property + def value(self): + return '{self.name}: {self.val:{self.fmt}}'.format(self=self) + + +def log_batch(metric_list, + batch_id, + epoch_id, + total_epoch, + mode, + ips, + tot_step=None, + max_iters=None): + batch_cost = str(metric_list['batch_time'].value) + ' sec,' + reader_cost = str(metric_list['reader_time'].value) + ' sec,' + + metric_values = [] + for m in metric_list: + if not (m == 'batch_time' or m == 'reader_time'): + metric_values.append(metric_list[m].value) + metric_str = ' '.join([str(v) for v in metric_values]) + if max_iters: + epoch_str = "iter:[{:>3d}/{:<3d}]".format(tot_step, max_iters) + else: + epoch_str = "epoch:[{:>3d}/{:<3d}]".format(epoch_id, total_epoch) + step_str = "{:s} step:{:<4d}".format(mode, batch_id) + + logger.info("{:s} {:s} {:s} {:s} {:s} {}".format( + coloring(epoch_str, "HEADER") if batch_id == 0 else epoch_str, + coloring(step_str, "PURPLE"), coloring(metric_str, 'OKGREEN'), + coloring(batch_cost, "OKGREEN"), coloring(reader_cost, 'OKGREEN'), + ips)) + + +def log_epoch(metric_list, epoch, mode, ips): + batch_cost = 'avg_' + str(metric_list['batch_time'].value) + ' sec,' + reader_cost = 'avg_' + str(metric_list['reader_time'].value) + ' sec,' + batch_sum = str(metric_list['batch_time'].total) + ' sec,' + + metric_values = [] + for m in metric_list: + if not (m == 'batch_time' or m == 'reader_time'): + metric_values.append(metric_list[m].mean) + metric_str = ' '.join([str(v) for v in metric_values]) + + end_epoch_str = "END epoch:{:<3d}".format(epoch) + + logger.info("{:s} {:s} {:s} {:s} {:s} {:s} {}".format( + coloring(end_epoch_str, "RED"), coloring(mode, "PURPLE"), + coloring(metric_str, "OKGREEN"), coloring(batch_cost, "OKGREEN"), + coloring(reader_cost, "OKGREEN"), coloring(batch_sum, "OKGREEN"), ips)) diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/registry.py b/applications/EIVideo/EIVideo/paddlevideo/utils/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..81b76bd51f55013cb45f2b923c3e518bfb218d53 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/registry.py @@ -0,0 +1,96 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Registry(object): + """ + The registry that provides name -> object mapping, to support third-party users' custom modules. + + To register an object: + + .. code-block:: python + + BACKBONES = Registry('backbone') + @BACKBONES.register() + class ResNet: + pass + Or: + .. code-block:: python + + BACKBONES = Registry('backbone') + class ResNet: + pass + BACKBONES.register(ResNet) + + Usage: To build a module. + + .. code-block:: python + backbone_name = "ResNet" + b = BACKBONES.get(backbone_name)() + + """ + def __init__(self, name): + """ + Args: + name (str): the name of this registry + """ + self._name = name + self._obj_map = {} + + def __contains__(self, key): + return self._obj_map.get(key) is not None + + def _do_register(self, name, obj): + assert ( + name not in self._obj_map + ), "An object named '{}' was already registered in '{}' registry!".format( + name, self._name) + self._obj_map[name] = obj + + def register(self, obj=None, name=None): + """ + Register the given object under the the name `obj.__name__`. + Can be used as either a decorator or not. See docstring of this class for usage. + """ + if obj is None: + # used as a decorator + def deco(func_or_class, name=name): + if name is None: + name = func_or_class.__name__ + self._do_register(name, func_or_class) + return func_or_class + + return deco + + # used as a function call + if name is None: + name = obj.__name__ + self._do_register(name, obj) + + def get(self, name): + """Get the registry record. + + Args: + name (str): The class name. + + Returns: + ret: The class. + """ + ret = self._obj_map.get(name) + if ret is None: + raise KeyError( + "No object named '{}' found in '{}' registry!".format( + name, self._name)) + + return ret diff --git a/applications/EIVideo/EIVideo/paddlevideo/utils/save_load.py b/applications/EIVideo/EIVideo/paddlevideo/utils/save_load.py new file mode 100644 index 0000000000000000000000000000000000000000..4624156204c568598de9097cb83896171180412d --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/utils/save_load.py @@ -0,0 +1,215 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import os.path as osp +import time + +from tqdm import tqdm +import paddle +import paddle.nn.functional as F +from EIVideo.paddlevideo.utils import get_logger +from EIVideo.paddlevideo.utils import main_only + + +def pretrain_vit_param_trans(model, state_dicts, num_patches, seg_num, + attention_type): + """ + Convert ViT's pre-trained model parameters to a parameter dictionary that matches the existing model + """ + if 'head' + '.weight' in state_dicts: + del state_dicts['head' + '.weight'] + if 'head' + '.bias' in state_dicts: + del state_dicts['head' + '.bias'] + + total_len = len(model.state_dict()) + if num_patches + 1 != state_dicts['pos_embed'].shape[1]: + pos_embed = state_dicts['pos_embed'] + cls_pos_embed = pos_embed[0, 0, :].unsqueeze(0).unsqueeze(1) + other_pos_embed = pos_embed[0, + 1:, :].unsqueeze(0).unsqueeze(1).transpose( + (0, 1, 3, 2)) + new_pos_embed = F.interpolate(other_pos_embed, + size=(other_pos_embed.shape[-2], + num_patches), + mode='nearest') + new_pos_embed = new_pos_embed.squeeze(0).transpose((0, 2, 1)) + new_pos_embed = paddle.concat((cls_pos_embed, new_pos_embed), axis=1) + state_dicts['pos_embed'] = new_pos_embed + time.sleep(0.01) + + if 'time_embed' in state_dicts and seg_num != state_dicts[ + 'time_embed'].shape[1]: + time_embed = state_dicts['time_embed'].transpose( + (0, 2, 1)).unsqueeze(0) + new_time_embed = F.interpolate(time_embed, + size=(time_embed.shape[-2], seg_num), + mode='nearest') + state_dicts['time_embed'] = new_time_embed.squeeze(0).transpose( + (0, 2, 1)) + time.sleep(0.01) + with tqdm(total=total_len, + position=1, + bar_format='{desc}', + desc="Loading weights") as desc: + if attention_type == 'divided_space_time': + new_state_dicts = state_dicts.copy() + for key in tqdm(state_dicts): + if 'blocks' in key and 'attn' in key: + desc.set_description("Loading %s" % key) + new_key = key.replace('attn', 'temporal_attn') + if not new_key in state_dicts: + new_state_dicts[new_key] = state_dicts[key] + else: + new_state_dicts[new_key] = state_dicts[new_key] + if 'blocks' in key and 'norm1' in key: + desc.set_description("Loading %s" % key) + new_key = key.replace('norm1', 'temporal_norm1') + if not new_key in state_dicts: + new_state_dicts[new_key] = state_dicts[key] + else: + new_state_dicts[new_key] = state_dicts[new_key] + time.sleep(0.01) + ret_str = "loading {:<20d} weights completed.".format( + len(model.state_dict())) + desc.set_description(ret_str) + return new_state_dicts + + +def pretrain_resnet18_param_trans(model, loaded_dict): + encoder_dict = model.encoder.state_dict() + pose_encoder_dict = model.pose_encoder.state_dict() + + names = ['encoder.', 'encoder_day.', 'encoder_night.'] + for name in names: + for key, value in loaded_dict.items(): + key = str(name + key) + if key in encoder_dict: + encoder_dict[key] = value + + num_input_images = 2 + loaded_dict['conv1.weight'] = paddle.concat( + [loaded_dict['conv1.weight']] * num_input_images, 1) / num_input_images + + for name, value in loaded_dict.items(): + name = str('encoder.' + name) + if name in pose_encoder_dict: + pose_encoder_dict[name] = value + + return encoder_dict, pose_encoder_dict + + +#XXX(shipping): maybe need load N times because of different cards have different params. +@main_only +def load_ckpt(model, weight_path, **kargs): + """ + 1. Load pre-trained model parameters + 2. Extract and convert from the pre-trained model to the parameters + required by the existing model + 3. Load the converted parameters of the existing model + """ + #model.set_state_dict(state_dict) + + if not osp.isfile(weight_path): + raise IOError(f'{weight_path} is not a checkpoint file') + #state_dicts = load(weight_path) + + logger = get_logger("paddlevideo") + state_dicts = paddle.load(weight_path) + if 'ResnetEncoder' in str(model): + encoder_dict, pose_encoder_dict = pretrain_resnet18_param_trans( + model, state_dicts) + tmp = model.state_dict() + tmp.update( + {'backbone.encoder.' + k: v + for (k, v) in encoder_dict.items()}) + tmp.update({ + 'backbone.pose_encoder.' + k: v + for (k, v) in pose_encoder_dict.items() + }) + elif "VisionTransformer" in str(model): # For TimeSformer case + tmp = pretrain_vit_param_trans(model, state_dicts, + kargs['num_patches'], kargs['seg_num'], + kargs['attention_type']) + else: + tmp = {} + total_len = len(model.state_dict()) + with tqdm(total=total_len, + position=1, + bar_format='{desc}', + desc="Loading weights") as desc: + for item in tqdm(model.state_dict(), total=total_len, position=0): + name = item + desc.set_description('Loading %s' % name) + if name not in state_dicts: # Convert from non-parallel model + if str('backbone.' + name) in state_dicts: + tmp[name] = state_dicts['backbone.' + name] + else: # Convert from parallel model + tmp[name] = state_dicts[name] + time.sleep(0.01) + ret_str = "loading {:<20d} weights completed.".format( + len(model.state_dict())) + desc.set_description(ret_str) + model.set_state_dict(tmp) + + +def mkdir(dir): + if not os.path.exists(dir): + # avoid error when train with multiple gpus + try: + os.makedirs(dir) + except: + pass + + +""" +def save(state_dicts, file_name): + def convert(state_dict): + model_dict = {} + + for k, v in state_dict.items(): + if isinstance( + v, + (paddle.fluid.framework.Variable, paddle.fluid.core.VarBase)): + model_dict[k] = v.numpy() + else: + model_dict[k] = v + + return model_dict + + final_dict = {} + for k, v in state_dicts.items(): + if isinstance( + v, + (paddle.fluid.framework.Variable, paddle.fluid.core.VarBase)): + final_dict = convert(state_dicts) + break + elif isinstance(v, dict): + final_dict[k] = convert(v) + else: + final_dict[k] = v + + with open(file_name, 'wb') as f: + pickle.dump(final_dict, f, protocol=2) +""" + + +@main_only +def save(obj, path): + paddle.save(obj, path) + + +def load(file_name): + if not osp.isfile(file_name): + raise IOError(f'{file_name} not exist') + return paddle.load(file_name) diff --git a/applications/EIVideo/EIVideo/paddlevideo/version.py b/applications/EIVideo/EIVideo/paddlevideo/version.py new file mode 100644 index 0000000000000000000000000000000000000000..b5b7f481f4afac92604a6b9f036eb93510069556 --- /dev/null +++ b/applications/EIVideo/EIVideo/paddlevideo/version.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ["paddlevideo_version"] +paddlevideo_version = "0.0.1" diff --git a/applications/EIVideo/EIVideo/setup.py b/applications/EIVideo/EIVideo/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..7174baeebe253b1b3caaddebde7e8309975a26b2 --- /dev/null +++ b/applications/EIVideo/EIVideo/setup.py @@ -0,0 +1,4 @@ +# Author: Acer Zhang +# Datetime: 2022/1/11 +# Copyright belongs to the author. +# Please indicate the source for reprinting. diff --git a/applications/EIVideo/EIVideo/version.py b/applications/EIVideo/EIVideo/version.py new file mode 100644 index 0000000000000000000000000000000000000000..1d2c585840916c47f03b53d038f134cb5a420362 --- /dev/null +++ b/applications/EIVideo/EIVideo/version.py @@ -0,0 +1,6 @@ +# Author: Acer Zhang +# Datetime: 2022/1/11 +# Copyright belongs to the author. +# Please indicate the source for reprinting. + +__version__ = "0.1a" diff --git a/applications/EIVideo/LICENSE b/applications/EIVideo/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..8000a6faacf471c537530805ab29523c7732e11a --- /dev/null +++ b/applications/EIVideo/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/applications/EIVideo/QEIVideo/__init__.py b/applications/EIVideo/QEIVideo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..53e6390b0423d6bac0b31be26f15af38b7c66343 --- /dev/null +++ b/applications/EIVideo/QEIVideo/__init__.py @@ -0,0 +1,13 @@ +# Author: Acer Zhang +# Datetime: 2022/1/6 +# Copyright belongs to the author. +# Please indicate the source for reprinting. + +import os + +QEI_VIDEO_ROOT = os.path.abspath(os.path.dirname(__file__)) + +import os +from QEIVideo.version import __version__ + +QEI_VIDEO_ROOT = os.path.abspath(os.path.dirname(__file__)) diff --git a/applications/EIVideo/QEIVideo/build_gui.py b/applications/EIVideo/QEIVideo/build_gui.py new file mode 100644 index 0000000000000000000000000000000000000000..477505acbfdad374afa854cdb71706bf5ff2d733 --- /dev/null +++ b/applications/EIVideo/QEIVideo/build_gui.py @@ -0,0 +1,151 @@ +# Author: Acer Zhang +# Datetime:2022/1/11 +# Copyright belongs to the author. +# Please indicate the source for reprinting. +import json +import os + +import numpy as np +from PIL import Image + +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * +import cv2 + +from EIVideo.api import json2frame, png2json, load_video +from EIVideo.main import main +# ToDo To AP-kai: 这是定义前端临时保存用于推理的json的地点之类的,因为是固定的,所以声明为全局常量是最好的 +from EIVideo import TEMP_JSON_SAVE_PATH, TEMP_IMG_SAVE_PATH, TEMP_JSON_FINAL_PATH + +from QEIVideo.gui.ui_main_window import Ui_MainWindow + + +class BuildGUI(QMainWindow, Ui_MainWindow): + def __init__(self): + super(BuildGUI, self).__init__() + # ToDo To AP-kai: 这里定义当前选择的视频路径的占位符,相当于全局变量 + self.select_video_path = None + # ToDo To AP-kai: 未来为用户提供个保存路径的入口哈,这里先随意定义了个路径 + self.save_path = "./result" + os.makedirs(self.save_path, exist_ok=True) + self.setupUi(self) + + def infer(self): + self.label.setText("Start infer") + self.progressBar.setProperty("value", 0) + image = self.paintBoard.get_content_as_q_image() + image.save(TEMP_IMG_SAVE_PATH) + print(self.slider_frame_num) + self.progressBar.setProperty("value", 25) + # ToDo To AP-kai:相同的文件路径,直接定义一个常量就好 + png2json(TEMP_IMG_SAVE_PATH, self.slider_frame_num, TEMP_JSON_SAVE_PATH) + self.progressBar.setProperty("value", 50) + # ToDo To AP-kai:打印的信息,需要注意首字母大写 + # ToDo To AP-kai: 此处传入保存路径以及当前选择的视频路径,最后会在manet_stage1.py里通过cfg来传入 + out = main(video_path=self.select_video_path, save_path=self.save_path) + print('Infer ok') + self.progressBar.setProperty("value", 75) + self.all_frames = json2frame(TEMP_JSON_FINAL_PATH) + print("Success get submit_masks") + self.open_frame() + self.progressBar.setProperty("value", 100) + self.label.setText("Infer succeed") + + def btn_func(self, btn): + if btn == self.playbtn: + self.label.setText("Play video") + if self.progress_slider.value() == self.cap.get(7) - 1: + self.slider_frame_num = 0 + self.progress_slider.setValue(self.slider_frame_num) + self.time_label.setText('{}/{}'.format(self.slider_frame_num, self.cap.get(7))) + self.timer_camera = QTimer() # 定义定时器 + self.timer_camera.start(1000 / self.cap.get(cv2.CAP_PROP_FPS)) + self.slider_frame_num = self.progress_slider.value() + self.timer_camera.timeout.connect(self.open_frame) + + elif btn == self.pushButton_2: + self.label.setText("Stop video") + self.slot_stop() + + elif btn == self.pushButton_4: + self.label.setText("Choose video") + self.select_video_path, _ = QFileDialog.getOpenFileName(self.frame, "Open", "", "*.mp4;;All Files(*)") + print("-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-") + print("Select video file path:\t" + self.select_video_path) + # ToDo To AP-kai:下断点来看一下,如果不选择的时候返回值是什么样的,然后再做判断,目前这个if没有生效 + if self.select_video_path != "": + self.cap = cv2.VideoCapture(self.select_video_path) + # 存所有frame + self.save_temp_frame() + print("save temp frame done") + self.progress_slider.setRange(0, self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) + self.slider_frame_num = 0 + self.open_frame() + + # ToDo To AP-kai: 未来这个地方增加提示框,告诉他没有选择文件 + + def on_cbtn_eraser_clicked(self): + self.label.setText("Eraser On") + if self.cbtn_Eraser.isChecked(): + self.paintBoard.EraserMode = True # 进入橡皮擦模式 + else: + self.paintBoard.EraserMode = False # 退出橡皮擦模式 + + def fill_color_list(self, combo_box): + index_black = 0 + index = 0 + for color in self.colorList: + if color == "black": + index_black = index + index += 1 + pix = QPixmap(70, 20) + pix.fill(QColor(color)) + combo_box.addItem(QIcon(pix), None) + combo_box.setIconSize(QSize(70, 20)) + combo_box.setSizeAdjustPolicy(QComboBox.AdjustToContents) + + combo_box.setCurrentIndex(index_black) + + def on_pen_color_change(self): + self.label.setText("Change pen color") + color_index = self.comboBox_penColor.currentIndex() + color_str = self.colorList[color_index] + + self.paintBoard.change_pen_color(color_str) + + # 拖拽进度条 + def update_video_position_func(self): + self.label.setText("Change slider position") + self.slider_frame_num = self.progress_slider.value() + self.slot_stop() + self.open_frame() + self.progress_slider.setValue(self.slider_frame_num) + self.time_label.setText('{}/{}'.format(self.slider_frame_num, self.cap.get(7))) + + def save_temp_frame(self): + _, self.all_frames = load_video(self.select_video_path, 480) + + def slot_stop(self): + if self.cap != []: + self.timer_camera.stop() # 停止计时器 + else: + # ToDo To AP-kai: QMessageBox.warning没有返回值,这里我把Warming = QMessageBox.warning的Warming删去了 + QMessageBox.warning(self, "Warming", "Push the left upper corner button to Quit.", + QMessageBox.Yes) + + def open_frame(self): + self.progress_slider.setValue(self.slider_frame_num) + self.slider_frame_num = self.progress_slider.value() + self.frame = self.all_frames[self.slider_frame_num] + frame = self.frame + height, width, bytes_per_component = frame.shape + bytes_per_line = bytes_per_component * width + q_image = QImage(frame.data, width, height, bytes_per_line, + QImage.Format_RGB888).scaled(self.picturelabel.width(), self.picturelabel.height()) + self.picturelabel.setPixmap(QPixmap.fromImage(q_image)) + self.slider_frame_num = self.slider_frame_num + 1 + self.time_label.setText('{}/{}'.format(self.slider_frame_num, self.cap.get(7))) + if self.progress_slider.value() == self.cap.get(7) - 1: + self.slot_stop() diff --git a/applications/EIVideo/QEIVideo/gui/__init__.py b/applications/EIVideo/QEIVideo/gui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1b0d211a2c07bc820b638295d8a4fa371d1959a0 --- /dev/null +++ b/applications/EIVideo/QEIVideo/gui/__init__.py @@ -0,0 +1,4 @@ +# Author: Acer Zhang +# Datetime: 2022/1/6 +# Copyright belongs to the author. +# Please indicate the source for reprinting. diff --git a/applications/EIVideo/QEIVideo/gui/demo.py b/applications/EIVideo/QEIVideo/gui/demo.py new file mode 100644 index 0000000000000000000000000000000000000000..b9573fafec08c6d9d17f2e3fa4f44659e64835f5 --- /dev/null +++ b/applications/EIVideo/QEIVideo/gui/demo.py @@ -0,0 +1,62 @@ +# Author: Acer Zhang +# Datetime: 2022/1/6 +# Copyright belongs to the author. +# Please indicate the source for reprinting. +import sys + +from PyQt5.QtWidgets import QApplication, QMainWindow, QFrame, QWidget +from PyQt5.QtGui import QPainter, QPixmap, QPen, QColor, QPainterPath +from PyQt5.QtCore import Qt, QPoint +from PyQt5 import QtCore, QtGui, QtWidgets + +from QEIVideo.ui.demo import Ui_MainWindow as DemoUIRoot + + +class DrawFrame(QWidget): + def __init__(self, painter, *args, **kwargs): + super(DrawFrame, self).__init__(*args, **kwargs) + self.painter = painter + + def paintEvent(self, event): + painter = QPainter(self) + pen = QPen(QColor("orange")) + pen.setWidth(5) + pen.setCapStyle(Qt.RoundCap) + pen.setJoinStyle(Qt.RoundJoin) + painter.setPen(pen) + painter.drawPath(self.painter) + + def mousePressEvent(self, event): + self.painter.moveTo(event.pos()) + self.update() + + def mouseMoveEvent(self, event): + self.painter.lineTo(event.pos()) + + self.update() + + +class DemoUI(QMainWindow, DemoUIRoot): + def __init__(self): + super(DemoUI, self).__init__() + self.setupUi(self) + + self.painter = QPainterPath() + self.draw_frame = DrawFrame(self.painter, self.video_frame) + self.draw_frame.setGeometry(QtCore.QRect(0, 10, 751, 301)) + self.draw_frame.setObjectName("draw_frame") + self.draw_frame.raise_() + self.draw_frame.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.start_btn.clicked.connect(self.export) + + def export(self): + a = self.painter.toFillPolygon() + pass + + +if __name__ == '__main__': + app = QApplication(sys.argv) + gui_class = DemoUI() + gui_class.show() + sys.exit(app.exec_()) diff --git a/applications/EIVideo/QEIVideo/gui/ui_main_window.py b/applications/EIVideo/QEIVideo/gui/ui_main_window.py new file mode 100644 index 0000000000000000000000000000000000000000..5c9627eb626e6e7298c1841008141d8acc6718ef --- /dev/null +++ b/applications/EIVideo/QEIVideo/gui/ui_main_window.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'GUI.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * +from QEIVideo.widget.PaintBoard import PaintBoard + + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("EIVideo") + MainWindow.resize(1101, 751) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.frame = QtWidgets.QFrame(self.centralwidget) + self.frame.setGeometry(QtCore.QRect(20, 20, 1271, 771)) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + + self.cap = [] + self.all_frames = [] + + self.fps = None + self.timer = QTimer(self.frame) + self.time_label = QLabel('--/--', self.frame) + + self.progress_slider = QSlider(self.frame) + self.progress_slider.setEnabled(True) + self.progress_slider.setOrientation(Qt.Horizontal) + self.progress_slider.setFixedWidth(710) + self.progress_slider.setFixedHeight(20) + self.progress_slider.setSingleStep(1) # 设置变化步长 + self.progress_slider.setValue(0) + self.progress_slider.sliderReleased.connect(self.update_video_position_func) # 拖拽进度条 + + self.picturelabel = QtWidgets.QLabel(self.frame) + self.picturelabel.setGeometry(30, 30, 810, 458) + self.picturelabel.setText("") + self.picturelabel.setObjectName("picturelabel") + + self.paintBoard = PaintBoard(self.frame) + self.paintBoard.setGeometry(30, 30, 810, 458) + + self.cbtn_Eraser = QCheckBox("橡皮擦") + self.cbtn_Eraser.setParent(self.frame) + self.cbtn_Eraser.move(950, 40) + self.cbtn_Eraser.clicked.connect(self.on_cbtn_eraser_clicked) + self.btn_Clear = QPushButton("清空画板") + self.btn_Clear.setParent(self.frame) # 设置父对象为本界面 + self.btn_Clear.move(950, 60) + self.btn_Clear.clicked.connect(self.paintBoard.clear) + self.label_penColor = QLabel(self.frame) + self.label_penColor.setText("画笔颜色") + self.label_penColor.move(990, 100) + # 获取颜色列表(字符串类型) + self.colorList = QColor.colorNames() + self.comboBox_penColor = QComboBox(self.frame) + self.fill_color_list(self.comboBox_penColor) # 用各种颜色填充下拉列表 + self.comboBox_penColor.move(1080, 80) + self.comboBox_penColor.currentIndexChanged.connect( + self.on_pen_color_change) # 关联下拉列表的当前索引变更信号与函数on_PenColorChange + + self.helplabel = QLabel() + self.helplabel.setText("Hi,Welcome to use EIVideo\n" + "This is a guide for EIVideo,\n" + "please check\n" + "1. Choose 'Add' for a video\n" + "2. Click 'Play' to start playing\n" + "3. At this point, all functions \n" + "are unlocked\n" + "4. Paint and enjoy it!\n") + + self.widget2 = QtWidgets.QWidget(self.frame) + self.widget2.setGeometry(860, 60, 200, 300) + self.widget2.setObjectName("widget2") + self.rightLayout = QtWidgets.QVBoxLayout(self.widget2) + self.rightLayout.setContentsMargins(0, 0, 0, 0) + self.rightLayout.setObjectName("rightLayout") + self.rightLayout.addWidget(self.helplabel) + self.rightLayout.addSpacing(50) + self.rightLayout.addWidget(self.cbtn_Eraser) + self.rightLayout.addWidget(self.btn_Clear) + self.colorLayout = QtWidgets.QHBoxLayout(self.widget2) + self.colorLayout.setContentsMargins(0, 0, 0, 0) + self.colorLayout.setObjectName('colorLayout') + self.colorLayout.addWidget(self.label_penColor) + self.colorLayout.addWidget(self.comboBox_penColor) + self.rightLayout.addLayout(self.colorLayout) + + + + # pushButton_6 -> GO + self.pushButton_6 = QtWidgets.QPushButton(self.frame) + self.pushButton_6.setGeometry(870, 600, 150, 90) + self.pushButton_6.setObjectName("pushButton_6") + self.pushButton_6.clicked.connect(self.infer) + + self.widget1 = QtWidgets.QWidget(self.frame) + self.widget1.move(60, 520) + self.widget1.setObjectName("widget1") + self.barLayout = QtWidgets.QVBoxLayout(self.widget1) + self.barLayout.setContentsMargins(0, 0, 0, 0) + self.barLayout.setObjectName("barLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget1) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.timeLayout = QtWidgets.QHBoxLayout(self.widget1) + self.timeLayout.setContentsMargins(0, 0, 0, 0) + self.timeLayout.setObjectName("horizontalLayout") + + self.playbtn = QtWidgets.QPushButton(self.widget1) + self.playbtn.setObjectName("playbtn") + self.playbtn.clicked.connect(lambda: self.btn_func(self.playbtn)) + self.horizontalLayout.addWidget(self.playbtn) + self.pushButton_2 = QtWidgets.QPushButton(self.widget1) + self.pushButton_2.setObjectName("pushButton_2") + self.pushButton_2.clicked.connect(lambda: self.btn_func(self.pushButton_2)) + self.horizontalLayout.addWidget(self.pushButton_2) + self.pushButton_4 = QtWidgets.QPushButton(self.widget1) + self.pushButton_4.setObjectName("pushButton_4") + self.pushButton_4.clicked.connect(lambda: self.btn_func(self.pushButton_4)) + self.horizontalLayout.addWidget(self.pushButton_4) + + self.timeLayout.addWidget(self.progress_slider) + self.timeLayout.addWidget(self.time_label) + self.barLayout.addSpacing(20) + self.barLayout.addLayout(self.timeLayout) + self.barLayout.addSpacing(30) + self.barLayout.addLayout(self.horizontalLayout) + + self.splitter = QtWidgets.QSplitter(self.frame) + self.splitter.setGeometry(QtCore.QRect(71, 670, 750, 20)) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.label = QtWidgets.QLabel(self.splitter) + self.label.setObjectName("label") + self.progressBar = QtWidgets.QProgressBar(self.splitter) + self.progressBar.setProperty("value", 0) + self.progressBar.setObjectName("progressBar") + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1327, 23)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.pushButton_6.setText(_translate("MainWindow", "GO")) + self.playbtn.setText(_translate("MainWindow", "Play")) + self.pushButton_2.setText(_translate("MainWindow", "Stop")) + self.pushButton_4.setText(_translate("MainWindow", "Add")) + self.label.setText(_translate("MainWindow", "Hi, This is EIVideo")) + + diff --git a/applications/EIVideo/QEIVideo/start.py b/applications/EIVideo/QEIVideo/start.py new file mode 100644 index 0000000000000000000000000000000000000000..fe8d3785a64b05b35d5c75faa72a786614dde48a --- /dev/null +++ b/applications/EIVideo/QEIVideo/start.py @@ -0,0 +1,20 @@ +# Author: AP-Kai +# Datetime: 2022/1/7 +# Copyright belongs to the author. +# Please indicate the source for reprinting. + + +import sys +from QEIVideo.build_gui import BuildGUI +from PyQt5.QtWidgets import QApplication + + +def run(): + app = QApplication(sys.argv) + demo = BuildGUI() + demo.show() + sys.exit(app.exec()) + + +if __name__ == '__main__': + run() diff --git a/applications/EIVideo/QEIVideo/tools/__init__.py b/applications/EIVideo/QEIVideo/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1b0d211a2c07bc820b638295d8a4fa371d1959a0 --- /dev/null +++ b/applications/EIVideo/QEIVideo/tools/__init__.py @@ -0,0 +1,4 @@ +# Author: Acer Zhang +# Datetime: 2022/1/6 +# Copyright belongs to the author. +# Please indicate the source for reprinting. diff --git a/applications/EIVideo/QEIVideo/ui/__init__.py b/applications/EIVideo/QEIVideo/ui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1b0d211a2c07bc820b638295d8a4fa371d1959a0 --- /dev/null +++ b/applications/EIVideo/QEIVideo/ui/__init__.py @@ -0,0 +1,4 @@ +# Author: Acer Zhang +# Datetime: 2022/1/6 +# Copyright belongs to the author. +# Please indicate the source for reprinting. diff --git a/applications/EIVideo/QEIVideo/ui/demo.py b/applications/EIVideo/QEIVideo/ui/demo.py new file mode 100644 index 0000000000000000000000000000000000000000..2985ec2ce43cf2460c68e1d48836b07bbd352088 --- /dev/null +++ b/applications/EIVideo/QEIVideo/ui/demo.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '/Users/zhanghongji/PycharmProjects/EIVideo/resources/QT/demo.ui' +# +# Created by: PyQt5 UI code generator 5.15.6 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(800, 486) + MainWindow.setMinimumSize(QtCore.QSize(800, 486)) + MainWindow.setMaximumSize(QtCore.QSize(800, 486)) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.video_frame = QtWidgets.QFrame(self.centralwidget) + self.video_frame.setGeometry(QtCore.QRect(20, 20, 761, 361)) + self.video_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.video_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.video_frame.setObjectName("video_frame") + self.graphicsView = QtWidgets.QGraphicsView(self.video_frame) + self.graphicsView.setGeometry(QtCore.QRect(0, 0, 761, 321)) + self.graphicsView.setObjectName("graphicsView") + self.frame_2 = QtWidgets.QFrame(self.video_frame) + self.frame_2.setGeometry(QtCore.QRect(0, 320, 761, 41)) + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.horizontalLayoutWidget = QtWidgets.QWidget(self.frame_2) + self.horizontalLayoutWidget.setGeometry(QtCore.QRect(-1, -1, 761, 41)) + self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.open_btn = QtWidgets.QPushButton(self.horizontalLayoutWidget) + self.open_btn.setObjectName("open_btn") + self.horizontalLayout.addWidget(self.open_btn) + self.save_btn = QtWidgets.QPushButton(self.horizontalLayoutWidget) + self.save_btn.setObjectName("save_btn") + self.horizontalLayout.addWidget(self.save_btn) + self.horizontalSlider = QtWidgets.QSlider(self.horizontalLayoutWidget) + self.horizontalSlider.setOrientation(QtCore.Qt.Horizontal) + self.horizontalSlider.setObjectName("horizontalSlider") + self.horizontalLayout.addWidget(self.horizontalSlider) + self.select_btn = QtWidgets.QPushButton(self.horizontalLayoutWidget) + self.select_btn.setObjectName("select_btn") + self.horizontalLayout.addWidget(self.select_btn) + self.clean_btn = QtWidgets.QPushButton(self.horizontalLayoutWidget) + self.clean_btn.setObjectName("clean_btn") + self.horizontalLayout.addWidget(self.clean_btn) + self.start_btn = QtWidgets.QPushButton(self.horizontalLayoutWidget) + self.start_btn.setObjectName("start_btn") + self.horizontalLayout.addWidget(self.start_btn) + self.draw_frame = QtWidgets.QFrame(self.video_frame) + self.draw_frame.setGeometry(QtCore.QRect(0, 10, 751, 301)) + self.draw_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.draw_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.draw_frame.setObjectName("draw_frame") + self.menu_tab = QtWidgets.QTabWidget(self.centralwidget) + self.menu_tab.setGeometry(QtCore.QRect(20, 380, 761, 81)) + self.menu_tab.setObjectName("menu_tab") + self.tab = QtWidgets.QWidget() + self.tab.setObjectName("tab") + self.act_label = QtWidgets.QLabel(self.tab) + self.act_label.setEnabled(True) + self.act_label.setGeometry(QtCore.QRect(10, 30, 71, 21)) + self.act_label.setObjectName("act_label") + self.act_info_label = QtWidgets.QLabel(self.tab) + self.act_info_label.setEnabled(True) + self.act_info_label.setGeometry(QtCore.QRect(80, 30, 81, 21)) + self.act_info_label.setObjectName("act_info_label") + self.act_progressbar = QtWidgets.QProgressBar(self.tab) + self.act_progressbar.setGeometry(QtCore.QRect(170, 32, 521, 21)) + self.act_progressbar.setProperty("value", 24) + self.act_progressbar.setObjectName("act_progressbar") + self.label_3 = QtWidgets.QLabel(self.tab) + self.label_3.setEnabled(True) + self.label_3.setGeometry(QtCore.QRect(680, 30, 60, 21)) + self.label_3.setLayoutDirection(QtCore.Qt.LeftToRight) + self.label_3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_3.setObjectName("label_3") + self.menu_tab.addTab(self.tab, "") + self.tab_2 = QtWidgets.QWidget() + self.tab_2.setObjectName("tab_2") + self.menu_tab.addTab(self.tab_2, "") + MainWindow.setCentralWidget(self.centralwidget) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + self.menu_tab.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.open_btn.setText(_translate("MainWindow", "打开视频")) + self.save_btn.setText(_translate("MainWindow", "保存标注")) + self.select_btn.setText(_translate("MainWindow", "选择目标")) + self.clean_btn.setText(_translate("MainWindow", "清空目标")) + self.start_btn.setText(_translate("MainWindow", "开始推理")) + self.act_label.setText(_translate("MainWindow", "当前状态:")) + self.act_info_label.setText(_translate("MainWindow", "-------------")) + self.label_3.setText(_translate("MainWindow", "12%")) + self.menu_tab.setTabText(self.menu_tab.indexOf(self.tab), _translate("MainWindow", "状态")) + self.menu_tab.setTabText(self.menu_tab.indexOf(self.tab_2), _translate("MainWindow", "属性配置")) diff --git a/applications/EIVideo/QEIVideo/version.py b/applications/EIVideo/QEIVideo/version.py new file mode 100644 index 0000000000000000000000000000000000000000..1d2c585840916c47f03b53d038f134cb5a420362 --- /dev/null +++ b/applications/EIVideo/QEIVideo/version.py @@ -0,0 +1,6 @@ +# Author: Acer Zhang +# Datetime: 2022/1/11 +# Copyright belongs to the author. +# Please indicate the source for reprinting. + +__version__ = "0.1a" diff --git a/applications/EIVideo/QEIVideo/widget/PaintBoard.py b/applications/EIVideo/QEIVideo/widget/PaintBoard.py new file mode 100644 index 0000000000000000000000000000000000000000..020d6a0d98562f5037087dbf8c8d57319ba27bb9 --- /dev/null +++ b/applications/EIVideo/QEIVideo/widget/PaintBoard.py @@ -0,0 +1,106 @@ +from PyQt5.QtWidgets import QWidget +from PyQt5.Qt import QPixmap, QPainter, QPoint, QPaintEvent, QMouseEvent, QPen, \ + QColor, QSize +from PyQt5.QtCore import Qt + + +class PaintBoard(QWidget): + + def __init__(self, parent=None): + ''' + Constructor + ''' + super().__init__(parent) + + self.__init_data() # 先初始化数据,再初始化界面 + self.__init_view() + + def __init_data(self): + + self.__size = QSize(810, 458) + + # 新建QPixmap作为画板,尺寸为__size + self.__board = QPixmap(self.__size) + self.__board.fill(Qt.transparent) # 用透明填充画板 + + self.__IsEmpty = True # 默认为空画板 + self.EraserMode = False # 默认为禁用橡皮擦模式 + + self.__lastPos = QPoint(0, 0) # 上一次鼠标位置 + self.__currentPos = QPoint(0, 0) # 当前的鼠标位置 + + self.__painter = QPainter() # 新建绘图工具 + + self.__thickness = 15 # 默认画笔粗细为10px + self.__penColor = QColor("black") # 设置默认画笔颜色为黑色 + self.__colorList = QColor.colorNames() # 获取颜色列表 + + def __init_view(self): + # 设置界面的尺寸为__size + self.setFixedSize(self.__size) + + def clear(self): + # 清空画板 + # self.__board.fill(Qt.white) + self.__board = QPixmap(self.__size) + self.__board.fill(Qt.transparent) # 用透明填充画板 + + self.update() + self.__IsEmpty = True + + def change_pen_color(self, color="black"): + # 改变画笔颜色 + # rgbaColor = QColor(255, 255, 0, 100) + self.__penColor = QColor(color) + + def change_pen_thickness(self, thickness=10): + # 改变画笔粗细 + self.__thickness = thickness + + def is_empty(self): + # 返回画板是否为空 + return self.__IsEmpty + + def get_content_as_q_image(self): + # 获取画板内容(返回QImage) + image = self.__board.toImage() + return image + + def paintEvent(self, paint_event): + # 绘图事件 + # 绘图时必须使用QPainter的实例,此处为__painter + # 绘图在begin()函数与end()函数间进行 + # begin(param)的参数要指定绘图设备,即把图画在哪里 + # drawPixmap用于绘制QPixmap类型的对象 + self.__painter.begin(self) + # 0,0为绘图的左上角起点的坐标,__board即要绘制的图 + self.__painter.drawPixmap(0, 0, self.__board) + self.__painter.end() + + def mousePressEvent(self, mouse_event): + # 鼠标按下时,获取鼠标的当前位置保存为上一次位置 + self.__currentPos = mouse_event.pos() + self.__lastPos = self.__currentPos + + def mouseMoveEvent(self, mouse_event): + # 鼠标移动时,更新当前位置,并在上一个位置和当前位置间画线 + self.__currentPos = mouse_event.pos() + self.__painter.begin(self.__board) + + if self.EraserMode == False: + # 非橡皮擦模式 + self.__painter.setPen(QPen(self.__penColor, self.__thickness)) # 设置画笔颜色,粗细 + else: + # 橡皮擦模式下画笔为纯白色,粗细为10 + self.__painter.setPen(QPen(Qt.transparent, 10)) + + # 画线 + # print(self.__lastPos + self.__currentPos) + self.__painter.drawLine(self.__lastPos, self.__currentPos) + self.__painter.end() + self.__lastPos = self.__currentPos + + self.update() # 更新显示 + + def mouseReleaseEvent(self, mouseEvent): + self.__IsEmpty = False # 画板不再为空 diff --git a/applications/EIVideo/QEIVideo/widget/__init__.py b/applications/EIVideo/QEIVideo/widget/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/EIVideo/README.md b/applications/EIVideo/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c88b596a389c44fa2486d64d5589d058b8d4120a --- /dev/null +++ b/applications/EIVideo/README.md @@ -0,0 +1,124 @@ +# EIVideo - 交互式智能视频标注工具 + +[![Downloads](https://static.pepy.tech/personalized-badge/eivideo?period=total&units=international_system&left_color=grey&right_color=orange&left_text=EIVideo%20User)](https://pepy.tech/project/eivideo) +[![Downloads](https://static.pepy.tech/personalized-badge/qeivideo?period=total&units=international_system&left_color=grey&right_color=orange&left_text=QEIVideo%20User)](https://pepy.tech/project/qeivideo) +![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/QPT-Family/EIVideo?include_prereleases) +![GitHub forks](https://img.shields.io/github/forks/QPT-Family/EIVideo) +![GitHub Repo stars](https://img.shields.io/github/stars/QPT-Family/EIVideo) +![GitHub](https://img.shields.io/github/license/QPT-Family/EIVideo) +![](https://img.shields.io/badge/%E6%B7%B1%E5%BA%A6%E9%80%82%E9%85%8D->Win7-9cf) + +--- + + +
+图片 +
+ +EIVideo,基于百度飞桨MA-Net交互式视频分割模型打造的交互式**智能视频**标注工具箱,只需简单标注几帧,即可完成全视频标注,若自动标注结果未达要求还可通过多次和视频交互而不断提升视频分割质量,直至对分割质量满意。 + +戳 -> 了解相关[技术文章&模型原理](等待微信公众号) + +
+图片 +
+ +> 为了更好的解放双手,我们还提供了图形化界面工具QEIVideo,通过它我们可以不使用繁杂的命令方式来完成视频的智能标注工作。 + +--- + +### README目录 + +- [EAP - The Early Access Program 早期访问计划](#eap---the-early-access-program-早期访问计划) +- [使用方式](#使用方式) + - [安装&运行](#安装运行) + - [QPT包 - 适合无Python基础用户](#qpt包---适合无python基础用户) + - [标准Python包 - 适合普通Python开发者](#标准python包---适合普通python开发者) + - [开发版本 - 适合高阶开发者进行开发/社区贡献](#开发版本---适合高阶开发者进行开发社区贡献) +- [(Q)EIVideo产品规划安排](#qeivideo产品规划安排) +- [开源协议](#开源协议) + +--- + +### EAP - The Early Access Program 早期访问计划 + +> Warning 当前图形化界面QEIVideo处于**极其初阶**的...建设阶段,并不能保证程序稳定性。 + +
图片
+ +当您选择使用QEIVideo作为图形化界面时,即可视为同意使用“可能会存在大量体验不佳”的EAP产品。 + +同样,您可选择借助基于[PaddleVideo](https://github.com/PaddlePaddle/PaddleVideo) 实现的 +交互式视频标注模型[EIVideo](https://github.com/QPT-Family/EIVideo/EIVideo) 进行二次开发,在此之上也可完成您需要的自定义图形化界面,后续也将提供二次开发指南。 + +
图片
+ + +> 如果您愿意参与到EIVideo或QEIVideo的建设中来,欢迎您与PMC取得联系 -> WX:GT_ZhangAcer + +## 使用方式 +### 安装&运行 +#### QPT包 - 适合无Python基础用户 +自动化配置相关Python环境,但仅支持Windows7/10/11操作系统,且不对盗版Windows7做任何适配。 +下载地址:暂未上传 +> 自动化部署工具由[QPT - 自动封装工具](https://github.com/QPT-Family/QPT) 支持 + +#### 标准Python包 - 适合普通Python开发者 +* 国际方式: + ```shell + python -m pip install eivideo + python qeivideo + ``` +* 国内推荐: + ```shell + python -m pip install eivideo -i https://mirrors.bfsu.edu.cn/pypi/web/simple + python qeivideo + ``` +> 上述命令仅适用于常规情况,若您安装了多个Python或修改了相关开发工具与配置,请自行修改相关命令使其符合您的开发环境。 + +#### 开发版本 - 适合高阶开发者进行开发/社区贡献 + +* 国际方式: + ```shell + git clone https://github.com/QPT-Family/EIVideo.git + python -m pip install -r requirements.txt + ``` +* 国内推荐: + ```shell + # 请勿用于Push!!! + git clone https://hub.fastgit.org/QPT-Family/EIVideo.git + python -m pip install -r requirements.txt -i https://mirrors.bfsu.edu.cn/pypi/web/simple + ``` +* 运行程序 + ```shell + # 进入工作目录 + cd 此处填写EIVideo所在的目录的绝对路径,且该目录下拥有EIVideo与QEIVideo两文件夹。 + # 运行 + python QEIVideo/start.py + + # 如运行时无法找到对应包,可选择下述方式添加环境变量来调整索引次序后执行python + # Windows + set PYTHONPATH=$pwd:$PYTHONPATH + # Linux + export PYTHONPATH=$pwd:$PYTHONPATH + ``` + +> 上述命令仅适用于常规情况,若您安装了多个Python或修改了相关开发工具与配置,请自行修改相关命令使其符合您的开发环境。 + +## (Q)EIVideo产品规划安排 +> 由于QEIVideo由飞桨开源社区学生爱好者构成,所以在项目的产出过程中将会以学习为主进行开源贡献,如您原因与我们一同建设,我们也将非常欢迎~ +
图片
+ +- [x] EIVideo与Demo版QEIVideo发布0.1.0Alpha版本 +- [ ] 完善QEIVideo,丰富基础标注功能,于Q1升级至1.0Alpha版本 +- [ ] 回归QEIVideo稳定性,于Q2完成1.0正式版本发版 +- [ ] 增加视频目标检测、分类任务的交互式标注功能。 + +### 开源协议 +本项目使用GNU LESSER GENERAL PUBLIC LICENSE(LGPL)开源协议。 +> 因所使用的模型与数据集等原因,本项目中任一代码、参数均不可直接进行商用,如需商用请与我们取得联系。 + +### 引用来源 +1. EIVideo模型以及相关源码、论文与项目 - [PaddleVideo](https://github.com/PaddlePaddle/PaddleVideo) +2. 部分表情包来源 - [甘城なつき](https://www.pixiv.net/users/3036679) + diff --git a/applications/EIVideo/requirements.txt b/applications/EIVideo/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b38e4d87114bbfa3924fb3f515c7eefdb6349270 --- /dev/null +++ b/applications/EIVideo/requirements.txt @@ -0,0 +1,17 @@ +numpy +pandas +tqdm +PyYAML>=5.1 +opencv-python==4.2.0.32 +decord==0.4.2 +av==8.0.3 +scipy==1.6.3 +ffmpeg-python==0.2.0 +scikit-image +matplotlib +paddlenlp +lmdb +SimpleITK +pyqt5 +paddlevideo +davisinteractive diff --git a/applications/EIVideo/resources/QT/demo.ui b/applications/EIVideo/resources/QT/demo.ui new file mode 100644 index 0000000000000000000000000000000000000000..b11250624fc90d6029c062d69a13481b24cc2e66 --- /dev/null +++ b/applications/EIVideo/resources/QT/demo.ui @@ -0,0 +1,236 @@ + + + MainWindow + + + + 0 + 0 + 800 + 486 + + + + + 800 + 486 + + + + + 800 + 486 + + + + MainWindow + + + + + + 20 + 20 + 761 + 361 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 0 + 0 + 761 + 321 + + + + + + + 0 + 320 + 761 + 41 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + -1 + -1 + 761 + 41 + + + + + + + 打开视频 + + + + + + + 保存标注 + + + + + + + Qt::Horizontal + + + + + + + 选择目标 + + + + + + + 清空目标 + + + + + + + 开始推理 + + + + + + + + + + 0 + 10 + 751 + 301 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 20 + 380 + 761 + 81 + + + + 0 + + + + 状态 + + + + true + + + + 10 + 30 + 71 + 21 + + + + 当前状态: + + + + + true + + + + 80 + 30 + 81 + 21 + + + + ------------- + + + + + + 170 + 32 + 521 + 21 + + + + 24 + + + + + true + + + + 680 + 30 + 60 + 21 + + + + Qt::LeftToRight + + + 12% + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 属性配置 + + + + + + + + + diff --git a/applications/EIVideo/resources/__init__.py b/applications/EIVideo/resources/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/EIVideo/resources/cmd b/applications/EIVideo/resources/cmd new file mode 100644 index 0000000000000000000000000000000000000000..c21d88afbed0dfa6b80c0e87842538a4b6472cf2 --- /dev/null +++ b/applications/EIVideo/resources/cmd @@ -0,0 +1,4 @@ +# 更新PaddleVideo上的EIVideo +git subtree push --prefix=applications/EIVideo/ https://github.com/QPT-Family/EIVideo 开发分支 +git subtree pull --prefix=applications/EIVideo/ https://github.com/QPT-Family/EIVideo 开发分支 --squash +git subtree split --rejoin --prefix=applications/EIVideo/ --branch 开发分支 \ No newline at end of file diff --git a/applications/FigureSkating/Alex.gif b/applications/FigureSkating/Alex.gif new file mode 100644 index 0000000000000000000000000000000000000000..cadc30dda3be3856065c5d5e42607ec4e063a609 Binary files /dev/null and b/applications/FigureSkating/Alex.gif differ diff --git a/applications/FigureSkating/README.md b/applications/FigureSkating/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5f7af394b5f05d7cae92d22c3867a4be0ba81520 --- /dev/null +++ b/applications/FigureSkating/README.md @@ -0,0 +1,92 @@ +# 花样滑冰动作识别 + +--- +## 内容 + +- [视频数据处理方法](#视频数据处理方法) +- [模型训练预测方法](#模型训练预测方法) + + +
+
+ +### 视频数据处理方法 + + - 提供从视频中提取骨骼点数据的方法,方便用户自行提取数据进行测试。 + + 花样滑冰数据提取采用了openpose,通过其提供的demo或是相应的api来实现数据的提取,因此需要用户配置openpose环境。 + 如下是通过花样滑冰数据集构建项目[Skeleton Scripts](https://github.com/HaxiSnake/skeleton_scripts)提取骨骼点数据方法的具体介绍。 + + #### step1 安装openpose + + - 参考:https://github.com/CMU-Perceptual-Computing-Lab/openpose + + #### step2 测试openpose提供demo + + - 这里通过测试openpose的demo程序来验证是否安装成功。 + + demo1:检测视频中身体骨骼点(以linux系统为例): + + ```bash + ./build/examples/openpose/openpose.bin --video examples_video.avi --write_json output/ --display 0 --render_pose 0 + ``` + + 执行成功之后会在output/路径下生成视频每一帧骨骼点数据的json文件。 + + demo2:检测视频中身体+面部+手部骨骼点(以linux系统为例): + + ```bash + ./build/examples/openpose/openpose.bin --video examples_video.avi --write_json output/ --display 0 --render_pose 0 --face --hand + ``` + + 执行成功之后会在output/路径下生成视频每一帧身体+面部+手部骨骼点数据的json文件。 + + #### step3 视频及相关信息处理 + + - 由于[Skeleton Scripts](https://github.com/HaxiSnake/skeleton_scripts)为制作花样滑冰数据集所用,因此此处步骤可能存在不同程度误差,实际请用户自行调试代码。 + + 将要转化的花样滑冰视频储存到[Skeleton Scripts](https://github.com/HaxiSnake/skeleton_scripts)的指定路径(可自行创建): + ```bash + ./skating2.0/skating63/ + ``` + + 同时需要用户自行完成对视频信息的提取,保存为label_skating63.csv文件,储存到如下路径中(可自行创建): + + ```bash + ./skating2.0/skating63/ + ./skating2.0/skating63_openpose_result/ + ``` + + label_skating63.csv中格式如下: + + | 动作分类 | 视频文件名 | 视频帧数 | 动作标签 | + | :----: | :----: | :----: | :---- | + + 此处用户只需要输入视频文件名(无需后缀,默认后缀名为.mp4,其他格式需自行更改代码),其他三项定义为空字符串即可,不同表项之间通过 ',' 分割。 + + #### step4 执行skating_convert.py: + + - 注意,这一步需要根据用户对openpose的配置进行代码的更改,主要修改项为openpose路径、openpose-demo路径等,具体详见代码。 + + 本脚步原理是调用openpose提供的demo提取视频中的骨骼点,并进行数据格式清洗,最后将每个视频的提取结果结果打包成json文件,json文件储存在如下路径: + + ```bash + ./skating2.0/skating63_openpose_result/label_skating63_data/ + ``` + + #### step5 执行skating_gendata.py: + + 将json文件整理为npy文件并保存,多个视频文件将保存为一个npy文件,保存路径为: + + ```bash + ./skating2.0/skating63_openpose_result/skeleton_file/ + ``` + + - 通过上述步骤就可以将视频数据转化为无标签的骨骼点数据。 + + - 最后用户只需将npy数据输入送入网络开始模型测试,亦可通过预测引擎推理。 + + + ### 模型训练预测方法 + + 模型使用方法参考[ST-GCN模型文档](../../docs/zh-CN/model_zoo/recognition/stgcn.md) diff --git a/applications/FootballAction/README.md b/applications/FootballAction/README.md new file mode 100644 index 0000000000000000000000000000000000000000..51770f24b30435abdede29363fb3c674bd819c86 --- /dev/null +++ b/applications/FootballAction/README.md @@ -0,0 +1,430 @@ +# 足球动作检测模型 + + +## 内容 +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型推理](#模型推理) +- [模型训练](#模型训练) +- [使用训练模型推理](#使用训练模型推理) +- [模型评估](#模型评估) +- [模型优化](#模型优化) +- [模型部署](#模型部署) +- [参考论文](#参考论文) + + +## 模型简介 +该代码库用于体育动作检测+识别, 基于paddle2.0版本开发,结合PaddleVideo中的PP-TSM, BMN, AttentionLSTM的多个视频模型进行视频时空二阶段检测算法。 +主要分为如下几步 + - 特征抽取 + - 图像特性,PP-TSM + - 音频特征,VGGish + - proposal提取,BMN + - LSTM,动作分类 + 回归 + + +AIStudio项目: [基于PP-TSM+BMN+LSTM实现足球精彩时刻剪辑](https://aistudio.baidu.com/aistudio/projectdetail/3473391?channelType=0&channel=0) + + +## 数据准备 +- 数据集label格式 +``` +数据集来自欧洲杯2016,共49个足球视频,其中训练集44个,验证集5个 +数据集地址: datasets/EuroCup2016/dataset_url.list +数据集label格式 +{ + "0": "背景", + "1": "进球", + "2": "角球", + "3": "任意球", + "4": "黄牌", + "5": "红牌", + "6": "换人", + "7": "界外球", +} +数据集标注文件: +datasets/EuroCup2016/label_cls8_train.json +datasets/EuroCup2016/label_cls8_val.json +``` +下载数据集: +``` +cd datasets/EuroCup2016 && sh download_dataset.sh +``` + +- 数据集gts处理, 将原始标注数据处理成如下json格式 +``` +{ + 'fps': 5, + 'gts': [ + { + 'url': 'xxx.mp4', + 'total_frames': 6341, + 'actions': [ + { + "label_ids": [7], + "label_names": ["界外球"], + "start_id": 395, + "end_id": 399 + }, + ... + ] + }, + ... + ] +} +``` + +- 数据集抽帧, 由mp4, 得到frames和pcm, 这里需要添加ffmpeg环境 +``` +cd datasets/script && python get_frames_pcm.py +``` +- 数据预处理后保存格式如下 +``` + |-- datasets # 训练数据集和处理脚本 + |-- EuroCup2016 # xx数据集 + |-- mp4 # 原始视频.mp4 + |-- frames # 图像帧, fps=5, '.jpg'格式 + |-- pcm # 音频pcm, 音频采样率16000,采用通道数1 + |-- url.list # 视频列表 + |-- label_train.json # 训练集原始gts + |-- label_val.json # 验证集原始gts +``` + +### 基础镜像 +```bash +docker pull tmtalgo/paddleaction:action-detection-v2 +``` + +### 代码结构 +``` +|-- root_dir + |-- checkpoints # 保存训练后的模型和log + |-- datasets # 训练数据集和处理脚本 + |-- EuroCup2016 # xx数据集 + |-- feature_bmn # bmn提取到的proposal + |-- features # tsn和audio特征, image fps=5, audio 每秒(1024) + |-- input_for_bmn # bmn训练的输入数据,widows=40 + |-- input_for_lstm # lstm训练的输入数据 + |-- input_for_pptsm # tsn训练的数据数据 + |-- mp4 # 原始视频.mp4 + |-- frames # 图像帧, fps=5, '.jpg'格式 + |-- pcm # 音频pcm, 音频采样率16000,采用通道数1 + |-- url.list # 视频列表 + |-- label_cls8_train.json # 训练集原始gts + |-- label_cls8_val.json # 验证集原始gts + |-- label.json # 动作label + |-- script # 数据集处理脚本 + |-- predict # 模型预测代码 + |-- extractor # 特征提取脚本 + |-- train_lstm # lstm训练代码 + |-- train_proposal # tsn、bmn训练代码,基本保持paddle-release-v1.8版本,数据接口部分稍有改动,参考官网 + |-- configs # tsn and bmn football config file +``` +## 模型推理 +可以通过如下命令直接进行推理,无需训练。 + +首先,通过以下命令,下载训练好的模型文件: +```bash +cd checkpoints +sh download.sh +``` + +进行预测前需要修改 `predict/configs/configs.yaml` 文件,修改模型路径。运行预测代码: +``` +cd predict && python predict.py +``` +产出文件:results.json + + +## 模型训练 +采样方式: +- image 采样频率fps=5,如果有些动作时间较短,可以适当提高采样频率 +- BMN windows=200,即40s,所以测试自己的数据时,视频时长需大于40s + +### step1 PP-TSM训练 + +#### step1.1 PP-TSM 训练数据处理 +由frames结合gts生成训练所需要的正负样本。注意按照实际路径更新python文件中的 `data_dir` +``` +cd datasets/script && python get_instance_for_pptsm.py + +# 文件名按照如下格式 +'{}_{}_{}_{}'.format(video_basename, start_id, end_id, label) +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- EuroCup2016 # xx数据集 + |-- input_for_pptsm # pptsm训练的数据 +``` + +#### step1.2 PP-TSM模型训练 +我们提供了足球数据训练的模型,参考checkpoints +如果需要在自己的数据上训练,可参考 +https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +config.yaml参考configs文件夹下pptsm_football_v2.0.yaml +注意更换config文件中的file_path +``` +# https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +cd ${PaddleVideo} +python -B -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + --log_dir=$save_dir/logs \ + main.py \ + --validate \ + -c ${FootballAcation}/train_proposal/configs/pptsm_football_v2.0.yaml \ + -o output_dir=$save_dir +``` + +#### step1.3 PP-TSM模型转为预测模式 +在转为预测模式前,需要修改 `PaddleVideo/paddlevideo/modeling/framework/recognizers/recognizer2d.py` 文件,将 init 和 infer_step 函数分别更新为如下代码: + +```python +def __init__(self, backbone=None, head=None): + super().__init__(backbone=backbone, head=head) + self.avgpool2d = paddle.nn.AdaptiveAvgPool2D((1, 1), data_format='NCHW') + +def infer_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + imgs = data_batch[0] + imgs = paddle.reshape_(imgs, [-1] + list(imgs.shape[2:])) + feature = self.backbone(imgs) + feat = self.avgpool2d(feature) + return feat +``` +再执行如下命令: + +``` +cd ${PaddleVideo} +python tools/export_model.py -c ${FootballAcation}/train_proposal/configs/pptsm_football_v2.0.yaml \ + -p ${pptsm_train_dir}/checkpoints/ppTSM_best.pdparams \ + -o {FootballAcation}/checkpoints/ppTSM +``` + +#### step1.4 基于PP-TSM的视频特征提取 + +将 `predict/action_detect/models/pptsm_infer.py` 文件中的 +```python +self.output_tensor = self.predictor.get_output_handle(output_names[1]) +``` +替换为 +```python +self.output_tensor = self.predictor.get_output_handle(output_names[0]) +``` + +image and audio特征提取,保存到datasets features文件夹下。注意更改python文件中 dataset_dir 的路径,以及configs.yaml文件下的模型路径。 + +``` +cd ${FootballAcation} +cd extractor && python extract_feat.py +# 特征维度, image(2048) + audio(1024) +# 特征保存格式如下,将如下dict保存在pkl格式,用于接下来的BMN训练 +video_features = {'image_feature': np_image_features, + 'audio_feature': np_audio_features} +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- EuroCup2016 # xx数据集 + |-- features # 视频的图像+音频特征 +``` + +### step2 BMN训练 +BMN训练代码为:https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +BMN文档参考:https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/model_zoo/localization/bmn.md + +#### step2.1 BMN训练数据处理 +用于提取二分类的proposal,windows=40,根据gts和特征得到BMN训练所需要的数据集 +``` +cd datasets/script && python get_instance_for_bmn.py +# 数据格式 +{ + "719b0a4bcb1f461eabb152298406b861_753_793": { + "duration_second": 40.0, + "duration_frame": 200, + "feature_frame": 200, + "subset": "train", + "annotations": [ + { + "segment": [ + 15.0, + 22.0 + ], + "label": "3.0", + "label_name": "任意球" + } + ] + }, + ... +} +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- EuroCup2016 # xx数据集 + |-- input_for_bmn # bmn训练的proposal +``` + +#### step2.2 BMN模型训练 +我们同样提供了足球数据训练的模型,参考checkpoints +如果要在自己的数据上训练,步骤与step1.2 ppTSM 训练相似,可参考 +https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +config.yaml参考configs文件夹下bmn_football_v2.0.yaml。训练前需更改config文件,配置好路径及其他参数。 +``` +# https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +cd ${PaddleVideo} +python -B -m paddle.distributed.launch \ + --gpus="0,1" \ + --log_dir=$out_dir/logs \ + main.py \ + --validate \ + -c ${FootballAcation}/train_proposal/configs/bmn_football_v2.0.yaml \ + -o output_dir=$out_dir +``` + +#### step2.3 BMN模型转为预测模式 +同step1.3 +``` +# https://github.com/PaddlePaddle/PaddleVideo/tree/release/2.0 +$cd {PaddleVideo} +python tools/export_model.py -c ${FootballAcation}/train_proposal/configs/bmn_football_v2.0.yaml \ + -p ${bmn_train_dir}/checkpoints/models_bmn/bmn_epoch16.pdparams \ + -o {FootballAcation}/checkpoints/BMN +``` + +#### step2.4 BMN模型预测 +得到动作proposal信息: start_id, end_id, score。预测前需先修改文件中的dataset_dir 以及configs.yaml文件下的模型路径。 +``` +cd extractor && python extract_bmn.py +# 数据格式 +[ + { + "video_name": "c9516c903de3416c97dae91a59e968d7", + "num_proposal": 5534, + "bmn_results": [ + { + "start": 7850.0, + "end": 7873.0, + "score": 0.77194699622342 + }, + { + "start": 4400.0, + "end": 4443.0, + "score": 0.7663803287641536 + }, + ... + ] + }, + ... +] +``` +完成该步骤后,数据存储位置 +``` + |-- datasets # 训练数据集和处理脚本 + |-- EuroCup2016 # xx数据集 + |-- feature_bmn + |-- prop.json # bmn 预测结果 +``` + +### step3 LSTM训练 + +#### step3.1 LSTM训练数据处理 +将BMN得到的proposal截断并处理成LSTM训练所需数据集。同理,注意数据集文件修改路径。 +``` +cd datasets/script && python get_instance_for_lstm.py +# 数据格式1,label_info +{ + "fps": 5, + "results": [ + { + "url": "https://xxx.mp4", + "mode": "train", # train or validation + "total_frames": 6128, + "num_gts": 93, + "num_proposals": 5043, + "proposal_actions": [ + { + "label": 6, + "norm_iou": 0.7575757575757576, + "norm_ioa": 0.7575757575757576, + "norm_start": -0.32, + "proposal": { + "start": 5011, + "end": 5036, + "score": 0.7723643666324231 + }, + "hit_gts": { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5003, + "end_id": 5036 + } + }, + ... + }, + ... +} +# 数据格式2,LSTM训练所需要的feature +{ + 'features': np.array(feature_hit, dtype=np.float32), # TSN and audio 特征 + 'feature_fps': 5, # fps = 5 + 'label_info': {'norm_iou': 0.5, 'label': 3, ...}, # 数据格式1中的'proposal_actions' + 'video_name': 'c9516c903de3416c97dae91a59e968d7' # video_name +} +# 数据格式3,LSTM训练所需label.txt +'{} {}'.format(filename, label) +``` + +#### step3.2 LSTM训练 +``` +python -u scenario_lib/train.py \ + --model_name=ActionNet \ + --config=conf/conf.txt \ + --save_dir=$save_dir + --log_interval=5 \ + --valid_interval=1 +``` + +#### step3.3 LSTM模型转为预测模式 +``` +${FootballAction}/train_lstm +python inference_model.py --config=conf/conf.yaml --weights=$weight_path/LSTM.pdparams --save_dir=$save_dir +``` + +## 使用训练模型推理 +运行预测代码 +``` +cd predict && python predict.py +``` +产出文件:results.json + + +## 模型评估 +``` +# 包括bmn proposal 评估和最终action评估 +cd predict && python eval.py results.json +``` + + +## 模型优化 +- 基础特征模型(图像)替换为PP-TSM,准确率由84%提升到94% +- 基础特征模型(音频)没变动 +- BMN,请使用paddlevideo最新版 +- LSTM,暂时提供v1.8训练代码(后续升级为v2.0),也可自行尝试使用paddlevideo-2.0中的attentation lstm +- 为兼容paddle-v1.8和paddle-v2.0,将模型预测改为inference model,训练代码可以使用v1.8或v2.0,只要export为inference model即可进行预测 +- 准确率提升,precision和recall均有大幅提升,F1-score从0.57提升到0.82 + + +## 模型部署 +本代码解决方案在动作的检测和召回指标F1-score=82% + + +## 参考论文 +- [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf), Ji Lin, Chuang Gan, Song Han +- [BMN: Boundary-Matching Network for Temporal Action Proposal Generation](https://arxiv.org/abs/1907.09702), Tianwei Lin, Xiao Liu, Xin Li, Errui Ding, Shilei Wen. +- [Attention Clusters: Purely Attention Based Local Feature Integration for Video Classification](https://arxiv.org/abs/1711.09550), Xiang Long, Chuang Gan, Gerard de Melo, Jiajun Wu, Xiao Liu, Shilei Wen +- [YouTube-8M: A Large-Scale Video Classification Benchmark](https://arxiv.org/abs/1609.08675), Sami Abu-El-Haija, Nisarg Kothari, Joonseok Lee, Paul Natsev, George Toderici, Balakrishnan Varadarajan, Sudheendra Vijayanarasimhan diff --git a/applications/FootballAction/checkpoints/download.sh b/applications/FootballAction/checkpoints/download.sh new file mode 100644 index 0000000000000000000000000000000000000000..594866525874509f0d3dba2d3ff1c6d6af496486 --- /dev/null +++ b/applications/FootballAction/checkpoints/download.sh @@ -0,0 +1,18 @@ +# audio +wget https://videotag.bj.bcebos.com/PaddleVideo-release2.1/FootballAction/audio.tar +# pptsm +wget https://videotag.bj.bcebos.com/PaddleVideo-release2.1/FootballAction/pptsm.tar +# bmn +wget https://videotag.bj.bcebos.com/PaddleVideo-release2.1/FootballAction/bmn.tar +# lstm +wget https://videotag.bj.bcebos.com/PaddleVideo-release2.1/FootballAction/lstm.tar + +tar -xvf audio.tar +tar -xvf pptsm.tar +tar -xvf bmn.tar +tar -xvf lstm.tar + +rm -f audio.tar +rm -f pptsm.tar +rm -f bmn.tar +rm -f lstm.tar diff --git a/applications/FootballAction/datasets/EuroCup2016/dataset_url.list b/applications/FootballAction/datasets/EuroCup2016/dataset_url.list new file mode 100644 index 0000000000000000000000000000000000000000..44e8a359410b37c6191ea1ef91737b0100127501 --- /dev/null +++ b/applications/FootballAction/datasets/EuroCup2016/dataset_url.list @@ -0,0 +1,49 @@ +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/63e51df254d2402fac703b6c4fdb4ea9.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/76b5f7ee28d942988c6b224bfac136bd.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/250b88724acf40dbb6d7e8ccb400ef38.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/c9516c903de3416c97dae91a59e968d7.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/e1982c90cdd74abaacc4d0692070b400.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/1be705a8f67648da8ec4b4296fa80895.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/de23c0b2be3a4eb1990c5c657061fb29.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/2754615de6e64c4fb95ce1a8095dc1c1.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/299fe30d8f3b4a45b89313fe31f9f3c0.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6cc7db52c5ef4e70b401a5e00d8dd67a.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/22e89747689e4f7e83e3620620c93269.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/2ceb6c549fc64305a06a75acb355642b.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/719b0a4bcb1f461eabb152298406b861.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/259856b769044b4d8dc94076deb356bf.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/d0bd3eab1e794f0f9501c353a6d37827.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/19eb47cc736240d6b2dd930ab69da839.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/4435b708af6d48519a6b726144147d51.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/ea16ad2a020643529e257bd6cb11b3c3.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/eeebffbd4ec74222a9c2d0775d79b689.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/8cfb4e605af44055b1576c37eb0e3209.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6bca62b57cc449c6935f0b17f28d06be.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/70cfc31e520840b2afca458f93a01ce4.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6496960935e845578e391a5916739752.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/d6d25403a4bb4784aecff5f21fd00dc5.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/3e23d452a082403391f8abfb87bf2fb4.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/4c5d9d9af4f044c4a68d134061dc264f.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6994844c64b44c26b935cee9604bef0a.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/d6322cb95f6a4402ac80432b561abd5d.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/2c8b5587083a4784a51622e4fec87ccd.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/5faa60d70ed141de8560110e840f2048.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/45d08bc5cb0f424f9ed9d7874eb561cd.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6630aaf0e32146088d0b624e9288f071.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/f2edbee29c1b4966b3a410260f78fbe3.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/f24116fdd6a54214991db32f7dddef67.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/0265731a0c6f4a9398c88db8e3d4a3bc.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/02d2de09997f4215b06e3b00ff0502a0.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/9c231896c56a43f291a5e190949f4333.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/4afbbf9afcd44dfea45b044117cccb48.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/745db97a080d4f44b450dc17a2bcf069.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/5933d0ce17854483b81a318d7d45a34e.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/d2cfef2da9f84237a6950c7f6659655c.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/5572686cb90f440988ded956a60e555d.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/8962ac5a332346e180c79d701ae0a175.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/f6e64ee9b13a4088b24c45c257894c1e.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/f6ed2b612b3d43baa0726be8b14ebe7c.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/8ab7b0cba5744eb3b6fb10003dfda383.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/1f0a0698e38d493988fe42a50f7e8723.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/737fdb054ca141f2a45013c1740dd0a0.mp4 +https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/bab63a9bcf204e4b99c4a887a01bfd60.mp4 diff --git a/applications/FootballAction/datasets/EuroCup2016/download_dataset.sh b/applications/FootballAction/datasets/EuroCup2016/download_dataset.sh new file mode 100644 index 0000000000000000000000000000000000000000..180a4be807d5859a9af4eea300c43c66d600e2eb --- /dev/null +++ b/applications/FootballAction/datasets/EuroCup2016/download_dataset.sh @@ -0,0 +1,51 @@ +mkdir mp4 +cd mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/63e51df254d2402fac703b6c4fdb4ea9.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/76b5f7ee28d942988c6b224bfac136bd.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/250b88724acf40dbb6d7e8ccb400ef38.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/c9516c903de3416c97dae91a59e968d7.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/e1982c90cdd74abaacc4d0692070b400.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/1be705a8f67648da8ec4b4296fa80895.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/de23c0b2be3a4eb1990c5c657061fb29.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/2754615de6e64c4fb95ce1a8095dc1c1.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/299fe30d8f3b4a45b89313fe31f9f3c0.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6cc7db52c5ef4e70b401a5e00d8dd67a.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/22e89747689e4f7e83e3620620c93269.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/2ceb6c549fc64305a06a75acb355642b.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/719b0a4bcb1f461eabb152298406b861.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/259856b769044b4d8dc94076deb356bf.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/d0bd3eab1e794f0f9501c353a6d37827.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/19eb47cc736240d6b2dd930ab69da839.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/4435b708af6d48519a6b726144147d51.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/ea16ad2a020643529e257bd6cb11b3c3.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/eeebffbd4ec74222a9c2d0775d79b689.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/8cfb4e605af44055b1576c37eb0e3209.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6bca62b57cc449c6935f0b17f28d06be.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/70cfc31e520840b2afca458f93a01ce4.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6496960935e845578e391a5916739752.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/d6d25403a4bb4784aecff5f21fd00dc5.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/3e23d452a082403391f8abfb87bf2fb4.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/4c5d9d9af4f044c4a68d134061dc264f.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6994844c64b44c26b935cee9604bef0a.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/d6322cb95f6a4402ac80432b561abd5d.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/2c8b5587083a4784a51622e4fec87ccd.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/5faa60d70ed141de8560110e840f2048.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/45d08bc5cb0f424f9ed9d7874eb561cd.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/6630aaf0e32146088d0b624e9288f071.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/f2edbee29c1b4966b3a410260f78fbe3.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/f24116fdd6a54214991db32f7dddef67.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/0265731a0c6f4a9398c88db8e3d4a3bc.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/02d2de09997f4215b06e3b00ff0502a0.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/9c231896c56a43f291a5e190949f4333.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/4afbbf9afcd44dfea45b044117cccb48.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/745db97a080d4f44b450dc17a2bcf069.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/5933d0ce17854483b81a318d7d45a34e.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/d2cfef2da9f84237a6950c7f6659655c.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/5572686cb90f440988ded956a60e555d.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/8962ac5a332346e180c79d701ae0a175.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/f6e64ee9b13a4088b24c45c257894c1e.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/f6ed2b612b3d43baa0726be8b14ebe7c.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/8ab7b0cba5744eb3b6fb10003dfda383.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/1f0a0698e38d493988fe42a50f7e8723.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/737fdb054ca141f2a45013c1740dd0a0.mp4 +wget https://bj.bcebos.com/v1/tmt-pub/datasets/EuroCup2016/bab63a9bcf204e4b99c4a887a01bfd60.mp4 \ No newline at end of file diff --git a/applications/FootballAction/datasets/EuroCup2016/label.json b/applications/FootballAction/datasets/EuroCup2016/label.json new file mode 100644 index 0000000000000000000000000000000000000000..2e76808a2aac6031956debf3609a6a1455664fb0 --- /dev/null +++ b/applications/FootballAction/datasets/EuroCup2016/label.json @@ -0,0 +1,10 @@ +{ +"0": "背景", +"1": "进球", +"2": "角球", +"3": "任意球", +"4": "黄牌", +"5": "红牌", +"6": "换人", +"7": "界外球", +} diff --git a/applications/FootballAction/datasets/EuroCup2016/label_cls8_train.json b/applications/FootballAction/datasets/EuroCup2016/label_cls8_train.json new file mode 100644 index 0000000000000000000000000000000000000000..7a704fb171fc6933bec2ff368043d5324c2492b4 --- /dev/null +++ b/applications/FootballAction/datasets/EuroCup2016/label_cls8_train.json @@ -0,0 +1,26557 @@ +{ + "fps": 5, + "gts": [ + { + "url": "EuroCup2016/719b0a4bcb1f461eabb152298406b861.mp4", + "total_frames": 6128, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 395, + "end_id": 399 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 612, + "end_id": 616 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 768, + "end_id": 775 + }, + { + "label_ids": [ + 1, + 2 + ], + "label_names": [ + "进球", + "角球" + ], + "start_id": 794, + "end_id": 831 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 895, + "end_id": 899 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1008, + "end_id": 1013 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1098, + "end_id": 1109 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1624, + "end_id": 1639 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1769, + "end_id": 1772 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2212, + "end_id": 2215 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2640, + "end_id": 2642 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2664, + "end_id": 2667 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2746, + "end_id": 2754 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2826, + "end_id": 2829 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2894, + "end_id": 2897 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2904, + "end_id": 2924 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3125, + "end_id": 3128 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3199, + "end_id": 3209 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3681, + "end_id": 3689 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3731, + "end_id": 3743 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3819, + "end_id": 3824 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3894, + "end_id": 3904 + }, + { + "label_ids": [ + 1, + 2 + ], + "label_names": [ + "进球", + "角球" + ], + "start_id": 4194, + "end_id": 4231 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4380, + "end_id": 4391 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4444, + "end_id": 4457 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4502, + "end_id": 4512 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4719, + "end_id": 4746 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4754, + "end_id": 4768 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4855, + "end_id": 4858 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4868, + "end_id": 4872 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4888, + "end_id": 4892 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5003, + "end_id": 5036 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5423, + "end_id": 5445 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5463, + "end_id": 5492 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5494, + "end_id": 5502 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5606, + "end_id": 5623 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5655, + "end_id": 5658 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5713, + "end_id": 5733 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5749, + "end_id": 5754 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5882, + "end_id": 5884 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5911, + "end_id": 5919 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5950, + "end_id": 5963 + } + ] + }, + { + "url": "EuroCup2016/5faa60d70ed141de8560110e840f2048.mp4", + "total_frames": 6148, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 541, + "end_id": 544 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 867, + "end_id": 872 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 891, + "end_id": 919 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1253, + "end_id": 1255 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1384, + "end_id": 1389 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1679, + "end_id": 1686 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1720, + "end_id": 1722 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1765, + "end_id": 1779 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1800, + "end_id": 1805 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2037, + "end_id": 2050 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2117, + "end_id": 2137 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2158, + "end_id": 2165 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 2348, + "end_id": 2378 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2489, + "end_id": 2491 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2577, + "end_id": 2584 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2651, + "end_id": 2666 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2680, + "end_id": 2684 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2787, + "end_id": 2795 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2882, + "end_id": 2887 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2904, + "end_id": 2906 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2940, + "end_id": 2941 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3101, + "end_id": 3118 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3249, + "end_id": 3260 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3287, + "end_id": 3289 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3328, + "end_id": 3330 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3401, + "end_id": 3407 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3417, + "end_id": 3426 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3517, + "end_id": 3526 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3561, + "end_id": 3562 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3623, + "end_id": 3625 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3655, + "end_id": 3687 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3828, + "end_id": 3832 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4237, + "end_id": 4247 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4493, + "end_id": 4496 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4529, + "end_id": 4533 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4583, + "end_id": 4585 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4616, + "end_id": 4619 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4666, + "end_id": 4673 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4699, + "end_id": 4703 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4734, + "end_id": 4744 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4818, + "end_id": 4826 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4875, + "end_id": 4876 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4989, + "end_id": 4992 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5049, + "end_id": 5052 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5173, + "end_id": 5175 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5184, + "end_id": 5186 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5332, + "end_id": 5390 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5401, + "end_id": 5403 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5499, + "end_id": 5531 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5609, + "end_id": 5611 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5656, + "end_id": 5669 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5778, + "end_id": 5786 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5863, + "end_id": 5865 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5887, + "end_id": 5899 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5913, + "end_id": 5939 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6016, + "end_id": 6017 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6085, + "end_id": 6090 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6112, + "end_id": 6115 + } + ] + }, + { + "url": "EuroCup2016/d6322cb95f6a4402ac80432b561abd5d.mp4", + "total_frames": 6405, + "actions": [ + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 489, + "end_id": 523 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 569, + "end_id": 573 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 581, + "end_id": 616 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 662, + "end_id": 666 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 810, + "end_id": 818 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 937, + "end_id": 947 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1032, + "end_id": 1040 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1087, + "end_id": 1100 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1228, + "end_id": 1236 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1278, + "end_id": 1283 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1291, + "end_id": 1302 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1324, + "end_id": 1327 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1372, + "end_id": 1378 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1435, + "end_id": 1447 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1593, + "end_id": 1595 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1756, + "end_id": 1764 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1806, + "end_id": 1809 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1966, + "end_id": 1973 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2054, + "end_id": 2058 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2066, + "end_id": 2070 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2082, + "end_id": 2086 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2187, + "end_id": 2193 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2226, + "end_id": 2235 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2250, + "end_id": 2255 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2386, + "end_id": 2392 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2632, + "end_id": 2635 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2688, + "end_id": 2696 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2753, + "end_id": 2764 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2775, + "end_id": 2790 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2812, + "end_id": 2824 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2959, + "end_id": 2968 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3032, + "end_id": 3036 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3241, + "end_id": 3244 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3247, + "end_id": 3253 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3533, + "end_id": 3543 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3875, + "end_id": 3877 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4091, + "end_id": 4126 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4189, + "end_id": 4200 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4316, + "end_id": 4349 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4434, + "end_id": 4439 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4565, + "end_id": 4572 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4573, + "end_id": 4579 + }, + { + "label_ids": [ + 5 + ], + "label_names": [ + "红牌" + ], + "start_id": 4586, + "end_id": 4602 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4732, + "end_id": 4737 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4755, + "end_id": 4763 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4777, + "end_id": 4781 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4812, + "end_id": 4814 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4939, + "end_id": 4945 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4961, + "end_id": 4966 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5052, + "end_id": 5059 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5194, + "end_id": 5200 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5378, + "end_id": 5391 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5396, + "end_id": 5408 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5436, + "end_id": 5444 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6010, + "end_id": 6014 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6236, + "end_id": 6243 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6271, + "end_id": 6274 + } + ] + }, + { + "url": "EuroCup2016/9c231896c56a43f291a5e190949f4333.mp4", + "total_frames": 8716, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 386, + "end_id": 388 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 399, + "end_id": 402 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 427, + "end_id": 430 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 583, + "end_id": 586 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 612, + "end_id": 616 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 715, + "end_id": 717 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 732, + "end_id": 735 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 806, + "end_id": 813 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 845, + "end_id": 849 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1006, + "end_id": 1011 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1326, + "end_id": 1328 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1339, + "end_id": 1345 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1421, + "end_id": 1433 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1476, + "end_id": 1487 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1568, + "end_id": 1570 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1623, + "end_id": 1632 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1669, + "end_id": 1676 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1720, + "end_id": 1723 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1871, + "end_id": 1873 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2044, + "end_id": 2051 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2242, + "end_id": 2251 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2347, + "end_id": 2358 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2406, + "end_id": 2418 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2616, + "end_id": 2627 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2635, + "end_id": 2670 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2753, + "end_id": 2757 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2847, + "end_id": 2849 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3114, + "end_id": 3121 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3225, + "end_id": 3230 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3260, + "end_id": 3268 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3342, + "end_id": 3344 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3395, + "end_id": 3398 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3550, + "end_id": 3553 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3581, + "end_id": 3584 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3631, + "end_id": 3634 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3709, + "end_id": 3713 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3738, + "end_id": 3740 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3790, + "end_id": 3807 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3912, + "end_id": 3918 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3918, + "end_id": 3944 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3967, + "end_id": 3978 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4243, + "end_id": 4251 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4286, + "end_id": 4288 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4317, + "end_id": 4320 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4329, + "end_id": 4332 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4375, + "end_id": 4378 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4573, + "end_id": 4588 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4593, + "end_id": 4599 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4659, + "end_id": 4669 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4868, + "end_id": 4882 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4896, + "end_id": 4901 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5064, + "end_id": 5077 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5117, + "end_id": 5119 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5135, + "end_id": 5140 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5176, + "end_id": 5186 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5241, + "end_id": 5244 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5295, + "end_id": 5308 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5345, + "end_id": 5351 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5390, + "end_id": 5424 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5533, + "end_id": 5537 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5772, + "end_id": 5783 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5932, + "end_id": 5935 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5948, + "end_id": 5951 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6047, + "end_id": 6059 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6090, + "end_id": 6093 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6260, + "end_id": 6270 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6301, + "end_id": 6303 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6349, + "end_id": 6353 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6371, + "end_id": 6374 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6440, + "end_id": 6443 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6507, + "end_id": 6511 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6607, + "end_id": 6609 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6649, + "end_id": 6651 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6855, + "end_id": 6857 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6862, + "end_id": 6873 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6878, + "end_id": 6881 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6902, + "end_id": 6905 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 7018, + "end_id": 7037 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 7131, + "end_id": 7137 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7371, + "end_id": 7374 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7398, + "end_id": 7400 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 7468, + "end_id": 7482 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 7503, + "end_id": 7524 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 7549, + "end_id": 7555 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7571, + "end_id": 7576 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 7637, + "end_id": 7652 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 7747, + "end_id": 7757 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 7801, + "end_id": 7805 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7839, + "end_id": 7842 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7897, + "end_id": 7899 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 7952, + "end_id": 7958 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 7973, + "end_id": 7985 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 8113, + "end_id": 8120 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 8163, + "end_id": 8166 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 8180, + "end_id": 8182 + } + ] + }, + { + "url": "EuroCup2016/d2cfef2da9f84237a6950c7f6659655c.mp4", + "total_frames": 6017, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 408, + "end_id": 414 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 420, + "end_id": 423 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 622, + "end_id": 654 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 704, + "end_id": 709 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 895, + "end_id": 900 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 970, + "end_id": 975 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1017, + "end_id": 1023 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1128, + "end_id": 1137 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1157, + "end_id": 1163 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1248, + "end_id": 1256 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1437, + "end_id": 1442 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 1464, + "end_id": 1495 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1538, + "end_id": 1542 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1590, + "end_id": 1594 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1668, + "end_id": 1674 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1699, + "end_id": 1705 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1759, + "end_id": 1766 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1828, + "end_id": 1833 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1845, + "end_id": 1849 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1930, + "end_id": 1936 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2017, + "end_id": 2023 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2077, + "end_id": 2082 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2224, + "end_id": 2228 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2354, + "end_id": 2360 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2402, + "end_id": 2406 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2453, + "end_id": 2458 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2568, + "end_id": 2571 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2594, + "end_id": 2608 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2631, + "end_id": 2640 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2728, + "end_id": 2740 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2881, + "end_id": 2885 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3125, + "end_id": 3129 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3162, + "end_id": 3165 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3206, + "end_id": 3224 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3270, + "end_id": 3276 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3303, + "end_id": 3309 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3392, + "end_id": 3398 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3476, + "end_id": 3483 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3664, + "end_id": 3671 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3695, + "end_id": 3704 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3749, + "end_id": 3755 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3806, + "end_id": 3812 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3827, + "end_id": 3833 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3842, + "end_id": 3847 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3922, + "end_id": 3928 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3964, + "end_id": 3977 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3985, + "end_id": 3989 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4260, + "end_id": 4265 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4291, + "end_id": 4303 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4348, + "end_id": 4354 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4395, + "end_id": 4401 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4433, + "end_id": 4438 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4486, + "end_id": 4491 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4609, + "end_id": 4616 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4762, + "end_id": 4767 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4816, + "end_id": 4821 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4966, + "end_id": 5006 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5208, + "end_id": 5213 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5328, + "end_id": 5333 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5422, + "end_id": 5430 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5558, + "end_id": 5573 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5630, + "end_id": 5637 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5666, + "end_id": 5702 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5733, + "end_id": 5740 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5795, + "end_id": 5802 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5854, + "end_id": 5859 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5998, + "end_id": 6004 + } + ] + }, + { + "url": "EuroCup2016/6994844c64b44c26b935cee9604bef0a.mp4", + "total_frames": 6363, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 434, + "end_id": 436 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 485, + "end_id": 489 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 530, + "end_id": 535 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 545, + "end_id": 549 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 585, + "end_id": 588 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 624, + "end_id": 631 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 652, + "end_id": 659 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 716, + "end_id": 718 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 765, + "end_id": 773 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 893, + "end_id": 895 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 996, + "end_id": 999 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1252, + "end_id": 1254 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1277, + "end_id": 1282 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1348, + "end_id": 1361 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1447, + "end_id": 1452 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1482, + "end_id": 1489 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1502, + "end_id": 1504 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1537, + "end_id": 1539 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1557, + "end_id": 1561 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1616, + "end_id": 1622 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1778, + "end_id": 1781 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1833, + "end_id": 1837 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1908, + "end_id": 1913 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1940, + "end_id": 1945 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2059, + "end_id": 2064 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2289, + "end_id": 2307 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2468, + "end_id": 2470 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2548, + "end_id": 2550 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2666, + "end_id": 2670 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2760, + "end_id": 2763 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2908, + "end_id": 2911 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2960, + "end_id": 2963 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3006, + "end_id": 3020 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3066, + "end_id": 3067 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3067, + "end_id": 3080 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3109, + "end_id": 3119 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3168, + "end_id": 3174 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3227, + "end_id": 3230 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3478, + "end_id": 3481 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3524, + "end_id": 3529 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3561, + "end_id": 3568 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3761, + "end_id": 3764 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3782, + "end_id": 3785 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3857, + "end_id": 3861 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3973, + "end_id": 3977 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3982, + "end_id": 4018 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4147, + "end_id": 4151 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4179, + "end_id": 4194 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4220, + "end_id": 4226 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4262, + "end_id": 4272 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4298, + "end_id": 4301 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4365, + "end_id": 4371 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4433, + "end_id": 4470 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4490, + "end_id": 4513 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4534, + "end_id": 4536 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4552, + "end_id": 4555 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4640, + "end_id": 4642 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4644, + "end_id": 4653 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4743, + "end_id": 4747 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4775, + "end_id": 4777 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4798, + "end_id": 4803 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4822, + "end_id": 4826 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4834, + "end_id": 4859 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4868, + "end_id": 4874 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4927, + "end_id": 4930 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4973, + "end_id": 4977 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5037, + "end_id": 5039 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5077, + "end_id": 5080 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5128, + "end_id": 5130 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5149, + "end_id": 5172 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5191, + "end_id": 5194 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5209, + "end_id": 5215 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5221, + "end_id": 5229 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5442, + "end_id": 5463 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5542, + "end_id": 5545 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5699, + "end_id": 5705 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5739, + "end_id": 5741 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5747, + "end_id": 5749 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5826, + "end_id": 5829 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5840, + "end_id": 5842 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5865, + "end_id": 5907 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5952, + "end_id": 5958 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5969, + "end_id": 5973 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6011, + "end_id": 6038 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6054, + "end_id": 6065 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6099, + "end_id": 6101 + } + ] + }, + { + "url": "EuroCup2016/22e89747689e4f7e83e3620620c93269.mp4", + "total_frames": 6532, + "actions": [ + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 707, + "end_id": 717 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 784, + "end_id": 790 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 813, + "end_id": 814 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 899, + "end_id": 903 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1084, + "end_id": 1096 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1113, + "end_id": 1115 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1418, + "end_id": 1432 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1456, + "end_id": 1460 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1534, + "end_id": 1538 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1681, + "end_id": 1684 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1818, + "end_id": 1821 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2538, + "end_id": 2542 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2653, + "end_id": 2657 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2696, + "end_id": 2698 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2737, + "end_id": 2746 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2778, + "end_id": 2786 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2811, + "end_id": 2818 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2840, + "end_id": 2846 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2881, + "end_id": 2884 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3132, + "end_id": 3141 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3240, + "end_id": 3244 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3449, + "end_id": 3455 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3520, + "end_id": 3522 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3798, + "end_id": 3804 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3818, + "end_id": 3823 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3887, + "end_id": 3890 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3918, + "end_id": 3922 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3923, + "end_id": 3932 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3958, + "end_id": 3963 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3968, + "end_id": 3970 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4378, + "end_id": 4383 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4523, + "end_id": 4531 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4681, + "end_id": 4691 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4893, + "end_id": 4899 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4932, + "end_id": 4935 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5062, + "end_id": 5079 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5098, + "end_id": 5100 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5112, + "end_id": 5116 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5155, + "end_id": 5168 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5225, + "end_id": 5236 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5459, + "end_id": 5473 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5500, + "end_id": 5508 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5693, + "end_id": 5700 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5710, + "end_id": 5719 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5726, + "end_id": 5728 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5821, + "end_id": 5822 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5828, + "end_id": 5830 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5889, + "end_id": 5902 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5979, + "end_id": 5983 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5994, + "end_id": 6039 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 6342, + "end_id": 6385 + } + ] + }, + { + "url": "EuroCup2016/0265731a0c6f4a9398c88db8e3d4a3bc.mp4", + "total_frames": 6020, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 445, + "end_id": 448 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 478, + "end_id": 491 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 567, + "end_id": 569 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 740, + "end_id": 743 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 766, + "end_id": 768 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 794, + "end_id": 799 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1032, + "end_id": 1039 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1133, + "end_id": 1144 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1172, + "end_id": 1176 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1620, + "end_id": 1633 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1692, + "end_id": 1716 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1759, + "end_id": 1767 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1780, + "end_id": 1789 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1800, + "end_id": 1808 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1824, + "end_id": 1855 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1905, + "end_id": 1909 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2023, + "end_id": 2026 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2067, + "end_id": 2076 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2096, + "end_id": 2099 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2288, + "end_id": 2291 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2383, + "end_id": 2395 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2421, + "end_id": 2423 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2447, + "end_id": 2464 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3090, + "end_id": 3095 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3194, + "end_id": 3195 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3265, + "end_id": 3268 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3278, + "end_id": 3280 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3294, + "end_id": 3298 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3333, + "end_id": 3344 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3390, + "end_id": 3392 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3492, + "end_id": 3510 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3867, + "end_id": 3880 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4178, + "end_id": 4202 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4214, + "end_id": 4215 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4227, + "end_id": 4233 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4261, + "end_id": 4268 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4375, + "end_id": 4378 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4468, + "end_id": 4470 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4498, + "end_id": 4500 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4629, + "end_id": 4632 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4686, + "end_id": 4689 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4848, + "end_id": 4861 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4927, + "end_id": 4929 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5072, + "end_id": 5075 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5162, + "end_id": 5181 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5357, + "end_id": 5369 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5408, + "end_id": 5421 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5494, + "end_id": 5497 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5515, + "end_id": 5517 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5568, + "end_id": 5584 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5674, + "end_id": 5696 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5789, + "end_id": 5792 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5891, + "end_id": 5895 + } + ] + }, + { + "url": "EuroCup2016/6cc7db52c5ef4e70b401a5e00d8dd67a.mp4", + "total_frames": 6157, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 354, + "end_id": 360 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 408, + "end_id": 411 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 447, + "end_id": 451 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 888, + "end_id": 902 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1026, + "end_id": 1037 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1081, + "end_id": 1084 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1108, + "end_id": 1112 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1173, + "end_id": 1182 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1360, + "end_id": 1363 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 1412, + "end_id": 1447 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1563, + "end_id": 1577 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1634, + "end_id": 1639 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1645, + "end_id": 1661 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1695, + "end_id": 1698 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1711, + "end_id": 1713 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1713, + "end_id": 1731 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1773, + "end_id": 1784 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1884, + "end_id": 1888 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1913, + "end_id": 1919 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1927, + "end_id": 1930 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1964, + "end_id": 1975 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2012, + "end_id": 2014 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2181, + "end_id": 2184 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2255, + "end_id": 2273 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2300, + "end_id": 2303 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2325, + "end_id": 2328 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2373, + "end_id": 2381 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2416, + "end_id": 2419 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2521, + "end_id": 2535 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2894, + "end_id": 2896 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3003, + "end_id": 3006 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3108, + "end_id": 3116 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3181, + "end_id": 3191 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3204, + "end_id": 3214 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3295, + "end_id": 3299 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3373, + "end_id": 3378 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3389, + "end_id": 3395 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3454, + "end_id": 3469 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3510, + "end_id": 3512 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3559, + "end_id": 3567 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3606, + "end_id": 3611 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3696, + "end_id": 3699 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3732, + "end_id": 3735 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3795, + "end_id": 3797 + }, + { + "label_ids": [ + 1, + 2 + ], + "label_names": [ + "进球", + "角球" + ], + "start_id": 3817, + "end_id": 3856 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4014, + "end_id": 4023 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4099, + "end_id": 4102 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4124, + "end_id": 4142 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4146, + "end_id": 4148 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4236, + "end_id": 4258 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4309, + "end_id": 4311 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4356, + "end_id": 4359 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4620, + "end_id": 4625 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4967, + "end_id": 4994 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5034, + "end_id": 5042 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5383, + "end_id": 5386 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5398, + "end_id": 5421 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5428, + "end_id": 5448 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5523, + "end_id": 5527 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5590, + "end_id": 5595 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5603, + "end_id": 5607 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5674, + "end_id": 5679 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5699, + "end_id": 5701 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5830, + "end_id": 5832 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5863, + "end_id": 5884 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5974, + "end_id": 5985 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6028, + "end_id": 6054 + } + ] + }, + { + "url": "EuroCup2016/4c5d9d9af4f044c4a68d134061dc264f.mp4", + "total_frames": 6365, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 515, + "end_id": 522 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 603, + "end_id": 612 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 619, + "end_id": 628 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 723, + "end_id": 746 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 955, + "end_id": 965 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 983, + "end_id": 992 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1046, + "end_id": 1050 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1376, + "end_id": 1389 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1429, + "end_id": 1437 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1447, + "end_id": 1456 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1634, + "end_id": 1640 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1711, + "end_id": 1719 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1867, + "end_id": 1872 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1926, + "end_id": 1932 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1978, + "end_id": 1984 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2272, + "end_id": 2285 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2413, + "end_id": 2432 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2452, + "end_id": 2455 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2480, + "end_id": 2491 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2530, + "end_id": 2535 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2538, + "end_id": 2544 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2745, + "end_id": 2752 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2810, + "end_id": 2816 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2818, + "end_id": 2822 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2956, + "end_id": 2960 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2960, + "end_id": 3002 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3086, + "end_id": 3090 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3093, + "end_id": 3097 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3282, + "end_id": 3294 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3305, + "end_id": 3325 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3379, + "end_id": 3383 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3443, + "end_id": 3447 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3451, + "end_id": 3455 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3487, + "end_id": 3491 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3518, + "end_id": 3526 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3618, + "end_id": 3624 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3680, + "end_id": 3685 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3702, + "end_id": 3713 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3799, + "end_id": 3806 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3903, + "end_id": 3938 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3960, + "end_id": 3970 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4017, + "end_id": 4031 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4174, + "end_id": 4179 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4223, + "end_id": 4242 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4245, + "end_id": 4250 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4258, + "end_id": 4263 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4307, + "end_id": 4313 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4426, + "end_id": 4440 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4562, + "end_id": 4566 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4590, + "end_id": 4596 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4637, + "end_id": 4645 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4732, + "end_id": 4747 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4830, + "end_id": 4834 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4911, + "end_id": 4921 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5034, + "end_id": 5041 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5148, + "end_id": 5153 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5244, + "end_id": 5248 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5315, + "end_id": 5328 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5495, + "end_id": 5509 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5633, + "end_id": 5639 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5681, + "end_id": 5698 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5759, + "end_id": 5773 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6009, + "end_id": 6028 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6083, + "end_id": 6088 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6103, + "end_id": 6119 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6151, + "end_id": 6157 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6157, + "end_id": 6166 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6243, + "end_id": 6248 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6279, + "end_id": 6284 + } + ] + }, + { + "url": "EuroCup2016/6bca62b57cc449c6935f0b17f28d06be.mp4", + "total_frames": 6052, + "actions": [ + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 460, + "end_id": 469 + }, + { + "label_ids": [ + 1, + 2 + ], + "label_names": [ + "进球", + "角球" + ], + "start_id": 614, + "end_id": 645 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 813, + "end_id": 820 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 865, + "end_id": 871 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1122, + "end_id": 1135 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1203, + "end_id": 1210 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1307, + "end_id": 1315 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1476, + "end_id": 1484 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1603, + "end_id": 1609 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1636, + "end_id": 1650 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1684, + "end_id": 1688 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1695, + "end_id": 1711 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2015, + "end_id": 2021 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2041, + "end_id": 2049 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2058, + "end_id": 2064 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2087, + "end_id": 2094 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2437, + "end_id": 2441 + }, + { + "label_ids": [ + 5 + ], + "label_names": [ + "红牌" + ], + "start_id": 2496, + "end_id": 2524 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2599, + "end_id": 2605 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2970, + "end_id": 2977 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3018, + "end_id": 3026 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3346, + "end_id": 3351 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3584, + "end_id": 3588 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3805, + "end_id": 3813 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4103, + "end_id": 4135 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4136, + "end_id": 4165 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4204, + "end_id": 4217 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4272, + "end_id": 4276 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4331, + "end_id": 4335 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4623, + "end_id": 4628 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4820, + "end_id": 4852 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5505, + "end_id": 5511 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5736, + "end_id": 5747 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5927, + "end_id": 5944 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6009, + "end_id": 6013 + } + ] + }, + { + "url": "EuroCup2016/ea16ad2a020643529e257bd6cb11b3c3.mp4", + "total_frames": 6212, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 715, + "end_id": 720 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 781, + "end_id": 787 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1041, + "end_id": 1048 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1088, + "end_id": 1100 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 1114, + "end_id": 1147 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1239, + "end_id": 1252 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1310, + "end_id": 1317 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1390, + "end_id": 1394 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1552, + "end_id": 1560 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1592, + "end_id": 1597 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1607, + "end_id": 1618 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1625, + "end_id": 1629 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 1665, + "end_id": 1696 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1742, + "end_id": 1749 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1928, + "end_id": 1941 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1982, + "end_id": 1996 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2002, + "end_id": 2009 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2082, + "end_id": 2098 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2159, + "end_id": 2166 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2202, + "end_id": 2205 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2436, + "end_id": 2496 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2505, + "end_id": 2517 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2578, + "end_id": 2586 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2588, + "end_id": 2599 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2817, + "end_id": 2822 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2980, + "end_id": 2986 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3059, + "end_id": 3064 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3106, + "end_id": 3113 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3169, + "end_id": 3177 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3338, + "end_id": 3353 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3362, + "end_id": 3367 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3597, + "end_id": 3608 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3668, + "end_id": 3684 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3909, + "end_id": 3921 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4076, + "end_id": 4084 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4146, + "end_id": 4151 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4152, + "end_id": 4156 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4397, + "end_id": 4402 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4425, + "end_id": 4440 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4496, + "end_id": 4501 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4578, + "end_id": 4584 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4715, + "end_id": 4721 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4751, + "end_id": 4760 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4992, + "end_id": 4999 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5117, + "end_id": 5157 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5191, + "end_id": 5199 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5288, + "end_id": 5299 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5555, + "end_id": 5572 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5595, + "end_id": 5605 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5666, + "end_id": 5676 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5791, + "end_id": 5799 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5891, + "end_id": 5901 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6058, + "end_id": 6076 + } + ] + }, + { + "url": "EuroCup2016/745db97a080d4f44b450dc17a2bcf069.mp4", + "total_frames": 6289, + "actions": [ + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 660, + "end_id": 665 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 681, + "end_id": 692 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 720, + "end_id": 723 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 757, + "end_id": 760 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 790, + "end_id": 793 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 860, + "end_id": 866 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 970, + "end_id": 973 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1015, + "end_id": 1018 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1220, + "end_id": 1225 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1258, + "end_id": 1261 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1273, + "end_id": 1276 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1308, + "end_id": 1316 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1334, + "end_id": 1338 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1413, + "end_id": 1418 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1448, + "end_id": 1451 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1459, + "end_id": 1462 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1476, + "end_id": 1481 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1614, + "end_id": 1616 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1630, + "end_id": 1635 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1716, + "end_id": 1723 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1806, + "end_id": 1808 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2109, + "end_id": 2110 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2300, + "end_id": 2302 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2378, + "end_id": 2417 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2472, + "end_id": 2477 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2555, + "end_id": 2559 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2648, + "end_id": 2651 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2902, + "end_id": 2913 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2919, + "end_id": 2922 + }, + { + "label_ids": [ + 1, + 2 + ], + "label_names": [ + "进球", + "角球" + ], + "start_id": 3155, + "end_id": 3195 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3311, + "end_id": 3325 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3344, + "end_id": 3349 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3491, + "end_id": 3495 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3934, + "end_id": 3937 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4025, + "end_id": 4028 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4131, + "end_id": 4135 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4243, + "end_id": 4247 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4273, + "end_id": 4289 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4392, + "end_id": 4398 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4405, + "end_id": 4411 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4584, + "end_id": 4594 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4682, + "end_id": 4691 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4890, + "end_id": 4920 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5090, + "end_id": 5102 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5106, + "end_id": 5115 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5122, + "end_id": 5126 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5203, + "end_id": 5206 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5252, + "end_id": 5254 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5341, + "end_id": 5350 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5372, + "end_id": 5391 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5530, + "end_id": 5548 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5697, + "end_id": 5705 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5731, + "end_id": 5735 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5844, + "end_id": 5846 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5859, + "end_id": 5866 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5878, + "end_id": 5881 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5895, + "end_id": 5899 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6006, + "end_id": 6011 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6105, + "end_id": 6107 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6157, + "end_id": 6165 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6182, + "end_id": 6187 + } + ] + }, + { + "url": "EuroCup2016/02d2de09997f4215b06e3b00ff0502a0.mp4", + "total_frames": 6210, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 551, + "end_id": 557 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 598, + "end_id": 605 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 773, + "end_id": 779 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 791, + "end_id": 797 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 840, + "end_id": 845 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 975, + "end_id": 1018 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1053, + "end_id": 1063 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1224, + "end_id": 1228 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1745, + "end_id": 1751 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1834, + "end_id": 1838 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1899, + "end_id": 1912 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2020, + "end_id": 2027 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2138, + "end_id": 2141 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2228, + "end_id": 2244 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2320, + "end_id": 2329 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2412, + "end_id": 2423 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2932, + "end_id": 2937 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3028, + "end_id": 3033 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3056, + "end_id": 3063 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3260, + "end_id": 3264 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3498, + "end_id": 3505 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3524, + "end_id": 3537 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3592, + "end_id": 3600 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3633, + "end_id": 3641 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3919, + "end_id": 3925 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4054, + "end_id": 4067 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4074, + "end_id": 4085 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4282, + "end_id": 4290 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4398, + "end_id": 4402 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4603, + "end_id": 4616 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4727, + "end_id": 4758 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4765, + "end_id": 4769 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4777, + "end_id": 4782 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4806, + "end_id": 4812 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4824, + "end_id": 4829 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5161, + "end_id": 5175 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5204, + "end_id": 5211 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5274, + "end_id": 5280 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5291, + "end_id": 5310 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5349, + "end_id": 5378 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5443, + "end_id": 5456 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5748, + "end_id": 5757 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5776, + "end_id": 5801 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6007, + "end_id": 6024 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6067, + "end_id": 6070 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6077, + "end_id": 6081 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6107, + "end_id": 6111 + } + ] + }, + { + "url": "EuroCup2016/e1982c90cdd74abaacc4d0692070b400.mp4", + "total_frames": 6247, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 435, + "end_id": 438 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 498, + "end_id": 500 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 517, + "end_id": 520 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 534, + "end_id": 537 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 690, + "end_id": 693 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 934, + "end_id": 937 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1042, + "end_id": 1044 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1076, + "end_id": 1079 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1272, + "end_id": 1286 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1622, + "end_id": 1623 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1642, + "end_id": 1645 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1739, + "end_id": 1750 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1786, + "end_id": 1797 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1987, + "end_id": 2005 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2090, + "end_id": 2094 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2198, + "end_id": 2200 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2325, + "end_id": 2337 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2420, + "end_id": 2423 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2516, + "end_id": 2524 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2648, + "end_id": 2659 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2882, + "end_id": 2890 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3069, + "end_id": 3072 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3255, + "end_id": 3259 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3265, + "end_id": 3269 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3331, + "end_id": 3333 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3522, + "end_id": 3537 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3565, + "end_id": 3571 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3577, + "end_id": 3580 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3664, + "end_id": 3670 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3803, + "end_id": 3808 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3846, + "end_id": 3873 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3940, + "end_id": 3946 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4042, + "end_id": 4062 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4075, + "end_id": 4079 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4132, + "end_id": 4140 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4312, + "end_id": 4315 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4418, + "end_id": 4429 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4529, + "end_id": 4533 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4606, + "end_id": 4609 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4939, + "end_id": 4945 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4966, + "end_id": 4980 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5002, + "end_id": 5009 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5072, + "end_id": 5074 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5124, + "end_id": 5134 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5191, + "end_id": 5196 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5246, + "end_id": 5250 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5277, + "end_id": 5285 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5346, + "end_id": 5354 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5360, + "end_id": 5362 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5545, + "end_id": 5552 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5618, + "end_id": 5630 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5686, + "end_id": 5687 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5810, + "end_id": 5813 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5893, + "end_id": 5895 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5949, + "end_id": 5951 + } + ] + }, + { + "url": "EuroCup2016/bab63a9bcf204e4b99c4a887a01bfd60.mp4", + "total_frames": 6002, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 484, + "end_id": 488 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 656, + "end_id": 659 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 769, + "end_id": 782 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1004, + "end_id": 1012 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1050, + "end_id": 1055 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1099, + "end_id": 1110 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1321, + "end_id": 1331 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1401, + "end_id": 1404 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1428, + "end_id": 1433 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1483, + "end_id": 1492 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1506, + "end_id": 1511 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1718, + "end_id": 1721 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1759, + "end_id": 1770 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1781, + "end_id": 1804 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1878, + "end_id": 1885 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1964, + "end_id": 1970 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2300, + "end_id": 2309 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2324, + "end_id": 2328 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2384, + "end_id": 2387 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2449, + "end_id": 2453 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2465, + "end_id": 2469 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2524, + "end_id": 2543 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2597, + "end_id": 2600 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2637, + "end_id": 2641 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2720, + "end_id": 2723 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2802, + "end_id": 2806 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2927, + "end_id": 2935 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3491, + "end_id": 3496 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3516, + "end_id": 3520 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3575, + "end_id": 3587 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3677, + "end_id": 3684 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3857, + "end_id": 3866 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3889, + "end_id": 3901 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3915, + "end_id": 3926 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3970, + "end_id": 3976 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3996, + "end_id": 4002 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4081, + "end_id": 4088 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4181, + "end_id": 4187 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4280, + "end_id": 4283 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4317, + "end_id": 4329 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4410, + "end_id": 4413 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4582, + "end_id": 4589 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4703, + "end_id": 4713 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 4765, + "end_id": 4801 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4987, + "end_id": 4996 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5001, + "end_id": 5026 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5144, + "end_id": 5149 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5412, + "end_id": 5417 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5458, + "end_id": 5465 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5577, + "end_id": 5588 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5701, + "end_id": 5710 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5728, + "end_id": 5732 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5843, + "end_id": 5860 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5870, + "end_id": 5892 + } + ] + }, + { + "url": "EuroCup2016/6630aaf0e32146088d0b624e9288f071.mp4", + "total_frames": 6174, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 414, + "end_id": 417 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 540, + "end_id": 548 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 651, + "end_id": 663 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 686, + "end_id": 689 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 958, + "end_id": 966 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1023, + "end_id": 1026 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1059, + "end_id": 1063 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1294, + "end_id": 1297 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1436, + "end_id": 1442 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1490, + "end_id": 1496 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1546, + "end_id": 1552 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1602, + "end_id": 1605 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1663, + "end_id": 1671 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1817, + "end_id": 1819 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1878, + "end_id": 1888 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2009, + "end_id": 2019 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2080, + "end_id": 2081 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2107, + "end_id": 2110 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2154, + "end_id": 2159 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2164, + "end_id": 2168 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2176, + "end_id": 2178 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2327, + "end_id": 2334 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2363, + "end_id": 2366 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2379, + "end_id": 2386 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2474, + "end_id": 2481 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2657, + "end_id": 2660 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2763, + "end_id": 2766 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 2842, + "end_id": 2881 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3121, + "end_id": 3125 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3259, + "end_id": 3262 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3419, + "end_id": 3424 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3447, + "end_id": 3452 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3467, + "end_id": 3469 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3785, + "end_id": 3795 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3808, + "end_id": 3844 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4066, + "end_id": 4072 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4125, + "end_id": 4138 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4170, + "end_id": 4182 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4323, + "end_id": 4329 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4415, + "end_id": 4421 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4425, + "end_id": 4428 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4465, + "end_id": 4494 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4534, + "end_id": 4538 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4545, + "end_id": 4549 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4563, + "end_id": 4568 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4737, + "end_id": 4763 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4798, + "end_id": 4801 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4839, + "end_id": 4859 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5048, + "end_id": 5053 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5088, + "end_id": 5093 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5464, + "end_id": 5472 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5502, + "end_id": 5504 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5804, + "end_id": 5806 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5840, + "end_id": 5845 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5913, + "end_id": 5916 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5944, + "end_id": 5991 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6056, + "end_id": 6059 + } + ] + }, + { + "url": "EuroCup2016/3e23d452a082403391f8abfb87bf2fb4.mp4", + "total_frames": 6306, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 381, + "end_id": 389 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 409, + "end_id": 414 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 437, + "end_id": 451 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 557, + "end_id": 559 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 570, + "end_id": 575 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 665, + "end_id": 670 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 681, + "end_id": 687 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 802, + "end_id": 805 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 916, + "end_id": 920 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1004, + "end_id": 1010 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1016, + "end_id": 1019 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1028, + "end_id": 1031 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1208, + "end_id": 1213 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1228, + "end_id": 1232 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1292, + "end_id": 1295 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1470, + "end_id": 1482 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1501, + "end_id": 1504 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1583, + "end_id": 1589 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1676, + "end_id": 1683 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1866, + "end_id": 1873 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1977, + "end_id": 1983 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2042, + "end_id": 2047 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2129, + "end_id": 2133 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2149, + "end_id": 2153 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2240, + "end_id": 2246 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2364, + "end_id": 2380 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2578, + "end_id": 2585 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2630, + "end_id": 2634 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2705, + "end_id": 2717 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2761, + "end_id": 2770 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2792, + "end_id": 2797 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2899, + "end_id": 2906 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2921, + "end_id": 2924 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3154, + "end_id": 3157 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3172, + "end_id": 3176 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3181, + "end_id": 3189 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3243, + "end_id": 3247 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3272, + "end_id": 3277 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 3334, + "end_id": 3368 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3441, + "end_id": 3447 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3499, + "end_id": 3503 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3542, + "end_id": 3545 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3623, + "end_id": 3629 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3706, + "end_id": 3709 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3732, + "end_id": 3735 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3762, + "end_id": 3765 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4110, + "end_id": 4114 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4122, + "end_id": 4127 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4140, + "end_id": 4143 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4198, + "end_id": 4211 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4238, + "end_id": 4240 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4416, + "end_id": 4429 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4539, + "end_id": 4567 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4579, + "end_id": 4584 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4922, + "end_id": 4925 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4937, + "end_id": 4950 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5208, + "end_id": 5215 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5402, + "end_id": 5415 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5422, + "end_id": 5432 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5592, + "end_id": 5601 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5624, + "end_id": 5628 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5723, + "end_id": 5726 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5752, + "end_id": 5756 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5932, + "end_id": 5936 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5978, + "end_id": 5988 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6057, + "end_id": 6073 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 6176, + "end_id": 6223 + } + ] + }, + { + "url": "EuroCup2016/8962ac5a332346e180c79d701ae0a175.mp4", + "total_frames": 6100, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 368, + "end_id": 374 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 389, + "end_id": 394 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 431, + "end_id": 433 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 475, + "end_id": 492 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 497, + "end_id": 503 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 593, + "end_id": 605 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 783, + "end_id": 788 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 902, + "end_id": 907 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1122, + "end_id": 1128 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1149, + "end_id": 1155 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1541, + "end_id": 1546 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1609, + "end_id": 1615 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1632, + "end_id": 1638 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1671, + "end_id": 1682 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1806, + "end_id": 1815 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1818, + "end_id": 1832 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1847, + "end_id": 1849 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2028, + "end_id": 2032 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2052, + "end_id": 2056 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2104, + "end_id": 2110 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2142, + "end_id": 2145 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2155, + "end_id": 2157 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2188, + "end_id": 2193 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2240, + "end_id": 2246 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2257, + "end_id": 2259 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2283, + "end_id": 2286 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2350, + "end_id": 2355 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2507, + "end_id": 2512 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2555, + "end_id": 2576 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2628, + "end_id": 2635 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2746, + "end_id": 2752 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2953, + "end_id": 2959 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3044, + "end_id": 3064 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3151, + "end_id": 3158 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3222, + "end_id": 3225 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3406, + "end_id": 3409 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3574, + "end_id": 3584 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3610, + "end_id": 3615 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3618, + "end_id": 3638 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3790, + "end_id": 3791 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3807, + "end_id": 3811 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3824, + "end_id": 3826 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3974, + "end_id": 3980 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3980, + "end_id": 3992 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4306, + "end_id": 4313 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4401, + "end_id": 4407 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4568, + "end_id": 4576 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4614, + "end_id": 4645 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4652, + "end_id": 4657 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4754, + "end_id": 4767 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4836, + "end_id": 4845 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5012, + "end_id": 5015 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5120, + "end_id": 5126 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5300, + "end_id": 5306 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5409, + "end_id": 5414 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5748, + "end_id": 5753 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5893, + "end_id": 5901 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5943, + "end_id": 5961 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5999, + "end_id": 6002 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6017, + "end_id": 6021 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6022, + "end_id": 6028 + } + ] + }, + { + "url": "EuroCup2016/5933d0ce17854483b81a318d7d45a34e.mp4", + "total_frames": 6102, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 379, + "end_id": 386 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 406, + "end_id": 415 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 448, + "end_id": 460 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 537, + "end_id": 546 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 624, + "end_id": 628 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 657, + "end_id": 660 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 751, + "end_id": 762 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 941, + "end_id": 959 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 999, + "end_id": 1008 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1242, + "end_id": 1253 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1463, + "end_id": 1473 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1540, + "end_id": 1544 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1870, + "end_id": 1873 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2099, + "end_id": 2129 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2209, + "end_id": 2219 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2239, + "end_id": 2246 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2388, + "end_id": 2392 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2435, + "end_id": 2452 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2524, + "end_id": 2527 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2535, + "end_id": 2538 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2628, + "end_id": 2631 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2692, + "end_id": 2700 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2709, + "end_id": 2721 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2828, + "end_id": 2834 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3188, + "end_id": 3195 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3302, + "end_id": 3303 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3329, + "end_id": 3331 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3394, + "end_id": 3396 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3459, + "end_id": 3466 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3687, + "end_id": 3718 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3727, + "end_id": 3734 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3940, + "end_id": 3944 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4016, + "end_id": 4023 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4051, + "end_id": 4062 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4221, + "end_id": 4228 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4243, + "end_id": 4251 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4313, + "end_id": 4316 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4344, + "end_id": 4352 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4519, + "end_id": 4531 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4574, + "end_id": 4595 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4662, + "end_id": 4673 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4934, + "end_id": 4950 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5139, + "end_id": 5143 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5150, + "end_id": 5154 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5244, + "end_id": 5251 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5326, + "end_id": 5331 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5367, + "end_id": 5372 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5426, + "end_id": 5428 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5514, + "end_id": 5521 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5582, + "end_id": 5594 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5785, + "end_id": 5799 + } + ] + }, + { + "url": "EuroCup2016/45d08bc5cb0f424f9ed9d7874eb561cd.mp4", + "total_frames": 6124, + "actions": [ + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 635, + "end_id": 643 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 771, + "end_id": 775 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 881, + "end_id": 886 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 965, + "end_id": 974 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1030, + "end_id": 1043 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1115, + "end_id": 1121 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1208, + "end_id": 1213 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1316, + "end_id": 1326 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 1485, + "end_id": 1505 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1778, + "end_id": 1786 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1895, + "end_id": 1902 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1928, + "end_id": 1936 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1969, + "end_id": 1977 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2081, + "end_id": 2089 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2194, + "end_id": 2203 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2267, + "end_id": 2276 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2295, + "end_id": 2302 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2323, + "end_id": 2325 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2631, + "end_id": 2663 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2836, + "end_id": 2842 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2864, + "end_id": 2881 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2890, + "end_id": 2893 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3062, + "end_id": 3070 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3143, + "end_id": 3150 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3257, + "end_id": 3266 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3375, + "end_id": 3379 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3678, + "end_id": 3682 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3916, + "end_id": 3922 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3985, + "end_id": 3998 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4060, + "end_id": 4065 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4162, + "end_id": 4165 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4282, + "end_id": 4293 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4436, + "end_id": 4461 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4473, + "end_id": 4479 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4518, + "end_id": 4529 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4534, + "end_id": 4544 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4861, + "end_id": 4865 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4887, + "end_id": 4895 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4906, + "end_id": 4912 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5028, + "end_id": 5047 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5151, + "end_id": 5179 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5188, + "end_id": 5193 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5319, + "end_id": 5329 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5349, + "end_id": 5358 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5460, + "end_id": 5465 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5704, + "end_id": 5713 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5810, + "end_id": 5817 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5853, + "end_id": 5864 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5987, + "end_id": 6001 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 6015, + "end_id": 6037 + } + ] + }, + { + "url": "EuroCup2016/299fe30d8f3b4a45b89313fe31f9f3c0.mp4", + "total_frames": 6056, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 439, + "end_id": 445 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 494, + "end_id": 508 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 509, + "end_id": 516 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 740, + "end_id": 743 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 869, + "end_id": 875 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 984, + "end_id": 988 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1106, + "end_id": 1112 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1209, + "end_id": 1211 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1265, + "end_id": 1268 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1372, + "end_id": 1376 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1634, + "end_id": 1637 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1704, + "end_id": 1710 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1789, + "end_id": 1791 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1869, + "end_id": 1873 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1888, + "end_id": 1890 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1904, + "end_id": 1907 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1967, + "end_id": 1972 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2005, + "end_id": 2009 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2053, + "end_id": 2057 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2131, + "end_id": 2133 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2150, + "end_id": 2153 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2232, + "end_id": 2235 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2291, + "end_id": 2293 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2327, + "end_id": 2330 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2357, + "end_id": 2372 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2668, + "end_id": 2672 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2789, + "end_id": 2794 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2893, + "end_id": 2903 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2946, + "end_id": 2949 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2960, + "end_id": 2963 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3030, + "end_id": 3051 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3201, + "end_id": 3211 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3230, + "end_id": 3233 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3361, + "end_id": 3364 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3406, + "end_id": 3407 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3421, + "end_id": 3423 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3526, + "end_id": 3528 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3550, + "end_id": 3553 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3609, + "end_id": 3612 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3669, + "end_id": 3677 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3687, + "end_id": 3697 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3799, + "end_id": 3801 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3874, + "end_id": 3882 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3941, + "end_id": 3949 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4050, + "end_id": 4052 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4082, + "end_id": 4089 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4109, + "end_id": 4111 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4311, + "end_id": 4321 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4385, + "end_id": 4400 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4538, + "end_id": 4546 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4555, + "end_id": 4557 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4717, + "end_id": 4721 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4830, + "end_id": 4835 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4886, + "end_id": 4890 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4914, + "end_id": 4942 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4963, + "end_id": 4967 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5007, + "end_id": 5009 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5173, + "end_id": 5194 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5296, + "end_id": 5298 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5325, + "end_id": 5327 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5370, + "end_id": 5372 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5393, + "end_id": 5398 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5438, + "end_id": 5443 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5547, + "end_id": 5557 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5591, + "end_id": 5613 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5624, + "end_id": 5631 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5678, + "end_id": 5682 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5830, + "end_id": 5833 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5918, + "end_id": 5921 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5941, + "end_id": 5959 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5993, + "end_id": 6002 + } + ] + }, + { + "url": "EuroCup2016/250b88724acf40dbb6d7e8ccb400ef38.mp4", + "total_frames": 6045, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 357, + "end_id": 359 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 397, + "end_id": 410 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 442, + "end_id": 445 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 498, + "end_id": 508 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 610, + "end_id": 616 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 670, + "end_id": 673 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 729, + "end_id": 731 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 834, + "end_id": 837 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1238, + "end_id": 1241 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1449, + "end_id": 1458 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1482, + "end_id": 1486 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1543, + "end_id": 1546 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1567, + "end_id": 1574 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1604, + "end_id": 1625 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1652, + "end_id": 1672 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1717, + "end_id": 1719 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1770, + "end_id": 1771 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1778, + "end_id": 1780 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1820, + "end_id": 1824 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1978, + "end_id": 1980 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2267, + "end_id": 2271 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2297, + "end_id": 2300 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2318, + "end_id": 2320 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2360, + "end_id": 2368 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2408, + "end_id": 2411 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2591, + "end_id": 2609 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2615, + "end_id": 2639 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2670, + "end_id": 2686 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2775, + "end_id": 2782 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2897, + "end_id": 2909 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3295, + "end_id": 3298 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3318, + "end_id": 3321 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3384, + "end_id": 3415 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3503, + "end_id": 3504 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3536, + "end_id": 3539 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3561, + "end_id": 3566 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3652, + "end_id": 3655 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3713, + "end_id": 3718 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3778, + "end_id": 3785 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3789, + "end_id": 3791 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4003, + "end_id": 4006 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4082, + "end_id": 4083 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4183, + "end_id": 4184 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4251, + "end_id": 4259 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4291, + "end_id": 4302 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4317, + "end_id": 4325 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4354, + "end_id": 4362 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4450, + "end_id": 4462 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4579, + "end_id": 4580 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4705, + "end_id": 4708 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4758, + "end_id": 4764 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4861, + "end_id": 4863 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4906, + "end_id": 4917 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4972, + "end_id": 4973 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5008, + "end_id": 5044 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5051, + "end_id": 5054 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5122, + "end_id": 5132 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5194, + "end_id": 5197 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5234, + "end_id": 5239 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5331, + "end_id": 5333 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5395, + "end_id": 5399 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5444, + "end_id": 5445 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5488, + "end_id": 5498 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5563, + "end_id": 5566 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5614, + "end_id": 5624 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5662, + "end_id": 5671 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5788, + "end_id": 5790 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5949, + "end_id": 5951 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5969, + "end_id": 5974 + } + ] + }, + { + "url": "EuroCup2016/4afbbf9afcd44dfea45b044117cccb48.mp4", + "total_frames": 6283, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 405, + "end_id": 408 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 436, + "end_id": 439 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 491, + "end_id": 494 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 651, + "end_id": 655 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 761, + "end_id": 789 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 859, + "end_id": 864 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 944, + "end_id": 947 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1041, + "end_id": 1044 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1109, + "end_id": 1116 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1186, + "end_id": 1188 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1289, + "end_id": 1303 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1458, + "end_id": 1464 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1489, + "end_id": 1494 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1890, + "end_id": 1896 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2033, + "end_id": 2053 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2102, + "end_id": 2114 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2209, + "end_id": 2214 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2280, + "end_id": 2287 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2329, + "end_id": 2337 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2432, + "end_id": 2435 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2884, + "end_id": 2890 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2912, + "end_id": 2916 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3035, + "end_id": 3066 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3524, + "end_id": 3526 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3686, + "end_id": 3690 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3847, + "end_id": 3854 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3889, + "end_id": 3895 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4064, + "end_id": 4082 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4091, + "end_id": 4095 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4144, + "end_id": 4147 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4299, + "end_id": 4304 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4483, + "end_id": 4489 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4498, + "end_id": 4503 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4514, + "end_id": 4522 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4687, + "end_id": 4708 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4736, + "end_id": 4752 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4964, + "end_id": 4973 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5050, + "end_id": 5052 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5077, + "end_id": 5078 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5218, + "end_id": 5223 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5383, + "end_id": 5410 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5513, + "end_id": 5555 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5561, + "end_id": 5567 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5574, + "end_id": 5576 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5607, + "end_id": 5610 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5721, + "end_id": 5750 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5833, + "end_id": 5838 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5854, + "end_id": 5871 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5876, + "end_id": 5880 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5994, + "end_id": 6041 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6081, + "end_id": 6085 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6113, + "end_id": 6157 + } + ] + }, + { + "url": "EuroCup2016/70cfc31e520840b2afca458f93a01ce4.mp4", + "total_frames": 414, + "actions": [ + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 96, + "end_id": 140 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 196, + "end_id": 203 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 306, + "end_id": 313 + } + ] + }, + { + "url": "EuroCup2016/4435b708af6d48519a6b726144147d51.mp4", + "total_frames": 6544, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 414, + "end_id": 420 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 438, + "end_id": 455 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 471, + "end_id": 500 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 581, + "end_id": 583 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 665, + "end_id": 681 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 748, + "end_id": 759 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 872, + "end_id": 874 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 895, + "end_id": 897 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 957, + "end_id": 959 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1038, + "end_id": 1042 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1103, + "end_id": 1105 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1181, + "end_id": 1187 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1218, + "end_id": 1224 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1265, + "end_id": 1267 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1333, + "end_id": 1336 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1379, + "end_id": 1385 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1466, + "end_id": 1468 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1763, + "end_id": 1777 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1800, + "end_id": 1801 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1834, + "end_id": 1840 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1953, + "end_id": 1964 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1984, + "end_id": 1993 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2000, + "end_id": 2009 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2034, + "end_id": 2046 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2322, + "end_id": 2324 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2356, + "end_id": 2358 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2468, + "end_id": 2475 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2481, + "end_id": 2483 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2509, + "end_id": 2514 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2578, + "end_id": 2608 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2745, + "end_id": 2748 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2833, + "end_id": 2845 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2904, + "end_id": 2907 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2986, + "end_id": 3003 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3072, + "end_id": 3074 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3290, + "end_id": 3294 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3401, + "end_id": 3404 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3431, + "end_id": 3434 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3518, + "end_id": 3520 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3590, + "end_id": 3594 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3599, + "end_id": 3603 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3610, + "end_id": 3612 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3692, + "end_id": 3694 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3788, + "end_id": 3791 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3813, + "end_id": 3816 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3940, + "end_id": 3942 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4015, + "end_id": 4061 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4129, + "end_id": 4130 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4169, + "end_id": 4197 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4327, + "end_id": 4338 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4346, + "end_id": 4347 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4483, + "end_id": 4501 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4759, + "end_id": 4767 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4814, + "end_id": 4821 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4897, + "end_id": 4908 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4998, + "end_id": 5017 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5218, + "end_id": 5221 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5254, + "end_id": 5260 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5316, + "end_id": 5330 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5528, + "end_id": 5534 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5584, + "end_id": 5596 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5605, + "end_id": 5617 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5936, + "end_id": 5939 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 6073, + "end_id": 6103 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6155, + "end_id": 6157 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6204, + "end_id": 6207 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6218, + "end_id": 6221 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6271, + "end_id": 6282 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6285, + "end_id": 6288 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6337, + "end_id": 6354 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6387, + "end_id": 6398 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6412, + "end_id": 6429 + } + ] + }, + { + "url": "EuroCup2016/63e51df254d2402fac703b6c4fdb4ea9.mp4", + "total_frames": 6189, + "actions": [ + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 393, + "end_id": 399 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 458, + "end_id": 461 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 473, + "end_id": 475 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 592, + "end_id": 597 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 649, + "end_id": 651 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 673, + "end_id": 675 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 814, + "end_id": 817 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 864, + "end_id": 868 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 880, + "end_id": 883 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 886, + "end_id": 888 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 911, + "end_id": 945 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1161, + "end_id": 1165 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1191, + "end_id": 1194 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1210, + "end_id": 1212 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1316, + "end_id": 1323 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1366, + "end_id": 1368 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1680, + "end_id": 1686 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1747, + "end_id": 1749 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1772, + "end_id": 1779 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1843, + "end_id": 1845 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1876, + "end_id": 1879 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2003, + "end_id": 2016 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2096, + "end_id": 2100 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2150, + "end_id": 2153 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2365, + "end_id": 2368 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2393, + "end_id": 2408 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2439, + "end_id": 2446 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2453, + "end_id": 2457 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2479, + "end_id": 2481 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2494, + "end_id": 2505 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2605, + "end_id": 2610 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2629, + "end_id": 2636 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2653, + "end_id": 2670 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2789, + "end_id": 2803 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3015, + "end_id": 3018 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3072, + "end_id": 3082 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3374, + "end_id": 3377 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3394, + "end_id": 3413 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3466, + "end_id": 3474 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3686, + "end_id": 3692 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3697, + "end_id": 3700 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3863, + "end_id": 3873 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3874, + "end_id": 3880 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3919, + "end_id": 3923 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4037, + "end_id": 4077 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4136, + "end_id": 4143 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4173, + "end_id": 4181 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 4315, + "end_id": 4358 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4554, + "end_id": 4563 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4581, + "end_id": 4596 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4676, + "end_id": 4687 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4772, + "end_id": 4775 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5121, + "end_id": 5129 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5197, + "end_id": 5204 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5233, + "end_id": 5237 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5326, + "end_id": 5329 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5387, + "end_id": 5401 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5471, + "end_id": 5474 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5531, + "end_id": 5538 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5654, + "end_id": 5660 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5667, + "end_id": 5683 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5727, + "end_id": 5734 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5813, + "end_id": 5825 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5839, + "end_id": 5864 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5982, + "end_id": 5996 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6064, + "end_id": 6069 + } + ] + }, + { + "url": "EuroCup2016/eeebffbd4ec74222a9c2d0775d79b689.mp4", + "total_frames": 5997, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 364, + "end_id": 367 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 433, + "end_id": 442 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 491, + "end_id": 494 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 524, + "end_id": 527 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 559, + "end_id": 572 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 720, + "end_id": 753 + }, + { + "label_ids": [ + 5 + ], + "label_names": [ + "红牌" + ], + "start_id": 847, + "end_id": 855 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 933, + "end_id": 943 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 999, + "end_id": 1008 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1303, + "end_id": 1311 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1703, + "end_id": 1706 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1901, + "end_id": 1909 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1963, + "end_id": 1969 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2165, + "end_id": 2169 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2182, + "end_id": 2186 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2371, + "end_id": 2400 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2522, + "end_id": 2555 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2680, + "end_id": 2686 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2735, + "end_id": 2744 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2778, + "end_id": 2786 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3061, + "end_id": 3067 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3180, + "end_id": 3183 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3211, + "end_id": 3214 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3317, + "end_id": 3338 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3631, + "end_id": 3642 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3653, + "end_id": 3664 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3686, + "end_id": 3689 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3735, + "end_id": 3747 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3920, + "end_id": 3925 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3940, + "end_id": 3943 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4121, + "end_id": 4128 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4157, + "end_id": 4168 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4258, + "end_id": 4283 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4295, + "end_id": 4307 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4321, + "end_id": 4325 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4474, + "end_id": 4478 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4555, + "end_id": 4566 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4620, + "end_id": 4624 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4632, + "end_id": 4651 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4659, + "end_id": 4681 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4788, + "end_id": 4800 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4919, + "end_id": 4921 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5025, + "end_id": 5030 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5057, + "end_id": 5059 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5272, + "end_id": 5303 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5353, + "end_id": 5361 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5411, + "end_id": 5413 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5555, + "end_id": 5559 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5610, + "end_id": 5614 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5857, + "end_id": 5863 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5946, + "end_id": 5949 + } + ] + }, + { + "url": "EuroCup2016/8ab7b0cba5744eb3b6fb10003dfda383.mp4", + "total_frames": 6056, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 436, + "end_id": 439 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 541, + "end_id": 551 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 571, + "end_id": 584 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 609, + "end_id": 611 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 664, + "end_id": 671 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 906, + "end_id": 909 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 968, + "end_id": 969 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1017, + "end_id": 1019 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1074, + "end_id": 1078 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1210, + "end_id": 1218 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1387, + "end_id": 1389 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1464, + "end_id": 1476 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1575, + "end_id": 1578 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1636, + "end_id": 1648 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1983, + "end_id": 1989 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2004, + "end_id": 2006 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2085, + "end_id": 2092 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2349, + "end_id": 2356 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2643, + "end_id": 2651 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2745, + "end_id": 2747 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3172, + "end_id": 3177 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3187, + "end_id": 3198 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3224, + "end_id": 3231 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3236, + "end_id": 3248 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3326, + "end_id": 3329 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3336, + "end_id": 3337 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3597, + "end_id": 3609 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3634, + "end_id": 3637 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3688, + "end_id": 3692 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3701, + "end_id": 3702 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3809, + "end_id": 3819 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4047, + "end_id": 4059 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4086, + "end_id": 4092 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4117, + "end_id": 4139 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4228, + "end_id": 4232 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4240, + "end_id": 4245 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4284, + "end_id": 4301 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4343, + "end_id": 4345 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4559, + "end_id": 4581 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4620, + "end_id": 4631 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4635, + "end_id": 4651 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4813, + "end_id": 4826 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4892, + "end_id": 4922 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5006, + "end_id": 5008 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5101, + "end_id": 5112 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5152, + "end_id": 5157 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5203, + "end_id": 5206 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5281, + "end_id": 5297 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5447, + "end_id": 5460 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5538, + "end_id": 5566 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5589, + "end_id": 5616 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5693, + "end_id": 5697 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5725, + "end_id": 5729 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5883, + "end_id": 5887 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5982, + "end_id": 5984 + } + ] + }, + { + "url": "EuroCup2016/c9516c903de3416c97dae91a59e968d7.mp4", + "total_frames": 6121, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 441, + "end_id": 443 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 633, + "end_id": 639 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 661, + "end_id": 665 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 719, + "end_id": 722 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 769, + "end_id": 778 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 792, + "end_id": 795 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 820, + "end_id": 827 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 854, + "end_id": 858 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1030, + "end_id": 1036 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1142, + "end_id": 1145 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1167, + "end_id": 1172 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1231, + "end_id": 1239 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1289, + "end_id": 1292 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1428, + "end_id": 1431 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1464, + "end_id": 1467 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1519, + "end_id": 1524 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1547, + "end_id": 1555 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1558, + "end_id": 1561 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1573, + "end_id": 1577 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1620, + "end_id": 1626 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1643, + "end_id": 1652 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1769, + "end_id": 1773 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1810, + "end_id": 1816 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1966, + "end_id": 1974 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2148, + "end_id": 2155 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2203, + "end_id": 2210 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2241, + "end_id": 2247 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2497, + "end_id": 2508 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2673, + "end_id": 2680 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2730, + "end_id": 2741 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2813, + "end_id": 2817 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3114, + "end_id": 3116 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3209, + "end_id": 3212 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3320, + "end_id": 3324 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3500, + "end_id": 3503 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3522, + "end_id": 3525 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3559, + "end_id": 3562 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3601, + "end_id": 3606 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3714, + "end_id": 3721 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3768, + "end_id": 3772 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3911, + "end_id": 3920 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3993, + "end_id": 4006 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4019, + "end_id": 4022 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4086, + "end_id": 4096 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4116, + "end_id": 4122 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4165, + "end_id": 4170 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4176, + "end_id": 4178 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4244, + "end_id": 4246 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4654, + "end_id": 4680 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4686, + "end_id": 4689 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4745, + "end_id": 4749 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4840, + "end_id": 4849 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4851, + "end_id": 4853 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4862, + "end_id": 4865 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4926, + "end_id": 4939 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4978, + "end_id": 4990 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5007, + "end_id": 5010 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5075, + "end_id": 5088 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5107, + "end_id": 5109 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5182, + "end_id": 5192 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5251, + "end_id": 5254 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5345, + "end_id": 5366 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5366, + "end_id": 5372 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5393, + "end_id": 5396 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5416, + "end_id": 5419 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5545, + "end_id": 5588 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5652, + "end_id": 5653 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5761, + "end_id": 5768 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5821, + "end_id": 5825 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5839, + "end_id": 5870 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5942, + "end_id": 5945 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5966, + "end_id": 5970 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5977, + "end_id": 5982 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6067, + "end_id": 6073 + } + ] + }, + { + "url": "EuroCup2016/f2edbee29c1b4966b3a410260f78fbe3.mp4", + "total_frames": 6160, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 460, + "end_id": 461 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 472, + "end_id": 475 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 629, + "end_id": 630 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 725, + "end_id": 741 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 752, + "end_id": 754 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 771, + "end_id": 772 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 935, + "end_id": 939 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1088, + "end_id": 1091 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1212, + "end_id": 1214 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1219, + "end_id": 1221 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1328, + "end_id": 1332 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1591, + "end_id": 1594 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1714, + "end_id": 1723 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1742, + "end_id": 1744 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1763, + "end_id": 1765 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1883, + "end_id": 1888 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2039, + "end_id": 2047 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2227, + "end_id": 2231 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2750, + "end_id": 2755 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2765, + "end_id": 2768 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2932, + "end_id": 2940 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3192, + "end_id": 3194 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3207, + "end_id": 3209 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3417, + "end_id": 3424 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3453, + "end_id": 3466 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3514, + "end_id": 3517 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3607, + "end_id": 3610 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3679, + "end_id": 3682 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3858, + "end_id": 3861 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3989, + "end_id": 4015 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4076, + "end_id": 4077 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4384, + "end_id": 4402 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4580, + "end_id": 4587 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4622, + "end_id": 4635 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4842, + "end_id": 4852 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4870, + "end_id": 4886 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5073, + "end_id": 5081 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5158, + "end_id": 5193 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5425, + "end_id": 5428 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5452, + "end_id": 5455 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5478, + "end_id": 5502 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5532, + "end_id": 5559 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5622, + "end_id": 5625 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5719, + "end_id": 5721 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5722, + "end_id": 5741 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5877, + "end_id": 5889 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5897, + "end_id": 5908 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5917, + "end_id": 5925 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5953, + "end_id": 5954 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5997, + "end_id": 6009 + } + ] + }, + { + "url": "EuroCup2016/2754615de6e64c4fb95ce1a8095dc1c1.mp4", + "total_frames": 6083, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 396, + "end_id": 404 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 412, + "end_id": 420 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 487, + "end_id": 492 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 530, + "end_id": 537 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 549, + "end_id": 553 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 642, + "end_id": 646 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 690, + "end_id": 705 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 766, + "end_id": 773 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1000, + "end_id": 1008 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1097, + "end_id": 1110 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1151, + "end_id": 1157 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1298, + "end_id": 1304 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1324, + "end_id": 1328 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1467, + "end_id": 1470 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1595, + "end_id": 1606 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1618, + "end_id": 1621 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1653, + "end_id": 1665 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1807, + "end_id": 1821 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1993, + "end_id": 1999 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2243, + "end_id": 2277 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2438, + "end_id": 2448 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2474, + "end_id": 2485 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2493, + "end_id": 2508 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2664, + "end_id": 2667 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2794, + "end_id": 2802 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2871, + "end_id": 2884 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3134, + "end_id": 3136 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3182, + "end_id": 3185 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3223, + "end_id": 3231 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3328, + "end_id": 3335 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3357, + "end_id": 3360 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3375, + "end_id": 3383 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3511, + "end_id": 3514 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3622, + "end_id": 3629 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3713, + "end_id": 3717 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3808, + "end_id": 3810 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3900, + "end_id": 3912 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3945, + "end_id": 3955 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4010, + "end_id": 4020 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4074, + "end_id": 4077 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4151, + "end_id": 4175 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4182, + "end_id": 4195 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4232, + "end_id": 4235 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4364, + "end_id": 4376 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4450, + "end_id": 4453 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4796, + "end_id": 4809 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4818, + "end_id": 4825 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4890, + "end_id": 4902 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4904, + "end_id": 4919 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4952, + "end_id": 4964 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5015, + "end_id": 5019 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5078, + "end_id": 5081 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5092, + "end_id": 5096 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5100, + "end_id": 5108 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5136, + "end_id": 5151 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5155, + "end_id": 5164 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5310, + "end_id": 5322 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5348, + "end_id": 5371 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5462, + "end_id": 5469 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5538, + "end_id": 5541 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5558, + "end_id": 5570 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5600, + "end_id": 5603 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5717, + "end_id": 5727 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5863, + "end_id": 5875 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5890, + "end_id": 5894 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5919, + "end_id": 5925 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5954, + "end_id": 5958 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5987, + "end_id": 6026 + } + ] + }, + { + "url": "EuroCup2016/2c8b5587083a4784a51622e4fec87ccd.mp4", + "total_frames": 6216, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 566, + "end_id": 580 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 672, + "end_id": 676 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 794, + "end_id": 799 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 898, + "end_id": 903 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 933, + "end_id": 940 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1043, + "end_id": 1053 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1123, + "end_id": 1127 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1196, + "end_id": 1204 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1347, + "end_id": 1353 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1361, + "end_id": 1366 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1458, + "end_id": 1464 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1508, + "end_id": 1516 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1565, + "end_id": 1579 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1599, + "end_id": 1611 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1654, + "end_id": 1663 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2005, + "end_id": 2011 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2184, + "end_id": 2191 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2206, + "end_id": 2215 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2607, + "end_id": 2617 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2867, + "end_id": 2871 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2942, + "end_id": 2951 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2961, + "end_id": 2977 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3018, + "end_id": 3026 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3107, + "end_id": 3113 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3140, + "end_id": 3149 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3262, + "end_id": 3268 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3335, + "end_id": 3344 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3462, + "end_id": 3474 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3661, + "end_id": 3667 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3932, + "end_id": 3942 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3965, + "end_id": 3969 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4013, + "end_id": 4017 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4054, + "end_id": 4066 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4164, + "end_id": 4197 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4346, + "end_id": 4357 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5199, + "end_id": 5226 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5232, + "end_id": 5246 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5430, + "end_id": 5440 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5602, + "end_id": 5608 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5725, + "end_id": 5733 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5960, + "end_id": 5963 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5981, + "end_id": 5988 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6040, + "end_id": 6044 + } + ] + }, + { + "url": "EuroCup2016/f24116fdd6a54214991db32f7dddef67.mp4", + "total_frames": 6101, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 380, + "end_id": 382 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 490, + "end_id": 498 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 614, + "end_id": 619 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 636, + "end_id": 639 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 688, + "end_id": 691 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 712, + "end_id": 716 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 736, + "end_id": 740 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 772, + "end_id": 775 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 851, + "end_id": 854 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 893, + "end_id": 896 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 974, + "end_id": 986 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1118, + "end_id": 1121 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1170, + "end_id": 1171 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1200, + "end_id": 1202 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1229, + "end_id": 1235 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1322, + "end_id": 1332 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1360, + "end_id": 1365 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1435, + "end_id": 1438 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1469, + "end_id": 1477 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1489, + "end_id": 1491 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1609, + "end_id": 1622 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2028, + "end_id": 2030 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2145, + "end_id": 2149 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2167, + "end_id": 2169 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2443, + "end_id": 2451 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2455, + "end_id": 2457 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2576, + "end_id": 2581 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2615, + "end_id": 2645 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2694, + "end_id": 2696 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2703, + "end_id": 2705 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2745, + "end_id": 2748 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2846, + "end_id": 2848 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2899, + "end_id": 2912 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 2998, + "end_id": 3045 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3099, + "end_id": 3102 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3217, + "end_id": 3219 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3276, + "end_id": 3279 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3285, + "end_id": 3287 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3299, + "end_id": 3338 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3416, + "end_id": 3432 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3507, + "end_id": 3518 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3544, + "end_id": 3554 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3567, + "end_id": 3572 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3593, + "end_id": 3597 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3649, + "end_id": 3666 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3710, + "end_id": 3714 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3967, + "end_id": 3990 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4018, + "end_id": 4021 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4081, + "end_id": 4088 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4108, + "end_id": 4111 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4112, + "end_id": 4125 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4206, + "end_id": 4208 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4240, + "end_id": 4265 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4305, + "end_id": 4306 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4315, + "end_id": 4317 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4525, + "end_id": 4528 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4591, + "end_id": 4597 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4636, + "end_id": 4638 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4667, + "end_id": 4675 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4725, + "end_id": 4764 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4880, + "end_id": 4886 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4972, + "end_id": 4991 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5076, + "end_id": 5096 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5129, + "end_id": 5143 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5174, + "end_id": 5183 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5198, + "end_id": 5203 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5428, + "end_id": 5436 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5538, + "end_id": 5547 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5589, + "end_id": 5602 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5607, + "end_id": 5611 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5642, + "end_id": 5644 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5769, + "end_id": 5776 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5804, + "end_id": 5807 + } + ] + }, + { + "url": "EuroCup2016/f6ed2b612b3d43baa0726be8b14ebe7c.mp4", + "total_frames": 6230, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 384, + "end_id": 387 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 487, + "end_id": 489 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 603, + "end_id": 617 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 657, + "end_id": 665 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 732, + "end_id": 747 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 808, + "end_id": 810 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 832, + "end_id": 842 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1032, + "end_id": 1037 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1109, + "end_id": 1122 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1333, + "end_id": 1341 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1419, + "end_id": 1422 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1637, + "end_id": 1642 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1983, + "end_id": 1993 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2011, + "end_id": 2015 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2064, + "end_id": 2070 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2144, + "end_id": 2155 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2190, + "end_id": 2198 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2279, + "end_id": 2282 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2292, + "end_id": 2302 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2347, + "end_id": 2361 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2383, + "end_id": 2394 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2503, + "end_id": 2515 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2683, + "end_id": 2689 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2794, + "end_id": 2804 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2844, + "end_id": 2849 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2966, + "end_id": 2969 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3002, + "end_id": 3011 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3178, + "end_id": 3187 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3221, + "end_id": 3229 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3293, + "end_id": 3309 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3340, + "end_id": 3344 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3383, + "end_id": 3396 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3413, + "end_id": 3424 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3442, + "end_id": 3446 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3936, + "end_id": 3957 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3991, + "end_id": 3994 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4030, + "end_id": 4047 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4205, + "end_id": 4220 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4342, + "end_id": 4364 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4404, + "end_id": 4424 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4432, + "end_id": 4444 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4461, + "end_id": 4465 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4541, + "end_id": 4545 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4569, + "end_id": 4586 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4597, + "end_id": 4604 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4635, + "end_id": 4673 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4675, + "end_id": 4679 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4943, + "end_id": 4950 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4963, + "end_id": 4977 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5009, + "end_id": 5016 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5091, + "end_id": 5103 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5248, + "end_id": 5260 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5348, + "end_id": 5367 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5376, + "end_id": 5387 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5428, + "end_id": 5441 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5455, + "end_id": 5495 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5763, + "end_id": 5773 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5774, + "end_id": 5778 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5880, + "end_id": 5889 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5902, + "end_id": 5909 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5943, + "end_id": 5948 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5980, + "end_id": 6024 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6031, + "end_id": 6040 + } + ] + }, + { + "url": "EuroCup2016/de23c0b2be3a4eb1990c5c657061fb29.mp4", + "total_frames": 6425, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 504, + "end_id": 507 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 604, + "end_id": 607 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 639, + "end_id": 641 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 649, + "end_id": 650 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 723, + "end_id": 726 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 766, + "end_id": 768 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 818, + "end_id": 820 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 957, + "end_id": 966 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 990, + "end_id": 996 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1017, + "end_id": 1023 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1136, + "end_id": 1144 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1310, + "end_id": 1315 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1473, + "end_id": 1477 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1583, + "end_id": 1595 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1602, + "end_id": 1610 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1675, + "end_id": 1695 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2066, + "end_id": 2069 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2221, + "end_id": 2226 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2374, + "end_id": 2377 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2388, + "end_id": 2395 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2492, + "end_id": 2510 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2579, + "end_id": 2592 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2734, + "end_id": 2747 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2816, + "end_id": 2845 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2936, + "end_id": 2963 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3099, + "end_id": 3101 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3192, + "end_id": 3195 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3201, + "end_id": 3204 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3308, + "end_id": 3312 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3544, + "end_id": 3554 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3628, + "end_id": 3634 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3713, + "end_id": 3716 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3730, + "end_id": 3732 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3842, + "end_id": 3852 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3942, + "end_id": 3950 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4000, + "end_id": 4014 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4213, + "end_id": 4218 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4232, + "end_id": 4237 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4244, + "end_id": 4248 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4277, + "end_id": 4279 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4303, + "end_id": 4306 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4459, + "end_id": 4497 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4507, + "end_id": 4516 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4559, + "end_id": 4565 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4588, + "end_id": 4593 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4688, + "end_id": 4714 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4724, + "end_id": 4730 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4779, + "end_id": 4786 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4788, + "end_id": 4792 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4808, + "end_id": 4812 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4852, + "end_id": 4869 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5007, + "end_id": 5025 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5077, + "end_id": 5085 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5134, + "end_id": 5156 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5183, + "end_id": 5198 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5256, + "end_id": 5265 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5380, + "end_id": 5382 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5390, + "end_id": 5411 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5431, + "end_id": 5447 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5515, + "end_id": 5541 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5558, + "end_id": 5572 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5589, + "end_id": 5591 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5607, + "end_id": 5620 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5639, + "end_id": 5640 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5687, + "end_id": 5689 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5789, + "end_id": 5830 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5984, + "end_id": 5986 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6008, + "end_id": 6026 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6101, + "end_id": 6103 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6240, + "end_id": 6259 + } + ] + }, + { + "url": "EuroCup2016/76b5f7ee28d942988c6b224bfac136bd.mp4", + "total_frames": 6170, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 582, + "end_id": 584 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 597, + "end_id": 599 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 647, + "end_id": 649 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 819, + "end_id": 821 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 834, + "end_id": 837 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1030, + "end_id": 1033 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1289, + "end_id": 1303 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1344, + "end_id": 1347 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1477, + "end_id": 1478 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 1479, + "end_id": 1509 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1578, + "end_id": 1580 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1614, + "end_id": 1618 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1720, + "end_id": 1723 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1738, + "end_id": 1741 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1745, + "end_id": 1748 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1853, + "end_id": 1857 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1934, + "end_id": 1937 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2008, + "end_id": 2010 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2140, + "end_id": 2142 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2344, + "end_id": 2346 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2370, + "end_id": 2372 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2518, + "end_id": 2526 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2529, + "end_id": 2548 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2794, + "end_id": 2804 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2896, + "end_id": 2901 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3012, + "end_id": 3015 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3068, + "end_id": 3072 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3189, + "end_id": 3192 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3285, + "end_id": 3296 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3523, + "end_id": 3527 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3558, + "end_id": 3562 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3608, + "end_id": 3611 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3887, + "end_id": 3892 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3920, + "end_id": 3924 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4039, + "end_id": 4069 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4277, + "end_id": 4279 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4470, + "end_id": 4473 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4638, + "end_id": 4665 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4683, + "end_id": 4699 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4717, + "end_id": 4719 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4902, + "end_id": 4909 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4981, + "end_id": 4982 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5085, + "end_id": 5098 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5111, + "end_id": 5116 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5187, + "end_id": 5207 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5212, + "end_id": 5236 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5251, + "end_id": 5258 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5276, + "end_id": 5281 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5357, + "end_id": 5379 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5426, + "end_id": 5428 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5497, + "end_id": 5503 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5591, + "end_id": 5622 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5650, + "end_id": 5660 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5710, + "end_id": 5712 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5802, + "end_id": 5829 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5832, + "end_id": 5834 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5890, + "end_id": 5895 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5908, + "end_id": 5911 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5914, + "end_id": 5917 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5987, + "end_id": 5988 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6022, + "end_id": 6028 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6032, + "end_id": 6035 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 6054, + "end_id": 6105 + } + ] + }, + { + "url": "EuroCup2016/6496960935e845578e391a5916739752.mp4", + "total_frames": 6263, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 398, + "end_id": 401 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 412, + "end_id": 414 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 421, + "end_id": 427 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 443, + "end_id": 458 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 538, + "end_id": 543 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 633, + "end_id": 656 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 697, + "end_id": 704 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 752, + "end_id": 767 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 800, + "end_id": 813 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 882, + "end_id": 887 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 938, + "end_id": 944 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 987, + "end_id": 1001 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1045, + "end_id": 1053 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1084, + "end_id": 1106 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1125, + "end_id": 1142 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1174, + "end_id": 1189 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1240, + "end_id": 1249 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1269, + "end_id": 1279 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1298, + "end_id": 1308 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1322, + "end_id": 1331 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1456, + "end_id": 1466 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 1467, + "end_id": 1500 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1560, + "end_id": 1562 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1650, + "end_id": 1658 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1683, + "end_id": 1701 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1777, + "end_id": 1783 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1804, + "end_id": 1810 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1992, + "end_id": 2020 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2033, + "end_id": 2049 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2063, + "end_id": 2077 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2088, + "end_id": 2094 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2176, + "end_id": 2179 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2200, + "end_id": 2208 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2212, + "end_id": 2216 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2377, + "end_id": 2388 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2406, + "end_id": 2433 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2535, + "end_id": 2542 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2657, + "end_id": 2660 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2698, + "end_id": 2703 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2784, + "end_id": 2794 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2858, + "end_id": 2880 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3089, + "end_id": 3098 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3188, + "end_id": 3192 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3228, + "end_id": 3234 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 3251, + "end_id": 3303 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3354, + "end_id": 3360 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3415, + "end_id": 3449 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3473, + "end_id": 3476 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3492, + "end_id": 3496 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3511, + "end_id": 3516 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3571, + "end_id": 3575 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3713, + "end_id": 3720 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3721, + "end_id": 3762 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3800, + "end_id": 3810 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3991, + "end_id": 3995 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4036, + "end_id": 4043 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4048, + "end_id": 4050 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4091, + "end_id": 4096 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4111, + "end_id": 4128 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4128, + "end_id": 4133 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4161, + "end_id": 4165 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4165, + "end_id": 4194 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4247, + "end_id": 4257 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4531, + "end_id": 4541 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4609, + "end_id": 4618 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4628, + "end_id": 4632 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4651, + "end_id": 4681 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4778, + "end_id": 4784 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4852, + "end_id": 4855 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4879, + "end_id": 4885 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5075, + "end_id": 5086 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5313, + "end_id": 5338 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5345, + "end_id": 5348 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5396, + "end_id": 5407 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5410, + "end_id": 5418 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5514, + "end_id": 5522 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5809, + "end_id": 5833 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6045, + "end_id": 6050 + } + ] + }, + { + "url": "EuroCup2016/2ceb6c549fc64305a06a75acb355642b.mp4", + "total_frames": 6360, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 577, + "end_id": 581 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1054, + "end_id": 1058 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1161, + "end_id": 1167 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1224, + "end_id": 1228 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1238, + "end_id": 1241 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1268, + "end_id": 1276 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1316, + "end_id": 1321 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1535, + "end_id": 1539 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1643, + "end_id": 1645 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2215, + "end_id": 2219 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2359, + "end_id": 2374 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2430, + "end_id": 2437 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2441, + "end_id": 2456 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2501, + "end_id": 2513 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2607, + "end_id": 2613 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2714, + "end_id": 2721 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2885, + "end_id": 2890 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3070, + "end_id": 3073 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3109, + "end_id": 3112 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3205, + "end_id": 3208 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3878, + "end_id": 3882 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3952, + "end_id": 3956 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4152, + "end_id": 4158 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4191, + "end_id": 4195 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4237, + "end_id": 4242 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4284, + "end_id": 4288 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4303, + "end_id": 4329 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4431, + "end_id": 4449 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4463, + "end_id": 4468 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4494, + "end_id": 4498 + }, + { + "label_ids": [ + 5 + ], + "label_names": [ + "红牌" + ], + "start_id": 4500, + "end_id": 4520 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4649, + "end_id": 4659 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4863, + "end_id": 4871 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5026, + "end_id": 5033 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5079, + "end_id": 5087 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5091, + "end_id": 5100 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5208, + "end_id": 5215 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5309, + "end_id": 5333 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5345, + "end_id": 5377 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5438, + "end_id": 5446 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5559, + "end_id": 5565 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5573, + "end_id": 5575 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5588, + "end_id": 5600 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5903, + "end_id": 5912 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5926, + "end_id": 5931 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5998, + "end_id": 6001 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6016, + "end_id": 6020 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6078, + "end_id": 6083 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6148, + "end_id": 6160 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6198, + "end_id": 6203 + } + ] + }, + { + "url": "EuroCup2016/737fdb054ca141f2a45013c1740dd0a0.mp4", + "total_frames": 6332, + "actions": [ + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 752, + "end_id": 763 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 790, + "end_id": 811 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 825, + "end_id": 836 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1024, + "end_id": 1027 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1040, + "end_id": 1049 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1094, + "end_id": 1105 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1218, + "end_id": 1225 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1314, + "end_id": 1317 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1466, + "end_id": 1470 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2018, + "end_id": 2027 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2074, + "end_id": 2078 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2222, + "end_id": 2225 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2272, + "end_id": 2280 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2310, + "end_id": 2336 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2390, + "end_id": 2393 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2424, + "end_id": 2439 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2458, + "end_id": 2476 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2494, + "end_id": 2504 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2661, + "end_id": 2667 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2683, + "end_id": 2690 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2715, + "end_id": 2722 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2761, + "end_id": 2770 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3007, + "end_id": 3010 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3144, + "end_id": 3164 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3400, + "end_id": 3403 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3437, + "end_id": 3452 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3497, + "end_id": 3500 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3540, + "end_id": 3576 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3676, + "end_id": 3684 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3693, + "end_id": 3696 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3740, + "end_id": 3752 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3767, + "end_id": 3773 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3795, + "end_id": 3803 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3815, + "end_id": 3824 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3946, + "end_id": 3955 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3989, + "end_id": 3997 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4012, + "end_id": 4015 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4069, + "end_id": 4077 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4225, + "end_id": 4232 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4382, + "end_id": 4385 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4399, + "end_id": 4401 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4456, + "end_id": 4460 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4775, + "end_id": 4783 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4966, + "end_id": 4974 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5001, + "end_id": 5009 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5197, + "end_id": 5215 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5219, + "end_id": 5229 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5242, + "end_id": 5258 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5288, + "end_id": 5291 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5354, + "end_id": 5360 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5408, + "end_id": 5417 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5424, + "end_id": 5437 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5521, + "end_id": 5524 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5723, + "end_id": 5729 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5781, + "end_id": 5785 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5852, + "end_id": 5866 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5925, + "end_id": 5938 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5983, + "end_id": 5987 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5990, + "end_id": 5995 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6046, + "end_id": 6053 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6108, + "end_id": 6112 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6172, + "end_id": 6175 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6175, + "end_id": 6182 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6223, + "end_id": 6231 + } + ] + }, + { + "url": "EuroCup2016/d0bd3eab1e794f0f9501c353a6d37827.mp4", + "total_frames": 6193, + "actions": [ + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 519, + "end_id": 527 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 556, + "end_id": 566 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 616, + "end_id": 618 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 666, + "end_id": 669 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 816, + "end_id": 818 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 868, + "end_id": 871 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 898, + "end_id": 916 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1120, + "end_id": 1122 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1172, + "end_id": 1173 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1220, + "end_id": 1226 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1438, + "end_id": 1441 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1558, + "end_id": 1564 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1654, + "end_id": 1661 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1952, + "end_id": 1958 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1996, + "end_id": 2000 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2172, + "end_id": 2182 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2264, + "end_id": 2284 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2392, + "end_id": 2394 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2420, + "end_id": 2423 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2487, + "end_id": 2494 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2607, + "end_id": 2609 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2673, + "end_id": 2688 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2756, + "end_id": 2758 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2800, + "end_id": 2818 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2871, + "end_id": 2889 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2902, + "end_id": 2904 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3000, + "end_id": 3003 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3042, + "end_id": 3046 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3084, + "end_id": 3087 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3214, + "end_id": 3217 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3271, + "end_id": 3273 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3285, + "end_id": 3290 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3299, + "end_id": 3309 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3379, + "end_id": 3395 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3413, + "end_id": 3417 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3462, + "end_id": 3464 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3624, + "end_id": 3630 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3666, + "end_id": 3668 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3728, + "end_id": 3733 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3818, + "end_id": 3825 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4071, + "end_id": 4094 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4140, + "end_id": 4152 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4177, + "end_id": 4183 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4235, + "end_id": 4237 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4333, + "end_id": 4348 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4367, + "end_id": 4373 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4417, + "end_id": 4420 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4702, + "end_id": 4729 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4770, + "end_id": 4772 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4881, + "end_id": 4890 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4914, + "end_id": 4916 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4958, + "end_id": 4959 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5131, + "end_id": 5151 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5281, + "end_id": 5283 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5367, + "end_id": 5369 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5428, + "end_id": 5430 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5462, + "end_id": 5471 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5539, + "end_id": 5565 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5608, + "end_id": 5632 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5642, + "end_id": 5665 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5741, + "end_id": 5743 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5756, + "end_id": 5760 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5787, + "end_id": 5799 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5871, + "end_id": 5873 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6001, + "end_id": 6003 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6091, + "end_id": 6094 + } + ] + }, + { + "url": "EuroCup2016/19eb47cc736240d6b2dd930ab69da839.mp4", + "total_frames": 8339, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 391, + "end_id": 397 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 407, + "end_id": 412 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 508, + "end_id": 511 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 528, + "end_id": 531 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 563, + "end_id": 565 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 937, + "end_id": 953 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 980, + "end_id": 987 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1275, + "end_id": 1283 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1335, + "end_id": 1337 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1624, + "end_id": 1629 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1690, + "end_id": 1703 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 1856, + "end_id": 1860 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1903, + "end_id": 1909 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2065, + "end_id": 2071 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2090, + "end_id": 2096 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2142, + "end_id": 2149 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2181, + "end_id": 2187 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2203, + "end_id": 2207 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2361, + "end_id": 2366 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2376, + "end_id": 2385 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2483, + "end_id": 2488 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2547, + "end_id": 2553 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2667, + "end_id": 2676 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2826, + "end_id": 2842 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2971, + "end_id": 2975 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3065, + "end_id": 3067 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3482, + "end_id": 3486 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3575, + "end_id": 3578 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3686, + "end_id": 3695 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3716, + "end_id": 3722 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3744, + "end_id": 3749 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3917, + "end_id": 3927 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3945, + "end_id": 3948 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3991, + "end_id": 4007 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4144, + "end_id": 4146 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4239, + "end_id": 4250 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4270, + "end_id": 4280 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4337, + "end_id": 4344 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4511, + "end_id": 4529 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4734, + "end_id": 4740 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4763, + "end_id": 4767 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4886, + "end_id": 4896 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5093, + "end_id": 5098 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5224, + "end_id": 5229 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5248, + "end_id": 5261 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5271, + "end_id": 5274 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5800, + "end_id": 5806 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5832, + "end_id": 5836 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6061, + "end_id": 6067 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6140, + "end_id": 6141 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6429, + "end_id": 6439 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6468, + "end_id": 6475 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6497, + "end_id": 6503 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6531, + "end_id": 6536 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6569, + "end_id": 6579 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6629, + "end_id": 6635 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6647, + "end_id": 6653 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6680, + "end_id": 6684 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6756, + "end_id": 6760 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6814, + "end_id": 6822 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6838, + "end_id": 6843 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6976, + "end_id": 6989 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 7299, + "end_id": 7305 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7330, + "end_id": 7333 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 7333, + "end_id": 7342 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 7385, + "end_id": 7391 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7425, + "end_id": 7437 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 7443, + "end_id": 7475 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 7523, + "end_id": 7526 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7535, + "end_id": 7536 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7543, + "end_id": 7549 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7635, + "end_id": 7637 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 7704, + "end_id": 7714 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 7780, + "end_id": 7792 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7995, + "end_id": 8000 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 8114, + "end_id": 8116 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 8127, + "end_id": 8132 + } + ] + }, + { + "url": "EuroCup2016/1be705a8f67648da8ec4b4296fa80895.mp4", + "total_frames": 6588, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 557, + "end_id": 563 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 592, + "end_id": 608 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 651, + "end_id": 655 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 792, + "end_id": 798 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 959, + "end_id": 961 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1043, + "end_id": 1048 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1127, + "end_id": 1132 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1160, + "end_id": 1167 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1338, + "end_id": 1342 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1630, + "end_id": 1653 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1912, + "end_id": 1928 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1992, + "end_id": 2002 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2044, + "end_id": 2056 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2386, + "end_id": 2388 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2671, + "end_id": 2679 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2842, + "end_id": 2857 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3002, + "end_id": 3008 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3020, + "end_id": 3028 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3183, + "end_id": 3192 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3193, + "end_id": 3208 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3269, + "end_id": 3296 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3529, + "end_id": 3542 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3568, + "end_id": 3577 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3629, + "end_id": 3632 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3653, + "end_id": 3658 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3680, + "end_id": 3686 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3796, + "end_id": 3799 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3860, + "end_id": 3864 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4450, + "end_id": 4467 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4506, + "end_id": 4509 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4610, + "end_id": 4613 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4654, + "end_id": 4670 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4779, + "end_id": 4783 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4805, + "end_id": 4811 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4916, + "end_id": 4922 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4934, + "end_id": 4961 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4991, + "end_id": 5020 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5180, + "end_id": 5187 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5232, + "end_id": 5240 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5309, + "end_id": 5340 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5383, + "end_id": 5398 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5424, + "end_id": 5436 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5494, + "end_id": 5498 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5544, + "end_id": 5558 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5599, + "end_id": 5604 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5665, + "end_id": 5668 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5708, + "end_id": 5711 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5930, + "end_id": 5933 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6140, + "end_id": 6171 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6206, + "end_id": 6217 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6305, + "end_id": 6308 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6333, + "end_id": 6336 + } + ] + }, + { + "url": "EuroCup2016/d6d25403a4bb4784aecff5f21fd00dc5.mp4", + "total_frames": 6214, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 640, + "end_id": 641 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 686, + "end_id": 688 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 729, + "end_id": 731 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 867, + "end_id": 873 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 882, + "end_id": 905 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 951, + "end_id": 954 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 967, + "end_id": 969 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 973, + "end_id": 977 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1184, + "end_id": 1186 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1219, + "end_id": 1221 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1543, + "end_id": 1550 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1659, + "end_id": 1663 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1812, + "end_id": 1818 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1863, + "end_id": 1866 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1937, + "end_id": 1943 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1961, + "end_id": 1963 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2227, + "end_id": 2229 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2537, + "end_id": 2540 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2568, + "end_id": 2570 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2794, + "end_id": 2797 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2938, + "end_id": 2946 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3034, + "end_id": 3037 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3368, + "end_id": 3372 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3421, + "end_id": 3429 + }, + { + "label_ids": [ + 1, + 2 + ], + "label_names": [ + "进球", + "角球" + ], + "start_id": 3446, + "end_id": 3488 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3576, + "end_id": 3583 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3588, + "end_id": 3593 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3602, + "end_id": 3606 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3622, + "end_id": 3627 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3636, + "end_id": 3677 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3758, + "end_id": 3770 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3906, + "end_id": 3926 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3986, + "end_id": 3989 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4148, + "end_id": 4162 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4214, + "end_id": 4220 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4241, + "end_id": 4248 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4316, + "end_id": 4325 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4410, + "end_id": 4421 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4669, + "end_id": 4675 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4705, + "end_id": 4711 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4712, + "end_id": 4726 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4769, + "end_id": 4788 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4803, + "end_id": 4811 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4864, + "end_id": 4908 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5131, + "end_id": 5138 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5176, + "end_id": 5211 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5214, + "end_id": 5216 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5321, + "end_id": 5324 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5375, + "end_id": 5378 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5441, + "end_id": 5450 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5498, + "end_id": 5501 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5621, + "end_id": 5643 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5651, + "end_id": 5653 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5680, + "end_id": 5685 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5733, + "end_id": 5752 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5848, + "end_id": 5853 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5994, + "end_id": 6002 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6019, + "end_id": 6021 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6029, + "end_id": 6033 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6044, + "end_id": 6047 + } + ] + } + ] +} diff --git a/applications/FootballAction/datasets/EuroCup2016/label_cls8_val.json b/applications/FootballAction/datasets/EuroCup2016/label_cls8_val.json new file mode 100644 index 0000000000000000000000000000000000000000..d21f105251fad6dd94a46e9bb9e16efa4a742588 --- /dev/null +++ b/applications/FootballAction/datasets/EuroCup2016/label_cls8_val.json @@ -0,0 +1,3161 @@ +{ + "fps": 5, + "gts": [ + { + "url": "EuroCup2016/5572686cb90f440988ded956a60e555d.mp4", + "total_frames": 6341, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 760, + "end_id": 763 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 820, + "end_id": 828 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 914, + "end_id": 920 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 966, + "end_id": 971 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 1282, + "end_id": 1313 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1475, + "end_id": 1488 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1634, + "end_id": 1637 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1921, + "end_id": 1929 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 1946, + "end_id": 1961 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2194, + "end_id": 2197 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2306, + "end_id": 2311 + }, + { + "label_ids": [ + 1, + 2 + ], + "label_names": [ + "进球", + "角球" + ], + "start_id": 2343, + "end_id": 2376 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2520, + "end_id": 2524 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2854, + "end_id": 2859 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2914, + "end_id": 2920 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3084, + "end_id": 3089 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3225, + "end_id": 3228 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3304, + "end_id": 3315 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3520, + "end_id": 3524 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3789, + "end_id": 3792 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3897, + "end_id": 3900 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 3912, + "end_id": 3947 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4151, + "end_id": 4167 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4252, + "end_id": 4257 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4359, + "end_id": 4363 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4467, + "end_id": 4473 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4525, + "end_id": 4530 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4554, + "end_id": 4560 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4633, + "end_id": 4636 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4652, + "end_id": 4655 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4724, + "end_id": 4727 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4810, + "end_id": 4812 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5091, + "end_id": 5099 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5128, + "end_id": 5139 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5140, + "end_id": 5147 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5216, + "end_id": 5220 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5263, + "end_id": 5274 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5428, + "end_id": 5438 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5664, + "end_id": 5669 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5716, + "end_id": 5719 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5733, + "end_id": 5773 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5848, + "end_id": 5854 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5954, + "end_id": 5960 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6006, + "end_id": 6014 + } + ] + }, + { + "url": "EuroCup2016/f6e64ee9b13a4088b24c45c257894c1e.mp4", + "total_frames": 9270, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 669, + "end_id": 682 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 691, + "end_id": 711 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 780, + "end_id": 791 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 889, + "end_id": 893 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 999, + "end_id": 1019 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1163, + "end_id": 1171 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 1357, + "end_id": 1381 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1441, + "end_id": 1450 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1459, + "end_id": 1462 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1603, + "end_id": 1609 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1822, + "end_id": 1825 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1874, + "end_id": 1880 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2158, + "end_id": 2163 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2483, + "end_id": 2492 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2526, + "end_id": 2530 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2665, + "end_id": 2671 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2694, + "end_id": 2708 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2825, + "end_id": 2840 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3085, + "end_id": 3099 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3121, + "end_id": 3129 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3177, + "end_id": 3188 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3619, + "end_id": 3622 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3700, + "end_id": 3705 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3712, + "end_id": 3716 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3791, + "end_id": 3810 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3899, + "end_id": 3909 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3967, + "end_id": 3981 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4026, + "end_id": 4037 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4273, + "end_id": 4286 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4434, + "end_id": 4462 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4667, + "end_id": 4683 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4710, + "end_id": 4715 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4862, + "end_id": 4870 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5044, + "end_id": 5050 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5124, + "end_id": 5149 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5208, + "end_id": 5228 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5435, + "end_id": 5446 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5578, + "end_id": 5581 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5718, + "end_id": 5742 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5748, + "end_id": 5753 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5936, + "end_id": 5943 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6049, + "end_id": 6053 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6242, + "end_id": 6252 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6394, + "end_id": 6398 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6406, + "end_id": 6411 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6421, + "end_id": 6427 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6634, + "end_id": 6640 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6760, + "end_id": 6772 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6787, + "end_id": 6792 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6826, + "end_id": 6837 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6856, + "end_id": 6882 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6928, + "end_id": 6934 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7070, + "end_id": 7074 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 7347, + "end_id": 7382 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7591, + "end_id": 7599 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 7659, + "end_id": 7667 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 7863, + "end_id": 7868 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 7899, + "end_id": 7905 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 8151, + "end_id": 8168 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 8175, + "end_id": 8184 + } + ] + }, + { + "url": "EuroCup2016/259856b769044b4d8dc94076deb356bf.mp4", + "total_frames": 8820, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 460, + "end_id": 463 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 487, + "end_id": 496 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 522, + "end_id": 524 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 526, + "end_id": 556 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 621, + "end_id": 623 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 680, + "end_id": 683 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 842, + "end_id": 845 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 953, + "end_id": 955 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 976, + "end_id": 978 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1003, + "end_id": 1005 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1110, + "end_id": 1118 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1178, + "end_id": 1187 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1197, + "end_id": 1200 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1242, + "end_id": 1248 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1385, + "end_id": 1393 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1527, + "end_id": 1530 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1654, + "end_id": 1660 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1744, + "end_id": 1752 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1789, + "end_id": 1791 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1836, + "end_id": 1839 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2023, + "end_id": 2026 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2270, + "end_id": 2272 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2349, + "end_id": 2351 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2383, + "end_id": 2385 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 2394, + "end_id": 2431 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2578, + "end_id": 2582 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2595, + "end_id": 2598 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2734, + "end_id": 2753 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2812, + "end_id": 2823 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2835, + "end_id": 2839 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2852, + "end_id": 2854 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2917, + "end_id": 2932 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3363, + "end_id": 3366 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3389, + "end_id": 3390 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3971, + "end_id": 3975 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4164, + "end_id": 4168 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4213, + "end_id": 4215 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4224, + "end_id": 4227 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4280, + "end_id": 4282 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4484, + "end_id": 4494 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4593, + "end_id": 4596 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4621, + "end_id": 4621 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4710, + "end_id": 4728 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4941, + "end_id": 4963 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5250, + "end_id": 5260 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5320, + "end_id": 5340 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5413, + "end_id": 5419 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5431, + "end_id": 5456 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5572, + "end_id": 5576 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5834, + "end_id": 5861 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5938, + "end_id": 5944 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 6071, + "end_id": 6082 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 6111, + "end_id": 6118 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6276, + "end_id": 6277 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6523, + "end_id": 6536 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6557, + "end_id": 6559 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 6626, + "end_id": 6652 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6664, + "end_id": 6668 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6759, + "end_id": 6761 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6860, + "end_id": 6863 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 6887, + "end_id": 6888 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7066, + "end_id": 7068 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7083, + "end_id": 7086 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7197, + "end_id": 7200 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 7316, + "end_id": 7336 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7512, + "end_id": 7515 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7533, + "end_id": 7537 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 7581, + "end_id": 7587 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7708, + "end_id": 7711 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 7808, + "end_id": 7812 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 8118, + "end_id": 8120 + } + ] + }, + { + "url": "EuroCup2016/1f0a0698e38d493988fe42a50f7e8723.mp4", + "total_frames": 6031, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 432, + "end_id": 437 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 497, + "end_id": 501 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 711, + "end_id": 714 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 740, + "end_id": 753 + }, + { + "label_ids": [ + 1, + 3 + ], + "label_names": [ + "进球", + "任意球" + ], + "start_id": 934, + "end_id": 967 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1013, + "end_id": 1016 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1035, + "end_id": 1039 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1081, + "end_id": 1084 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1163, + "end_id": 1165 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1264, + "end_id": 1275 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1320, + "end_id": 1336 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1356, + "end_id": 1360 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1382, + "end_id": 1386 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1477, + "end_id": 1480 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1496, + "end_id": 1498 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1529, + "end_id": 1541 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1584, + "end_id": 1586 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1998, + "end_id": 2001 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2394, + "end_id": 2406 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2454, + "end_id": 2465 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2673, + "end_id": 2676 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2894, + "end_id": 2903 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3165, + "end_id": 3179 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3206, + "end_id": 3217 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3254, + "end_id": 3263 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3392, + "end_id": 3396 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3454, + "end_id": 3456 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3541, + "end_id": 3554 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3586, + "end_id": 3590 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3646, + "end_id": 3658 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3692, + "end_id": 3695 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3902, + "end_id": 3926 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4012, + "end_id": 4022 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4034, + "end_id": 4043 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4067, + "end_id": 4078 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4349, + "end_id": 4353 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 4376, + "end_id": 4392 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4435, + "end_id": 4443 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4480, + "end_id": 4488 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4551, + "end_id": 4554 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4565, + "end_id": 4570 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4581, + "end_id": 4599 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4808, + "end_id": 4814 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4868, + "end_id": 4890 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4901, + "end_id": 4907 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4955, + "end_id": 4979 + }, + { + "label_ids": [ + 1, + 2 + ], + "label_names": [ + "进球", + "角球" + ], + "start_id": 5053, + "end_id": 5093 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5150, + "end_id": 5186 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5242, + "end_id": 5256 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5265, + "end_id": 5276 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5335, + "end_id": 5348 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 5359, + "end_id": 5375 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5447, + "end_id": 5450 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5461, + "end_id": 5468 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5636, + "end_id": 5649 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5743, + "end_id": 5758 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 5816, + "end_id": 5848 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5898, + "end_id": 5918 + } + ] + }, + { + "url": "EuroCup2016/8cfb4e605af44055b1576c37eb0e3209.mp4", + "total_frames": 6082, + "actions": [ + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 398, + "end_id": 400 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 496, + "end_id": 499 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 508, + "end_id": 510 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 556, + "end_id": 558 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 627, + "end_id": 629 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 695, + "end_id": 701 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 923, + "end_id": 927 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1064, + "end_id": 1067 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1107, + "end_id": 1109 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1264, + "end_id": 1266 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 1316, + "end_id": 1322 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1505, + "end_id": 1507 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1519, + "end_id": 1521 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 1677, + "end_id": 1681 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1709, + "end_id": 1711 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 1714, + "end_id": 1717 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2045, + "end_id": 2046 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2078, + "end_id": 2085 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2097, + "end_id": 2099 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2107, + "end_id": 2109 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2126, + "end_id": 2129 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2220, + "end_id": 2222 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2239, + "end_id": 2241 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2252, + "end_id": 2254 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2360, + "end_id": 2361 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2384, + "end_id": 2386 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2528, + "end_id": 2530 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2573, + "end_id": 2575 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2620, + "end_id": 2623 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 2676, + "end_id": 2682 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 2717, + "end_id": 2727 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 2763, + "end_id": 2765 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 2927, + "end_id": 2930 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3030, + "end_id": 3032 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3113, + "end_id": 3114 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3212, + "end_id": 3214 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3366, + "end_id": 3370 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3425, + "end_id": 3428 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3438, + "end_id": 3440 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3579, + "end_id": 3580 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 3629, + "end_id": 3643 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3664, + "end_id": 3667 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 3815, + "end_id": 3821 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 3831, + "end_id": 3838 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3906, + "end_id": 3910 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3928, + "end_id": 3931 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 3955, + "end_id": 3957 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 3990, + "end_id": 3994 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4031, + "end_id": 4033 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4092, + "end_id": 4106 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4139, + "end_id": 4141 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4209, + "end_id": 4211 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4224, + "end_id": 4226 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4248, + "end_id": 4250 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4317, + "end_id": 4319 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 4382, + "end_id": 4390 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 4417, + "end_id": 4422 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4427, + "end_id": 4429 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4471, + "end_id": 4472 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 4486, + "end_id": 4508 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4526, + "end_id": 4529 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4610, + "end_id": 4614 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4802, + "end_id": 4804 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 4826, + "end_id": 4828 + }, + { + "label_ids": [ + 1 + ], + "label_names": [ + "进球" + ], + "start_id": 4853, + "end_id": 4869 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5048, + "end_id": 5050 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5061, + "end_id": 5063 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5090, + "end_id": 5096 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5102, + "end_id": 5104 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5120, + "end_id": 5122 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5131, + "end_id": 5134 + }, + { + "label_ids": [ + 3 + ], + "label_names": [ + "任意球" + ], + "start_id": 5166, + "end_id": 5170 + }, + { + "label_ids": [ + 6 + ], + "label_names": [ + "换人" + ], + "start_id": 5376, + "end_id": 5393 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5447, + "end_id": 5451 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5675, + "end_id": 5677 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5756, + "end_id": 5760 + }, + { + "label_ids": [ + 7 + ], + "label_names": [ + "界外球" + ], + "start_id": 5960, + "end_id": 5963 + }, + { + "label_ids": [ + 4 + ], + "label_names": [ + "黄牌" + ], + "start_id": 5965, + "end_id": 5977 + }, + { + "label_ids": [ + 2 + ], + "label_names": [ + "角球" + ], + "start_id": 6016, + "end_id": 6020 + } + ] + } + ] +} diff --git a/applications/FootballAction/datasets/EuroCup2016/url.list b/applications/FootballAction/datasets/EuroCup2016/url.list new file mode 100644 index 0000000000000000000000000000000000000000..ddff158d698a012a51547ae4e416fe2f1a7f6bc2 --- /dev/null +++ b/applications/FootballAction/datasets/EuroCup2016/url.list @@ -0,0 +1,49 @@ +mp4/63e51df254d2402fac703b6c4fdb4ea9.mp4 +mp4/76b5f7ee28d942988c6b224bfac136bd.mp4 +mp4/250b88724acf40dbb6d7e8ccb400ef38.mp4 +mp4/c9516c903de3416c97dae91a59e968d7.mp4 +mp4/e1982c90cdd74abaacc4d0692070b400.mp4 +mp4/1be705a8f67648da8ec4b4296fa80895.mp4 +mp4/de23c0b2be3a4eb1990c5c657061fb29.mp4 +mp4/2754615de6e64c4fb95ce1a8095dc1c1.mp4 +mp4/299fe30d8f3b4a45b89313fe31f9f3c0.mp4 +mp4/6cc7db52c5ef4e70b401a5e00d8dd67a.mp4 +mp4/22e89747689e4f7e83e3620620c93269.mp4 +mp4/2ceb6c549fc64305a06a75acb355642b.mp4 +mp4/719b0a4bcb1f461eabb152298406b861.mp4 +mp4/259856b769044b4d8dc94076deb356bf.mp4 +mp4/d0bd3eab1e794f0f9501c353a6d37827.mp4 +mp4/19eb47cc736240d6b2dd930ab69da839.mp4 +mp4/4435b708af6d48519a6b726144147d51.mp4 +mp4/ea16ad2a020643529e257bd6cb11b3c3.mp4 +mp4/eeebffbd4ec74222a9c2d0775d79b689.mp4 +mp4/8cfb4e605af44055b1576c37eb0e3209.mp4 +mp4/6bca62b57cc449c6935f0b17f28d06be.mp4 +mp4/70cfc31e520840b2afca458f93a01ce4.mp4 +mp4/6496960935e845578e391a5916739752.mp4 +mp4/d6d25403a4bb4784aecff5f21fd00dc5.mp4 +mp4/3e23d452a082403391f8abfb87bf2fb4.mp4 +mp4/4c5d9d9af4f044c4a68d134061dc264f.mp4 +mp4/6994844c64b44c26b935cee9604bef0a.mp4 +mp4/d6322cb95f6a4402ac80432b561abd5d.mp4 +mp4/2c8b5587083a4784a51622e4fec87ccd.mp4 +mp4/5faa60d70ed141de8560110e840f2048.mp4 +mp4/45d08bc5cb0f424f9ed9d7874eb561cd.mp4 +mp4/6630aaf0e32146088d0b624e9288f071.mp4 +mp4/f2edbee29c1b4966b3a410260f78fbe3.mp4 +mp4/f24116fdd6a54214991db32f7dddef67.mp4 +mp4/0265731a0c6f4a9398c88db8e3d4a3bc.mp4 +mp4/02d2de09997f4215b06e3b00ff0502a0.mp4 +mp4/9c231896c56a43f291a5e190949f4333.mp4 +mp4/4afbbf9afcd44dfea45b044117cccb48.mp4 +mp4/745db97a080d4f44b450dc17a2bcf069.mp4 +mp4/5933d0ce17854483b81a318d7d45a34e.mp4 +mp4/d2cfef2da9f84237a6950c7f6659655c.mp4 +mp4/5572686cb90f440988ded956a60e555d.mp4 +mp4/8962ac5a332346e180c79d701ae0a175.mp4 +mp4/f6e64ee9b13a4088b24c45c257894c1e.mp4 +mp4/f6ed2b612b3d43baa0726be8b14ebe7c.mp4 +mp4/8ab7b0cba5744eb3b6fb10003dfda383.mp4 +mp4/1f0a0698e38d493988fe42a50f7e8723.mp4 +mp4/737fdb054ca141f2a45013c1740dd0a0.mp4 +mp4/bab63a9bcf204e4b99c4a887a01bfd60.mp4 diff --git a/applications/FootballAction/datasets/EuroCup2016/url_val.list b/applications/FootballAction/datasets/EuroCup2016/url_val.list new file mode 100644 index 0000000000000000000000000000000000000000..c401f3174097e638917ed620ebd08d4aa32e2205 --- /dev/null +++ b/applications/FootballAction/datasets/EuroCup2016/url_val.list @@ -0,0 +1,5 @@ +mp4/5572686cb90f440988ded956a60e555d.mp4 +mp4/f6e64ee9b13a4088b24c45c257894c1e.mp4 +mp4/259856b769044b4d8dc94076deb356bf.mp4 +mp4/1f0a0698e38d493988fe42a50f7e8723.mp4 +mp4/8cfb4e605af44055b1576c37eb0e3209.mp4 diff --git a/applications/FootballAction/datasets/script/get_frames_pcm.py b/applications/FootballAction/datasets/script/get_frames_pcm.py new file mode 100644 index 0000000000000000000000000000000000000000..286f47aa6aa88795e547a68ecb571aaebf94b807 --- /dev/null +++ b/applications/FootballAction/datasets/script/get_frames_pcm.py @@ -0,0 +1,54 @@ +""" +get frames and pcm from video +""" +import os +from concurrent import futures + +dataset = "../EuroCup2016" +url_list = os.path.join(dataset, 'url.list') +dst_frames = os.path.join(dataset, 'frames') +dst_pcm = os.path.join(dataset, 'pcm') +if not os.path.exists(dst_frames): + os.mkdir(dst_frames) +if not os.path.exists(dst_pcm): + os.mkdir(dst_pcm) + + +def extract_frames(video_name, out_folder, fps=5): + if os.path.exists(out_folder): + os.system('rm -rf ' + out_folder + '/*') + os.system('rm -rf ' + out_folder) + os.makedirs(out_folder) + cmd = 'ffmpeg -v 0 -i %s -r %d -q 0 %s/%s.jpg' % (video_name, fps, + out_folder, '%08d') + os.system(cmd) + + +def extract_pcm(video_name, file_name_pcm): + cmd = 'ffmpeg -y -i %s -acodec pcm_s16le -f s16le -ac 1 -ar 16000 %s -v 0' % ( + video_name, file_name_pcm) + os.system(cmd) + + +def process(line): + print(line) + mp4_name = os.path.join(dataset, line) + basename = os.path.basename(line).split('.')[0] + folder_frame = os.path.join(dst_frames, basename) + filename_pcm = os.path.join(dst_pcm, basename + '.pcm') + # extract + extract_frames(mp4_name, folder_frame) + extract_pcm(mp4_name, filename_pcm) + + +if __name__ == "__main__": + with open(url_list, 'r') as f: + lines = f.readlines() + lines = [k.strip() for k in lines] + + # multi thread + with futures.ProcessPoolExecutor(max_workers=10) as executer: + fs = [executer.submit(process, line) for line in lines] + #for line in lines: + # process(line) + print("done") diff --git a/applications/FootballAction/datasets/script/get_instance_for_bmn.py b/applications/FootballAction/datasets/script/get_instance_for_bmn.py new file mode 100644 index 0000000000000000000000000000000000000000..5d348492f96a1ff8d50f914f00a1cd4c1f6b2044 --- /dev/null +++ b/applications/FootballAction/datasets/script/get_instance_for_bmn.py @@ -0,0 +1,216 @@ +""" +get instance for bmn +使用winds=40的滑窗,将所有子窗口的长度之和小于winds的进行合并 +合并后,父窗口代表bmn训练数据,子窗口代表tsn训练数据 +""" +import os +import sys +import json +import random +import pickle +import numpy as np + +bmn_window = 40 +dataset = "../EuroCup2016" +feat_dir = dataset + '/features' +out_dir = dataset + '/input_for_bmn' +label_files = { + 'train': 'label_cls8_train.json', + 'validation': 'label_cls8_val.json' +} + +global fps + + +def gen_gts_for_bmn(gts_data): + """ + @param, gts_data, original gts for action detection + @return, gts_bmn, output gts dict for bmn + """ + fps = gts_data['fps'] + gts_bmn = {'fps': fps, 'gts': []} + for sub_item in gts_data['gts']: + url = sub_item['url'] + + max_length = sub_item['total_frames'] + # 特征提取没有获取所有帧特征,这里load feature获取准确max_length + #feat_path = feat_dir + '/' + os.path.basename(url).replace('.mp4', '.pkl') + #feature_video = pickle.load(open(feat_path, 'rb'))['features'] + #max_length = int(len(feature_video) * 1.0 / fps) + + gts_bmn['gts'].append({ + 'url': url, + 'total_frames': max_length, + 'root_actions': [] + }) + sub_actions = sub_item['actions'] + # duration > bmn_window, 直接删除 + for idx, sub_action in enumerate(sub_actions): + if sub_action['end_id'] - sub_action['start_id'] > bmn_window: + sub_actions.pop(idx) + + root_actions = [sub_actions[0]] + # before_id, 前一动作的最后一帧 + # after_id, 后一动作的第一帧 + before_id = 0 + for idx in range(1, len(sub_actions)): + cur_action = sub_actions[idx] + duration = (cur_action['end_id'] - root_actions[0]['start_id']) + if duration > bmn_window: + after_id = cur_action['start_id'] + gts_bmn['gts'][-1]['root_actions'].append({ + 'before_id': + before_id, + 'after_id': + after_id, + 'actions': + root_actions + }) + before_id = root_actions[-1]['end_id'] + root_actions = [cur_action] + else: + root_actions.append(cur_action) + if idx == len(sub_actions) - 1: + after_id = max_length + gts_bmn['gts'][-1]['root_actions'].append({ + 'before_id': + before_id, + 'after_id': + after_id, + 'actions': + root_actions + }) + return gts_bmn + + +def combile_gts(gts_bmn, gts_process, mode): + """ + 1、bmn_window 范围内只有一个动作,只取一个目标框 + 2、bmn_window 范围内有多个动作,取三个目标框(第一个动作、最后一个动作、所有动作) + """ + global fps + fps = gts_process['fps'] + duration_second = bmn_window * 1.0 + duration_frame = bmn_window * fps + feature_frame = duration_frame + for item in gts_process['gts']: + url = item['url'] + basename = os.path.basename(url).split('.')[0] + root_actions = item['root_actions'] + for root_action in root_actions: + segments = [] + # all actions + segments.append({ + 'actions': root_action['actions'], + 'before_id': root_action['before_id'], + 'after_id': root_action['after_id'] + }) + if len(root_action['actions']) > 1: + # first action + segments.append({ + 'actions': [root_action['actions'][0]], + 'before_id': + root_action['before_id'], + 'after_id': + root_action['actions'][1]['start_id'] + }) + # last action + segments.append({ + 'actions': [root_action['actions'][-1]], + 'before_id': + root_action['actions'][-2]['end_id'], + 'after_id': + root_action['after_id'] + }) + for segment in segments: + before_id = segment['before_id'] + after_id = segment['after_id'] + actions = segment['actions'] + box0 = int(max(actions[-1]['end_id'] - bmn_window, before_id)) + box1 = int(min(actions[0]['start_id'], after_id - bmn_window)) + if box0 <= box1: + cur_start = random.randint(box0, box1) + cur_end = cur_start + bmn_window + name = '{}_{}_{}'.format(basename, cur_start, cur_end) + annotations = [] + for action in actions: + label = str(1.0 * action['label_ids'][0]) + label_name = action['label_names'][0] + seg0 = 1.0 * (action['start_id'] - cur_start) + seg1 = 1.0 * (action['end_id'] - cur_start) + annotations.append({ + 'segment': [seg0, seg1], + 'label': label, + 'label_name': label_name + }) + gts_bmn[name] = { + 'duration_second': duration_second, + 'duration_frame': duration_frame, + 'feature_frame': feature_frame, + 'subset': mode, + 'annotations': annotations + } + + return gts_bmn + + +def save_feature_to_numpy(gts_bmn, folder): + global fps + print('save feature for bmn ...') + if not os.path.exists(folder): + os.mkdir(folder) + process_gts_bmn = {} + for item, value in gts_bmn.items(): + basename, start_id, end_id = item.split('_') + if not basename in process_gts_bmn: + process_gts_bmn[basename] = [] + process_gts_bmn[basename].append({ + 'name': item, + 'start': int(start_id), + 'end': int(end_id) + }) + + for item, values in process_gts_bmn.items(): + feat_path = os.path.join(feat_dir, item + '.pkl') + print(feat_path) + feature = pickle.load(open(feat_path, 'rb')) + image_feature = feature['image_feature'] + pcm_feature = feature['pcm_feature'] + + pcm_feature = pcm_feature.reshape((pcm_feature.shape[0] * 5, 640)) + min_length = min(image_feature.shape[0], pcm_feature.shape[0]) + if min_length == 0: + continue + image_feature = image_feature[:min_length, :] + pcm_feature = pcm_feature[:min_length, :] + feature_video = np.concatenate((image_feature, pcm_feature), axis=1) + for value in values: + save_cut_name = os.path.join(folder, value['name']) + start_frame = (value['start']) * fps + end_frame = (value['end']) * fps + if end_frame > len(feature_video): + del gts_bmn[value['name']] + continue + feature_cut = [ + feature_video[i] for i in range(start_frame, end_frame) + ] + np_feature_cut = np.array(feature_cut, dtype=np.float32) + np.save(save_cut_name, np_feature_cut) + return gts_bmn + + +if __name__ == "__main__": + if not os.path.exists(out_dir): + os.mkdir(out_dir) + gts_bmn = {} + for item, value in label_files.items(): + label_file = os.path.join(dataset, value) + gts_data = json.load(open(label_file, 'rb')) + gts_process = gen_gts_for_bmn(gts_data) + gts_bmn = combile_gts(gts_bmn, gts_process, item) + + gts_bmn = save_feature_to_numpy(gts_bmn, out_dir + '/feature') + + with open(out_dir + '/label.json', 'w', encoding='utf-8') as f: + data = json.dumps(gts_bmn, indent=4, ensure_ascii=False) + f.write(data) diff --git a/applications/FootballAction/datasets/script/get_instance_for_lstm.py b/applications/FootballAction/datasets/script/get_instance_for_lstm.py new file mode 100644 index 0000000000000000000000000000000000000000..a3f48281d986399437e09310c082239ede58410e --- /dev/null +++ b/applications/FootballAction/datasets/script/get_instance_for_lstm.py @@ -0,0 +1,171 @@ +""" +get instance for lstm +根据gts计算每个proposal_bmn的iou、ioa、label等信息 +""" +import os +import sys +import json +import random +import pickle +import numpy as np + +dataset = "/home/work/datasets/EuroCup2016" +feat_dir = dataset + '/features' +prop_file = dataset + '/feature_bmn/prop.json' +out_dir = dataset + '/input_for_lstm' +label_files = { + 'train': 'label_cls8_train.json', + 'validation': 'label_cls8_val.json' +} + + +def IoU(e1, e2): + """ + clc iou and ioa + """ + area1 = e1["end"] - e1["start"] + area2 = e2["end"] - e2["start"] + x1 = np.maximum(e1["start"], e2["start"]) + x2 = np.minimum(e1["end"], e2["end"]) + inter = np.maximum(0.0, x2 - x1) + iou = 0.0 if (area1 + area2 - + inter) == 0 else inter * 1.0 / (area1 + area2 - inter) + ioa = 0.0 if area2 == 0 else inter * 1.0 / area2 + return iou, ioa + + +def clc_iou_of_proposal(proposal, gts): + hit_gts = {} + label = 0 + norm_start = 0. + hit = False + for gt in gts: + e1 = {'start': proposal['start'], 'end': proposal['end']} + e2 = {'start': gt['start_id'], 'end': gt['end_id']} + iou, ioa = IoU(e1, e2) + if iou > 0: + hit = True + hit_gts = gt + label = hit_gts['label_ids'][0] + norm_start = (gt['start_id'] - proposal['start']) * 1.0 / ( + proposal['end'] - proposal['start']) + break + res = { + 'label': label, + 'norm_iou': iou, + 'norm_ioa': ioa, + 'norm_start': norm_start, + 'proposal': proposal, + 'hit_gts': hit_gts + } + return res + + +def get_bmn_info(gts_data, proposal_data, res_bmn, mode, score_threshold=0.01): + """ + @param, gts_data, original gts for action detection + @param, proposal_data, proposal actions from bmn + @param, mode, train or validation + @return, None. + """ + fps = gts_data['fps'] + res_bmn['fps'] = fps + for gts_item in gts_data['gts']: + url = gts_item['url'] + print(url) + max_length = gts_item['total_frames'] + + video_name = os.path.basename(url).split('.')[0] + if not video_name in proposal_data: + continue + + gts_actions = gts_item['actions'] + prop_actions = proposal_data[video_name] + + res_bmn['results'].append({ + 'url': url, + 'mode': mode, + 'total_frames': max_length, + 'num_gts': len(gts_actions), + 'num_proposals': len(prop_actions), + 'proposal_actions': [] + }) + for proposal in prop_actions: + if proposal['score'] < score_threshold: + continue + proposal['start'] = int(proposal['start'] * 1.0 / fps) + proposal['end'] = int(proposal['end'] * 1.0 / fps) + gts_info = clc_iou_of_proposal(proposal, gts_actions) + res_bmn['results'][-1]['proposal_actions'].append(gts_info) + + return res_bmn + + +def save_feature(label_info, out_dir): + print('save feature ...') + fps = label_info['fps'] + out_feature_dir = out_dir + '/feature' + if not os.path.exists(out_feature_dir): + os.mkdir(out_feature_dir) + fid_train = open(out_dir + '/train.txt', 'w') + fid_val = open(out_dir + '/val.txt', 'w') + for res in label_info['results']: + basename = os.path.basename(res['url']).split('.')[0] + print(basename, res['num_proposals']) + mode = res['mode'] + fid = fid_train if mode == 'train' else fid_val + feature_path = os.path.join(feat_dir, basename + '.pkl') + feature_data = pickle.load(open(feature_path, 'rb')) + image_feature = feature_data['image_feature'] + audio_feature = feature_data['audio_feature'] + max_len_audio = len(audio_feature) + for proposal in res['proposal_actions']: + label = proposal['label'] + start_id = proposal['proposal']['start'] + end_id = proposal['proposal']['end'] + # get hit feature + image_feature_hit = image_feature[start_id * fps:end_id * fps] + audio_feature_hit = audio_feature[min(start_id, max_len_audio + ):min(end_id, max_len_audio)] + + # save + anno_info = { + 'image_feature': np.array(image_feature_hit, dtype=np.float32), + 'audio_feature': np.array(audio_feature_hit, dtype=np.float32), + 'feature_fps': fps, + 'label_info': proposal, + 'video_name': basename + } + save_name = '{}/{}_{}_{}.pkl'.format(out_feature_dir, basename, + start_id, end_id) + with open(save_name, 'wb') as f: + pickle.dump(anno_info, f, protocol=pickle.HIGHEST_PROTOCOL) + fid.write('{} {}\n'.format(save_name, label)) + + fid_train.close() + fid_val.close() + print('done!') + + +if __name__ == "__main__": + if not os.path.exists(out_dir): + os.mkdir(out_dir) + prop_data = json.load(open(prop_file, 'rb')) + proposal_data = {} + for item in prop_data: + proposal_data[os.path.basename( + item['video_name'])] = item['bmn_results'] + + # get label info + res_bmn = {'fps': 0, 'results': []} + for item, value in label_files.items(): + label_file = os.path.join(dataset, value) + gts_data = json.load(open(label_file, 'rb')) + res_bmn = get_bmn_info(gts_data, proposal_data, res_bmn, item) + + with open(out_dir + '/label_info.json', 'w', encoding='utf-8') as f: + data = json.dumps(res_bmn, indent=4, ensure_ascii=False) + f.write(data) + + # save feature + save_feature(res_bmn, out_dir) diff --git a/applications/FootballAction/datasets/script/get_instance_for_pptsm.py b/applications/FootballAction/datasets/script/get_instance_for_pptsm.py new file mode 100644 index 0000000000000000000000000000000000000000..47bd2c12c5b7e1f66923aebf90360b6d0551e11e --- /dev/null +++ b/applications/FootballAction/datasets/script/get_instance_for_pptsm.py @@ -0,0 +1,95 @@ +""" +get instance for tsn +positive: 标注后的动作区间,一个区间所有frames生成一个pkl +negative: 标注后的非动作区间,随机取N个区间生成N个pkl,每个区间长度等于最近的前一个动作区间的长度 +""" +import os +import json +import numpy as np +import random +import pickle +from concurrent import futures + +dataset = "../EuroCup2016" +frames_dir = dataset + '/frames' +label_files = {'train': 'label_cls8_train.json', 'val': 'label_cls8_val.json'} + + +def process(item, fps, save_folder): + actions_pos = [] + actions_neg = [] + url = item['url'] + print(url) + basename = os.path.basename(url).split('.')[0] + actions = item['actions'] + # pos + for action in actions: + actions_pos.append({ + 'label': action['label_ids'], + 'start': action['start_id'] * fps, + 'end': action['end_id'] * fps + }) + # neg + for idx, pos in enumerate(actions_pos): + if idx == len(actions_pos) - 1: + break + len_pos = pos['end'] - pos['start'] + duration_start = [pos['end'], actions_pos[idx + 1]['start'] - len_pos] + if duration_start[1] - duration_start[0] < 3: + continue + for k in range(1, 3): + start_frame = random.randint(duration_start[0], duration_start[1]) + end_frame = start_frame + len_pos + actions_neg.append({ + 'label': [0], + 'start': start_frame, + 'end': end_frame + }) + # save pkl + for item in np.concatenate((actions_pos, actions_neg), axis=0): + start = item['start'] + end = item['end'] + label = item['label'] + label_str = str(label[0]) + if len(item['label']) == 2: + label_str = label_str + '-' + str(label[1]) + frames = [] + for ii in range(start, end + 1): + img = os.path.join(frames_dir, basename, '%08d.jpg' % ii) + with open(img, 'rb') as f: + data = f.read() + frames.append(data) + # print(label_str) + outname = '%s/%s_%08d_%08d_%s.pkl' % (save_folder, basename, start, end, + label_str) + with open(outname, 'wb') as f: + pickle.dump((basename, label, frames), f, -1) + + +def gen_instance_pkl(label_data, save_folder): + fps = label_data['fps'] + gts = label_data['gts'] + with futures.ProcessPoolExecutor(max_workers=10) as executer: + fs = [executer.submit(process, gt, fps, save_folder) for gt in gts] + + #for gt in gts: + # process(gt, fps, save_folder) + + +if __name__ == "__main__": + for item, value in label_files.items(): + save_folder = os.path.join(dataset, 'input_for_pptsm', item) + if not os.path.exists(save_folder): + os.makedirs(save_folder) + + label_file = os.path.join(dataset, value) + label_data = json.load(open(label_file, 'rb')) + + gen_instance_pkl(label_data, save_folder) + + # gen train val list + data_dir = '/home/work/PaddleVideo/applications/FootballAction/datasets/EuroCup2016/input_for_pptsm/' + os.system('find ' + data_dir + 'train -name "*.pkl" > ' + data_dir + + 'train.list') + os.system('find ' + data_dir + 'val -name "*.pkl" > ' + data_dir + + 'val.list') diff --git a/applications/FootballAction/extractor/configs/configs.yaml b/applications/FootballAction/extractor/configs/configs.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1c51610acab63fd16f7408eac988d602fa703174 --- /dev/null +++ b/applications/FootballAction/extractor/configs/configs.yaml @@ -0,0 +1,62 @@ +COMMON: + fps: 5 + use_gpu: True + label_dic: '/home/work/inference/configs/index_label_football_8.json' + # debug + PCM_ONLY: False + DEBUG: False + BMN_ONLY: False + LSTM_ONLY: False + +PPTSM: + name: "PPTSM" + model_file: "/home/work/inference/checkpoints/ppTSM/ppTSM.pdmodel" + params_file: "/home/work/inference/checkpoints/ppTSM/ppTSM.pdiparams" + gpu_mem: 8000 + device_id: 0 + seg_num: 8 + seglen: 1 + short_size: 256 + target_size: 224 + batch_size: 32 + image_mean: [0.485, 0.456, 0.406] + image_std: [0.229, 0.224, 0.225] + reader_threads: 12 + buf_size: 1024 + +AUDIO: + name: "AUDIO" + model_file: "/home/work/inference/checkpoints/AUDIO/__model__" + params_file: "/home/work/inference/checkpoints/AUDIO/__param__" + gpu_mem: 8000 + device_id: 0 + sample_rate: 16000 + batch_size: 32 + +BMN: + name: "BMN" + model_file: "/home/work/inference/checkpoints/BMN/__model__" + params_file: "/home/work/inference/checkpoints/BMN/__param__" + gpu_mem: 8000 + device_id: 0 + window_step: 200 # 200 + tscale: 200 + dscale: 200 + batch_size: 8 # 8 + nms_thread: 0.7 + score_thread: 0.03 # 0.05 + +ACTION: + name: "ACTION" + model_file: "/home/work/inference/checkpoints/LSTM/__model__" + params_file: "/home/work/inference/checkpoints/LSTM/__param__" + gpu_mem: 8000 + device_id: 0 + batch_size: 32 + topk: 1 + nms_thread: 0.01 + nms_offset: 10 + + classify_score_thread: 0.05 # 0.15 + iou_score_thread: 0.1 # 0.4 + diff --git a/applications/FootballAction/extractor/configs/index_label_football_8.json b/applications/FootballAction/extractor/configs/index_label_football_8.json new file mode 100644 index 0000000000000000000000000000000000000000..a6eed11c216b762de2735acafd3cad87cdd340ab --- /dev/null +++ b/applications/FootballAction/extractor/configs/index_label_football_8.json @@ -0,0 +1,10 @@ +{ + "0": "背景", + "1": "进球", + "2": "角球", + "3": "任意球", + "4": "黄牌", + "5": "红牌", + "6": "换人", + "7": "界外球" +} diff --git a/applications/FootballAction/extractor/extract_bmn.py b/applications/FootballAction/extractor/extract_bmn.py new file mode 100644 index 0000000000000000000000000000000000000000..7d340c03fa491326b8d846501c8597c69e63bce8 --- /dev/null +++ b/applications/FootballAction/extractor/extract_bmn.py @@ -0,0 +1,86 @@ +#!./python27-gcc482/bin/python +# coding: utf-8 +""" +BAIDU CLOUD action +""" + +import os +import sys +import pickle +import json +import time +import shutil + +import numpy as np + +sys.path.append("../predict/action_detect") +import models.bmn_infer as prop_model +from utils.preprocess import get_images +from utils.config_utils import parse_config, print_configs +import utils.config_utils as config_utils + +import logger +logger = logger.Logger() + +def load_model(cfg_file="configs/configs.yaml"): + """ + load_model + """ + logger.info("load model ... ") + global infer_configs + infer_configs = parse_config(cfg_file) + print_configs(infer_configs, "Infer") + + t0 = time.time() + global prop_model + prop_model = prop_model.InferModel(infer_configs) + t1 = time.time() + logger.info("step0: load model time: {} min\n".format((t1 - t0) * 1.0 / 60)) + + +def video_classify(video_name): + """ + extract_feature + """ + logger.info('predict ... ') + logger.info(video_name) + imgs_path = video_name.replace(".mp4", "").replace("mp4", "frames") + pcm_path = video_name.replace(".mp4", ".pcm").replace("mp4", "pcm") + + # step 1: extract feature + + feature_path = video_name.replace(".mp4", ".pkl").replace("mp4", "features") + video_features = pickle.load(open(feature_path, 'rb')) + + # step2: get proposal + t0 = time.time() + bmn_results = prop_model.predict(infer_configs, material=video_features) + t1 = time.time() + logger.info(np.array(bmn_results).shape) + logger.info("step2: proposal time: {} min".format((t1 - t0) * 1.0 / 60)) + + return bmn_results + + +if __name__ == '__main__': + dataset_dir = "/home/work/PaddleVideo/applications/FootballAction/datasets/EuroCup2016" + if not os.path.exists(dataset_dir + '/feature_bmn'): + os.mkdir(dataset_dir + '/feature_bmn') + results = [] + + load_model() + + video_url = os.path.join(dataset_dir, 'url.list') + with open(video_url, 'r') as f: + lines = f.readlines() + lines = [os.path.join(dataset_dir, k.strip()) for k in lines] + + for line in lines: + bmn_results = video_classify(line) + results.append({'video_name': os.path.basename(line).split('.')[0], + 'num_proposal': len(bmn_results), + 'bmn_results': bmn_results}) + + with open(dataset_dir + '/feature_bmn/prop.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) diff --git a/applications/FootballAction/extractor/extract_feat.py b/applications/FootballAction/extractor/extract_feat.py new file mode 100644 index 0000000000000000000000000000000000000000..fd17a24f610be26f5fcb47e56e98c3eb838afe4b --- /dev/null +++ b/applications/FootballAction/extractor/extract_feat.py @@ -0,0 +1,95 @@ +#!./python27-gcc482/bin/python +# coding: utf-8 +""" +BAIDU CLOUD action +""" + +import os +import sys +import pickle +import json +import time +import shutil + +import numpy as np + +sys.path.append("../predict/action_detect") +import models.pptsm_infer as image_model +import models.audio_infer as audio_model + +from utils.preprocess import get_images +from utils.config_utils import parse_config, print_configs +import utils.config_utils as config_utils + +import logger +logger = logger.Logger() + + +def load_model(cfg_file="configs/configs.yaml"): + """ + load_model + """ + logger.info("load model ... ") + global infer_configs + infer_configs = parse_config(cfg_file) + print_configs(infer_configs, "Infer") + + t0 = time.time() + global image_model, audio_model + image_model = image_model.InferModel(infer_configs) + audio_model = audio_model.InferModel(infer_configs) + t1 = time.time() + logger.info("step0: load model time: {} min\n".format((t1 - t0) * 1.0 / 60)) + + +def video_classify(video_name): + """ + extract_feature + """ + logger.info('predict ... ') + logger.info(video_name) + imgs_path = video_name.replace(".mp4", "").replace("mp4", "frames") + pcm_path = video_name.replace(".mp4", ".pcm").replace("mp4", "pcm") + + # step 1: extract feature + t0 = time.time() + image_path_list = get_images(imgs_path) + infer_configs['PPTSM']['frame_list'] = image_path_list + infer_configs['AUDIO']['pcm_file'] = pcm_path + image_features = image_model.predict(infer_configs) + audio_features, pcm_features = audio_model.predict(infer_configs) + + np_image_features = np.array(image_features, dtype=np.float32) + np_audio_features = np.array(audio_features, dtype=np.float32) + np_pcm_features = np.array(pcm_features, dtype=np.float32) + t1 = time.time() + + logger.info('{} {} {}'.format(np_image_features.shape, + np_audio_features.shape, + np_pcm_features.shape)) + logger.info("step1: feature extract time: {} min".format((t1 - t0) * 1.0 / 60)) + video_features = {'image_feature': np_image_features, + 'audio_feature': np_audio_features, + 'pcm_feature': np_pcm_features} + + # save feature + feature_path = video_name.replace(".mp4", ".pkl").replace("mp4", "features") + feat_pkl_str = pickle.dumps(video_features, protocol=pickle.HIGHEST_PROTOCOL) + with open(feature_path, 'wb') as fout: + fout.write(feat_pkl_str) + + +if __name__ == '__main__': + dataset_dir = "/home/work/PaddleVideo/applications/FootballAction/datasets/EuroCup2016" + if not os.path.exists(dataset_dir + '/features'): + os.mkdir(dataset_dir + '/features') + + load_model() + + video_url = os.path.join(dataset_dir, 'url.list') + with open(video_url, 'r') as f: + lines = f.readlines() + lines = [os.path.join(dataset_dir, k.strip()) for k in lines] + + for line in lines: + video_classify(line) diff --git a/applications/FootballAction/predict/action_detect/action.py b/applications/FootballAction/predict/action_detect/action.py new file mode 100644 index 0000000000000000000000000000000000000000..6f4775f38656753a8e0b8d058b78e890606f4020 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/action.py @@ -0,0 +1,174 @@ +#!./python27-gcc482/bin/python +# coding: utf-8 +""" +BAIDU CLOUD action +""" + +import os +import sys +import pickle +import json +import time +import functools + +import numpy as np + +from utils.preprocess import get_images +from utils.config_utils import parse_config, print_configs +import mfcc.feature_extractor as mfcc_extractor + +import models.pptsm_infer as image_model +import models.audio_infer as audio_model +import models.bmn_infer as prop_model +import models.lstm_infer as classify_model + +import logger +logger = logger.Logger() + +def record_time_info(func): + """decorator func to log cost time for func + """ + @functools.wraps(func) + def timer(*args): + """log cost time for func + """ + logger.info("function [{}] processing ...".format(func.__name__)) + start_time = time.time() + retval = func(*args) + cost_time = round(time.time() - start_time, 5) + logger.info("function [{}] run time: {:.2f} min".format(func.__name__, cost_time / 60)) + return retval + return timer + + +class ActionDetection(object): + """ModelPredict""" + def __init__(self, cfg_file="configs/configs.yaml"): + cfg = parse_config(cfg_file) + self.configs = cfg + print_configs(self.configs, "Infer") + + name = 'COMMON' + self.DEBUG = cfg[name]['DEBUG'] + self.BMN_ONLY = cfg[name]['BMN_ONLY'] + self.LSTM_ONLY = cfg[name]['LSTM_ONLY'] + self.PCM_ONLY = cfg[name]['PCM_ONLY'] + if self.LSTM_ONLY: + self.prop_dict = {} + for dataset in ['EuroCup2016']: + prop_json = '/home/work/datasets/{}/feature_bmn/prop.json'.format(dataset) + json_data = json.load(open(prop_json, 'r')) + for item in json_data: + basename = prop_json.replace('feature_bmn/prop.json', 'mp4') + basename = basename + '/' + item['video_name'] + '.mp4' + self.prop_dict[basename] = item['bmn_results'] + + + @record_time_info + def load_model(self): + """ + load_model + """ + if not self.DEBUG: + self.image_model = image_model.InferModel(self.configs) + if not self.PCM_ONLY: + self.audio_model = audio_model.InferModel(self.configs) + + if not self.LSTM_ONLY: + self.prop_model = prop_model.InferModel(self.configs) + + if not self.BMN_ONLY: + self.classify_model = classify_model.InferModel(self.configs) + + logger.info("==> Action Detection prepared.") + + @record_time_info + def infer(self, imgs_path, pcm_path, fps=5): + """ + extract_feature + """ + self.imgs_path = imgs_path + self.pcm_path = pcm_path + self.configs['COMMON']['fps'] = fps + + logger.info("==> input video {}".format(os.path.basename(self.imgs_path))) + + # step 1: extract feature + video_features = self.extract_feature() + + # step2: get proposal + bmn_results = self.extract_proposal(video_features) + + # step3: classify + material = {'feature': video_features, 'proposal': bmn_results} + action_results = self.video_classify(material) + + return bmn_results, action_results + + @record_time_info + def video_classify(self, material): + """video classify""" + if self.BMN_ONLY: + return [] + action_results = self.classify_model.predict(self.configs, material=material) + logger.info('action shape {}'.format(np.array(action_results).shape)) + return action_results + + @record_time_info + def extract_proposal(self, video_features): + """extract proposal""" + if self.LSTM_ONLY: + basename = self.imgs_path.replace('frames', 'mp4') + '.mp4' + bmn_results = self.prop_dict[basename] + return bmn_results + bmn_results = self.prop_model.predict(self.configs, material=video_features) + logger.info('proposal shape {}'.format(np.array(bmn_results).shape)) + return bmn_results + + @record_time_info + def extract_feature(self): + """extract feature""" + if not self.DEBUG: + image_path_list = get_images(self.imgs_path) + self.configs['PPTSM']['frame_list'] = image_path_list + self.configs['AUDIO']['pcm_file'] = self.pcm_path + image_features = self.image_model.predict(self.configs) + if self.PCM_ONLY: + sample_rate = self.configs['AUDIO']['sample_rate'] + pcm_features = mfcc_extractor.extract_pcm(self.pcm_path, sample_rate) + audio_features = [] + else: + audio_features, pcm_features = self.audio_model.predict(self.configs) + + np_image_features = np.array(image_features, dtype=np.float32) + np_audio_features = np.array(audio_features, dtype=np.float32) + np_pcm_features = np.array(pcm_features, dtype=np.float32) + + video_features = {'image_feature': np_image_features, + 'audio_feature': np_audio_features, + 'pcm_feature': np_pcm_features} + else: + feature_path = self.imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + logger.info("feature shape {} {} {}".format(video_features['image_feature'].shape, + video_features['audio_feature'].shape, + video_features['pcm_feature'].shape)) + + return video_features + +if __name__ == '__main__': + + model_predict = ActionDetection(cfg_file="../configs/configs.yaml") + model_predict.load_model() + + imgs_path = "/home/work/datasets/EuroCup2016/frames/1be705a8f67648da8ec4b4296fa80895" + pcm_path = "/home/work/datasets/EuroCup2016/pcm/1be705a8f67648da8ec4b4296fa80895.pcm" + + bmn_results, action_results = model_predict.infer(imgs_path, pcm_path) + results = {'bmn_results': bmn_results, 'action_results': action_results} + + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + diff --git a/applications/FootballAction/predict/action_detect/logger.py b/applications/FootballAction/predict/action_detect/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..b03348721df29fdf6cbd22954b718cec2b83efc2 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/logger.py @@ -0,0 +1,24 @@ +""" +logger +""" +import os +import logging + +class Logger(logging.Logger): + """Customized logger for news stripper + """ + def __init__(self): + super(Logger, self).__init__(self) + if not os.path.exists('logs'): + os.mkdir('logs') + handler = logging.FileHandler("logs/action_detect.log") + # handler.setLevel(logging.DEBUG) + handler.setLevel(logging.INFO) + + format = "%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d %(message)s" + datefmt = "%y-%m-%d %H:%M:%S" + + formatter = logging.Formatter(format, datefmt) + handler.setFormatter(formatter) + self.addHandler(handler) + diff --git a/applications/FootballAction/predict/action_detect/mfcc/__init__.py b/applications/FootballAction/predict/action_detect/mfcc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/FootballAction/predict/action_detect/mfcc/feature_extractor.py b/applications/FootballAction/predict/action_detect/mfcc/feature_extractor.py new file mode 100755 index 0000000000000000000000000000000000000000..07c1027a207c2b9bd1b14eb750a8d7d7b2b802a2 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/mfcc/feature_extractor.py @@ -0,0 +1,182 @@ +""" +audio feature extract +""" +# coding: utf-8 +import os +import numpy as np +import pickle +import mfcc.vgg_params as vgg_params + + +def frame(data, window_length, hop_length): + """ + frame + """ + num_samples = data.shape[0] + #print("window_length , hop_length", window_length, hop_length) + #print("num_sample = ", num_samples) + num_frames = 1 + int(np.floor((num_samples - window_length) / hop_length)) + #print(" num_frames = ", num_frames) + shape = (num_frames, window_length) + data.shape[1:] + #print(" shape = ", shape) + strides = (data.strides[0] * hop_length, ) + data.strides + #print("data.strides = ", data.strides) + #print("strides = ", strides) + return np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides) + + +def periodic_hann(window_length): + """ + periodic_hann + """ + return 0.5 - (0.5 * + np.cos(2 * np.pi / window_length * np.arange(window_length))) + + +def stft_magnitude(signal, fft_length, hop_length=None, window_length=None): + """ + stft_magnitude + """ + frames = frame(signal, window_length, hop_length) + window = periodic_hann(window_length) + windowed_frames = frames * window + return np.abs(np.fft.rfft(windowed_frames, int(fft_length))) + + +_MEL_BREAK_FREQUENCY_HERTZ = 700.0 +_MEL_HIGH_FREQUENCY_Q = 1127.0 + + +def hertz_to_mel(frequencies_hertz): + """ + hertz_to_mel + """ + return _MEL_HIGH_FREQUENCY_Q * np.log(1.0 + (frequencies_hertz / + _MEL_BREAK_FREQUENCY_HERTZ)) + + +def spectrogram_to_mel_matrix(num_mel_bins=20, + num_spectrogram_bins=129, + audio_sample_rate=8000, + lower_edge_hertz=125.0, + upper_edge_hertz=3800.0): + """ + spectrogram_to_mel_matrix + """ + nyquist_hertz = audio_sample_rate / 2. + if lower_edge_hertz >= upper_edge_hertz: + raise ValueError("lower_edge_hertz %.1f >= upper_edge_hertz %.1f" % + (lower_edge_hertz, upper_edge_hertz)) + spectrogram_bins_hertz = np.linspace(0.0, nyquist_hertz, + num_spectrogram_bins) + spectrogram_bins_mel = hertz_to_mel(spectrogram_bins_hertz) + band_edges_mel = np.linspace(hertz_to_mel(lower_edge_hertz), + hertz_to_mel(upper_edge_hertz), + num_mel_bins + 2) + mel_weights_matrix = np.empty((num_spectrogram_bins, num_mel_bins)) + for i in range(num_mel_bins): + lower_edge_mel, center_mel, upper_edge_mel = band_edges_mel[i:i + 3] + lower_slope = ((spectrogram_bins_mel - lower_edge_mel) / + (center_mel - lower_edge_mel)) + upper_slope = ((upper_edge_mel - spectrogram_bins_mel) / + (upper_edge_mel - center_mel)) + mel_weights_matrix[:, + i] = np.maximum(0.0, + np.minimum(lower_slope, upper_slope)) + mel_weights_matrix[0, :] = 0.0 + return mel_weights_matrix + + +def log_mel_spectrogram(data, + audio_sample_rate=8000, + log_offset=0.0, + window_length_secs=0.025, + hop_length_secs=0.010, + **kwargs): + """ + log_mel_spectrogram + """ + window_length_samples = int(round(audio_sample_rate * window_length_secs)) + #print("audio_sample_rate = ", audio_sample_rate) + #print("window_length_secs = ", window_length_secs) + #print("window_length_sample ", window_length_samples) + hop_length_samples = int(round(audio_sample_rate * hop_length_secs)) + #print("hop_length_samples ", hop_length_samples) + fft_length = 2**int(np.ceil(np.log(window_length_samples) / np.log(2.0))) + #print(" fft_lengt = ", fft_length) + spectrogram = stft_magnitude(data, + fft_length=fft_length, + hop_length=hop_length_samples, + window_length=window_length_samples) + #print(" spectrogram.shape = ", spectrogram.shape) + mel_spectrogram = np.dot( + spectrogram, + spectrogram_to_mel_matrix(num_spectrogram_bins=spectrogram.shape[1], + audio_sample_rate=audio_sample_rate, + **kwargs)) + + return np.log(mel_spectrogram + log_offset) + + +def wav_to_example(wav_data, sample_rate): + """ + wav_to_example + """ + #sample_rate, wav_data = wavfile.read(wav_file) + assert wav_data.dtype == np.int16, 'Bad sample type: %r' % wav_data.dtype + #wav_data = wav_data[:16000*30] + #print(" wav_data ", wav_data.shape) + #print(" wav_data ", wav_data.shape) + pad_zero_num = int(sample_rate * (vgg_params.STFT_WINDOW_LENGTH_SECONDS - + vgg_params.STFT_HOP_LENGTH_SECONDS)) + wav_data_extend = np.hstack((wav_data, np.zeros(pad_zero_num))) + wav_data = wav_data_extend + #print(" wav_data ", wav_data.shape) + wav_data = wav_data / 32768.0 # Convert to [-1.0, +1.0] + #print(" wav_data after convert to -1 1", wav_data) + #if wav_data.shape[0] > max_second * sample_rate: + # wav_data = wav_data[:max_second * sample_rate, :] + if len(wav_data.shape) > 1: + wav_data = np.mean(wav_data, axis=1) + #print(" wav_data after mean", wav_data.shape, len(wav_data.shape), wav_data) + # Resample to the rate assumed by vgg. + #if sample_rate != vgg_params.SAMPLE_RATE: + # wav_data = resampy.resample(wav_data, sample_rate, vgg_params.SAMPLE_RATE) + log_mel = log_mel_spectrogram( + wav_data, + audio_sample_rate=vgg_params.SAMPLE_RATE, + log_offset=vgg_params.LOG_OFFSET, + window_length_secs=vgg_params.STFT_WINDOW_LENGTH_SECONDS, + hop_length_secs=vgg_params.STFT_HOP_LENGTH_SECONDS, + num_mel_bins=vgg_params.NUM_MEL_BINS, + lower_edge_hertz=vgg_params.MEL_MIN_HZ, + upper_edge_hertz=vgg_params.MEL_MAX_HZ) + # Frame features into examples. + features_sample_rate = 1.0 / vgg_params.STFT_HOP_LENGTH_SECONDS + example_window_length = int( + round(vgg_params.EXAMPLE_WINDOW_SECONDS * features_sample_rate)) + + example_hop_length = int( + round(vgg_params.EXAMPLE_HOP_SECONDS * features_sample_rate)) + log_mel_examples = frame(log_mel, + window_length=example_window_length, + hop_length=example_hop_length) + return log_mel_examples + + +def extract_pcm(pcm_file, sample_rate): + with open(pcm_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + examples = wav_to_example(audio_data, sample_rate) + return examples + + +if __name__ == "__main__": + wav_file = sys.argv[1] + print("wav_file = ", wav_file) + with open(wav_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype = np.int16) + examples_batch = wav_to_example(audio_data, 16000) + print("examples_batch.shape", examples_batch.shape) diff --git a/applications/FootballAction/predict/action_detect/mfcc/model_config.py b/applications/FootballAction/predict/action_detect/mfcc/model_config.py new file mode 100644 index 0000000000000000000000000000000000000000..194365ece7545bd469c8ad23a46e7e461ac29edf --- /dev/null +++ b/applications/FootballAction/predict/action_detect/mfcc/model_config.py @@ -0,0 +1,51 @@ +""" +audio model config +""" +import numpy as np + +import mfcc.feature_extractor as feature_extractor + + +class ModelAudio(object): + """ + modelAudio + """ + def __init__(self, configs, use_gpu=1): + self.use_gpu = use_gpu + + self.audio_fps = configs.COMMON.fps + self.audio_feat_scale = configs.TSN.audio_scale + self.sample_rate = 16000 + + def predict_slice(self, wav_data, sample_rate): + """ + audio predict + """ + examples_batch = feature_extractor.wav_to_example( + wav_data, sample_rate)[0] + return examples_batch + + def predict_audio(self, audio_file): + """ + predict_audio + """ + audio_feature_list = [] + # read pcm + sample_rate = self.sample_rate + try: + with open(audio_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + audio_status = "audio load success" + except Exception as e: + audio_data = [] + audio_status = "audio load failed" + step = 1 + len_video = int(len(audio_data) / sample_rate) + print(len_video) + for i in range(0, len_video, step): + audio_data_part = audio_data[i * sample_rate:(i + step) * + sample_rate] + feature_audio = self.predict_slice(audio_data_part, sample_rate) + audio_feature_list.append(feature_audio) + return audio_feature_list diff --git a/applications/FootballAction/predict/action_detect/mfcc/vgg_params.py b/applications/FootballAction/predict/action_detect/mfcc/vgg_params.py new file mode 100755 index 0000000000000000000000000000000000000000..0a995196103f12e1d975d4d7ee7eaa122a19fb5f --- /dev/null +++ b/applications/FootballAction/predict/action_detect/mfcc/vgg_params.py @@ -0,0 +1,37 @@ +"""Global parameters for the VGGish model. +See vggish_slim.py for more information. +""" + +# Architectural constants. +NUM_FRAMES = 50 # Frames in input mel-spectrogram patch. +NUM_BANDS = 64 # Frequency bands in input mel-spectrogram patch. +EMBEDDING_SIZE = 128 # Size of embedding layer. + +# Hyperparameters used in feature and example generation. +SAMPLE_RATE = 16000 +STFT_WINDOW_LENGTH_SECONDS = 0.040 +STFT_HOP_LENGTH_SECONDS = 0.020 +NUM_MEL_BINS = NUM_BANDS +MEL_MIN_HZ = 125 +MEL_MAX_HZ = 7500 +LOG_OFFSET = 0.01 # Offset used for stabilized log of input mel-spectrogram. +EXAMPLE_WINDOW_SECONDS = 1.00 # Each example contains 96 10ms frames +EXAMPLE_HOP_SECONDS = 1.00 # with zero overlap. + +# Parameters used for embedding postprocessing. +PCA_EIGEN_VECTORS_NAME = 'pca_eigen_vectors' +PCA_MEANS_NAME = 'pca_means' +QUANTIZE_MIN_VAL = -2.0 +QUANTIZE_MAX_VAL = +2.0 + +# Hyperparameters used in training. +INIT_STDDEV = 0.01 # Standard deviation used to initialize weights. +LEARNING_RATE = 1e-4 # Learning rate for the Adam optimizer. +ADAM_EPSILON = 1e-8 # Epsilon for the Adam optimizer. + +# Names of ops, tensors, and features. +INPUT_OP_NAME = 'vggish/input_features' +INPUT_TENSOR_NAME = INPUT_OP_NAME + ':0' +OUTPUT_OP_NAME = 'vggish/embedding' +OUTPUT_TENSOR_NAME = OUTPUT_OP_NAME + ':0' +AUDIO_EMBEDDING_FEATURE_NAME = 'audio_embedding' diff --git a/applications/FootballAction/predict/action_detect/models/__init__.py b/applications/FootballAction/predict/action_detect/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/FootballAction/predict/action_detect/models/audio_infer.py b/applications/FootballAction/predict/action_detect/models/audio_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..7b19c90ed369572bd78ad6fa5cf619d1bda8dc61 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/models/audio_infer.py @@ -0,0 +1,80 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """audio infer""" + def __init__(self, cfg, name='AUDIO'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + self.output_tensor = self.predictor.get_output_handle(output_names[0]) + + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output = self.output_tensor.copy_to_cpu() + return output + + + def predict(self, infer_config): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config) + feature_list = [] + pcm_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = np.array(data, dtype = 'float32') + output = self.infer(inputs) + feature_list.append(np.squeeze(output)) + pcm_list.append(inputs) + feature_values = np.vstack(feature_list) + pcm_values = np.vstack(pcm_list) + return feature_values, pcm_values + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + pcm_path = '/home/work/datasets/WorldCup2018/pcm/6e577252c4004961ac7caa738a52c238.pcm' + t0 = time.time() + cfg['AUDIO']['pcm_file'] = pcm_path + outputs = model.predict(cfg) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + t1 = time.time() + + print(outputs.shape) + print(outputs[0]) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/FootballAction/predict/action_detect/models/bmn_infer.py b/applications/FootballAction/predict/action_detect/models/bmn_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..963f756690f1a70da2091f9f711d259e95f37492 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/models/bmn_infer.py @@ -0,0 +1,156 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import json +import pickle +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config +from utils.process_result import process_proposal + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """bmn infer""" + def __init__(self, cfg, name='BMN'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + self.nms_thread = cfg[name]['nms_thread'] + self.min_pred_score = cfg[name]['score_thread'] + self.min_frame_thread = cfg['COMMON']['fps'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + self.output1_tensor = self.predictor.get_output_handle(output_names[0]) + self.output2_tensor = self.predictor.get_output_handle(output_names[1]) + self.output3_tensor = self.predictor.get_output_handle(output_names[2]) + + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output1 = self.output1_tensor.copy_to_cpu() + output2 = self.output2_tensor.copy_to_cpu() + output3 = self.output3_tensor.copy_to_cpu() + return output1, output2, output3 + + + def generate_props(self, pred_bmn, pred_start, pred_end, max_window=200, min_window=5): + """generate_props""" + video_len = min(pred_bmn.shape[-1], min(pred_start.shape[-1], pred_end.shape[-1])) + pred_bmn = pred_bmn[0, :, :] * pred_bmn[1, :, :] + start_mask = self.boundary_choose(pred_start) + start_mask[0] = 1. + end_mask = self.boundary_choose(pred_end) + end_mask[-1] = 1. + score_results = [] + for idx in range(min_window, max_window): + for jdx in range(video_len): + start_index = jdx + end_index = start_index + idx + if end_index < video_len and start_mask[start_index] == 1 and end_mask[end_index] == 1: + xmin = start_index + xmax = end_index + xmin_score = pred_start[start_index] + xmax_score = pred_end[end_index] + bmn_score = pred_bmn[idx, jdx] + conf_score = xmin_score * xmax_score * bmn_score + score_results.append([xmin, xmax, conf_score]) + return score_results + + + def boundary_choose(self, score_list): + """boundary_choose""" + max_score = max(score_list) + mask_high = (score_list > max_score * 0.5) + score_list = list(score_list) + score_middle = np.array([0.0] + score_list + [0.0]) + score_front = np.array([0.0, 0.0] + score_list) + score_back = np.array(score_list + [0.0, 0.0]) + mask_peak = ((score_middle > score_front) & (score_middle > score_back)) + mask_peak = mask_peak[1:-1] + mask = (mask_high | mask_peak).astype('float32') + return mask + + + def predict(self, infer_config, material): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config, material=material) + feature_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = [items[0] for items in data] + winds = [items[1] for items in data] + feat_info = [items[2] for items in data] + feature_T = feat_info[0][0] + feature_N = feat_info[0][1] + + inputs = np.array(inputs) + pred_bmn, pred_sta, pred_end = self.infer(inputs) + + if infer_iter == 0: + sum_pred_bmn = np.zeros((2, feature_N, feature_T)) + sum_pred_sta = np.zeros((feature_T, )) + sum_pred_end = np.zeros((feature_T, )) + sum_pred_cnt = np.zeros((feature_T, )) + + for idx, sub_wind in enumerate(winds): + sum_pred_bmn[:, :, sub_wind[0]: sub_wind[1]] += pred_bmn[idx] + sum_pred_sta[sub_wind[0]: sub_wind[1]] += pred_sta[idx] + sum_pred_end[sub_wind[0]: sub_wind[1]] += pred_end[idx] + sum_pred_cnt[sub_wind[0]: sub_wind[1]] += np.ones((sub_wind[1] - sub_wind[0], )) + + pred_bmn = sum_pred_bmn / sum_pred_cnt + pred_sta = sum_pred_sta / sum_pred_cnt + pred_end = sum_pred_end / sum_pred_cnt + + score_result = self.generate_props(pred_bmn, pred_sta, pred_end) + results = process_proposal(score_result, self.min_frame_thread, self.nms_thread, self.min_pred_score) + + return results + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238' + + # feature + feature_path = imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + t0 = time.time() + outputs = model.predict(cfg, video_features) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + t1 = time.time() + + results = {'proposal': outputs} + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/FootballAction/predict/action_detect/models/lstm_infer.py b/applications/FootballAction/predict/action_detect/models/lstm_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..acb3874228402a8fe3307e8964c752fb4c2fe11e --- /dev/null +++ b/applications/FootballAction/predict/action_detect/models/lstm_infer.py @@ -0,0 +1,152 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import json +import pickle +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config +from utils.process_result import get_action_result + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """lstm infer""" + def __init__(self, cfg, name='ACTION'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + self.topk = cfg[name]['topk'] + self.frame_offset = cfg[name]['nms_offset'] + self.nms_thread = cfg[name]['nms_thread'] + self.cls_thread = cfg[name]['classify_score_thread'] + self.iou_thread = cfg[name]['iou_score_thread'] + + self.label_map_file = cfg['COMMON']['label_dic'] + self.fps = cfg['COMMON']['fps'] + self.nms_id = 5 + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input1_tensor = self.predictor.get_input_handle(input_names[0]) + self.input2_tensor = self.predictor.get_input_handle(input_names[1]) + + output_names = self.predictor.get_output_names() + self.output1_tensor = self.predictor.get_output_handle(output_names[0]) + self.output2_tensor = self.predictor.get_output_handle(output_names[1]) + + + def infer(self, input1_arr, input1_lod, input2_arr=None, input2_lod=None): + """infer""" + self.input1_tensor.copy_from_cpu(input1_arr) + self.input1_tensor.set_lod(input1_lod) + if not input2_arr is None: + self.input2_tensor.copy_from_cpu(input2_arr) + self.input2_tensor.set_lod(input2_lod) + self.predictor.run() + output1 = self.output1_tensor.copy_to_cpu() + output2 = self.output2_tensor.copy_to_cpu() + # print(output.shape) + return output1, output2 + + def pre_process(self, input): + """pre process""" + input_arr = [] + input_lod = [0] + start_lod = 0 + end_lod = 0 + for sub_item in input: + end_lod = start_lod + len(sub_item) + input_lod.append(end_lod) + input_arr.extend(sub_item) + start_lod = end_lod + input_arr = np.array(input_arr) + # print(input_arr.shape) + # print([input_lod]) + return input_arr, [input_lod] + + def predict(self, infer_config, material): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config, material=material) + results = [] + for infer_iter, data in enumerate(infer_reader()): + video_id = [[items[-2], items[-1]] for items in data] + input1 = [items[0] for items in data] + input2 = [items[1] for items in data] + input1_arr, input1_lod = self.pre_process(input1) + input2_arr, input2_lod = self.pre_process(input2) + output1, output2 = self.infer(input1_arr, input1_lod, input2_arr, input2_lod) + # output1, output2 = self.infer(input1_arr, input1_lod) + + predictions_id = output1 + predictions_iou = output2 + for i in range(len(predictions_id)): + topk_inds = predictions_id[i].argsort()[0 - self.topk:] + topk_inds = topk_inds[::-1] + preds_id = predictions_id[i][topk_inds] + preds_iou = predictions_iou[i][0] + results.append((video_id[i], preds_id.tolist(), topk_inds.tolist(), preds_iou.tolist())) + + predict_result = get_action_result(results, self.label_map_file, self.fps, + self.cls_thread, self.iou_thread, + self.nms_id, self.nms_thread, self.frame_offset) + return predict_result + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + # proposal total + prop_dict = {} + for dataset in ['EuroCup2016', 'WorldCup2018']: + prop_json = '/home/work/datasets/{}/feature_bmn/prop.json'.format(dataset) + json_data = json.load(open(prop_json, 'r')) + for item in json_data: + basename = prop_json.replace('feature_bmn/prop.json', 'mp4') + basename = basename + '/' + item['video_name'] + '.mp4' + prop_dict[basename] = item['bmn_results'] + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238' + + # feature + feature_path = imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + # proposal + basename = imgs_path.replace('frames', 'mp4') + '.mp4' + bmn_results = prop_dict[basename] + + material = {'feature': video_features, 'proposal': bmn_results} + + t0 = time.time() + outputs = model.predict(cfg, material) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + # print(outputs.shape) + t1 = time.time() + results = {'actions': outputs} + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/FootballAction/predict/action_detect/models/pptsm_infer.py b/applications/FootballAction/predict/action_detect/models/pptsm_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..94ee4d973799fbae201e883aafccb835da31fa5f --- /dev/null +++ b/applications/FootballAction/predict/action_detect/models/pptsm_infer.py @@ -0,0 +1,80 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """pptsm infer""" + def __init__(self, cfg, name='PPTSM'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + self.output_tensor = self.predictor.get_output_handle(output_names[1]) + #self.output_tensor = self.predictor.get_output_handle(output_names[0]) + + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output = self.output_tensor.copy_to_cpu() + return output + + + def predict(self, infer_config): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config) + feature_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = [items[:-1] for items in data] + inputs = np.array(inputs) + output = self.infer(inputs) + feature_list.append(np.squeeze(output)) + feature_list = np.vstack(feature_list) + return feature_list + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238/' + imgs_list = get_images(imgs_path) + t0 = time.time() + cfg['PPTSM']['frame_list'] = imgs_list + outputs = model.predict(cfg) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + t1 = time.time() + + print(outputs.shape) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/FootballAction/predict/action_detect/reader/__init__.py b/applications/FootballAction/predict/action_detect/reader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..547b2d6bb2495dadccd53e178ab80cb656a66496 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/reader/__init__.py @@ -0,0 +1,15 @@ +""" +read map for model +""" +from reader.reader_utils import regist_reader, get_reader +import reader.tsminf_reader as tsminf_reader +import reader.audio_reader as audio_reader +import reader.bmninf_reader as bmninf_reader +import reader.feature_reader as feature_reader + +# regist reader, sort by alphabet +regist_reader("TSM", tsminf_reader.TSMINFReader) +regist_reader("PPTSM", tsminf_reader.TSMINFReader) +regist_reader("AUDIO", audio_reader.AudioReader) +regist_reader("BMN", bmninf_reader.BMNINFReader) +regist_reader("ACTION", feature_reader.FeatureReader) diff --git a/applications/FootballAction/predict/action_detect/reader/audio_reader.py b/applications/FootballAction/predict/action_detect/reader/audio_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..2e1f1d28f7f21d329923841c2f04c3e857778425 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/reader/audio_reader.py @@ -0,0 +1,78 @@ +""" +audio reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import os +import _pickle as cPickle +#from .reader_utils import DataReader +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle + from io import BytesIO +import numpy as np +import random +import code + +from .reader_utils import DataReader +import mfcc.feature_extractor as feature_extractor + +class AudioReader(DataReader): + """ + Data reader for youtube-8M dataset, which was stored as features extracted by prior networks + This is for the three models: lstm, attention cluster, nextvlad + + dataset cfg: num_classes + batch_size + list + NextVlad only: eigen_file + """ + + def __init__(self, name, mode, cfg, material=None): + self.name = name + self.mode = mode + + # set batch size and file list + self.sample_rate = cfg[self.name.upper()]['sample_rate'] + self.batch_size = cfg[self.name.upper()]['batch_size'] + self.pcm_file = cfg[self.name.upper()]['pcm_file'] + self.material = material + + def create_reader(self): + """create_reader""" + with open(self.pcm_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + examples = feature_extractor.wav_to_example(audio_data, self.sample_rate) + # print(examples.shape) + + def reader(): + """reader""" + batch_out = [] + batch_out_pre = [] + + for audio in examples: + # batch_out.append([audio]) + batch_out.append(audio) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + if len(batch_out) > 0: + yield batch_out + + return reader diff --git a/applications/FootballAction/predict/action_detect/reader/bmninf_reader.py b/applications/FootballAction/predict/action_detect/reader/bmninf_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..a076f2bfefeec5c34c38bc43bfd466c6efaa9fdf --- /dev/null +++ b/applications/FootballAction/predict/action_detect/reader/bmninf_reader.py @@ -0,0 +1,155 @@ +""" +# @File : bmninf_reader.py +# @Author: macaihong +# @Date : 2019/12/15 +# @Desc : +""" + +import os +import random +import pickle +import json +import numpy as np +import multiprocessing + +import numpy as np + +from .reader_utils import DataReader + + +def get_sw_prop(duration, window=200, step=10): + """ + get_sw_prop + """ + pr = [] + local_boxes = [] + for k in np.arange(0, duration - window + step, step): + start_id = k + end_id = min(duration, k + window) + if end_id - start_id < window: + start_id = end_id - window + local_boxes = (start_id, end_id) + pr.append(local_boxes) + + def valid_proposal(duration, span): + """ + valid_proposal + """ + # fileter proposals + # a valid proposal should have at least one second in the video + real_span = min(duration, span[1]) - span[0] + return real_span >= 1 + + pr = list(filter(lambda x: valid_proposal(duration, x), pr)) + return pr + + +class BMNINFReader(DataReader): + """ + Data reader for BMN model, which was stored as features extracted by prior networks + dataset cfg: feat_path, feature path, + tscale, temporal length of BM map, + dscale, duration scale of BM map, + anchor_xmin, anchor_xmax, the range of each point in the feature sequence, + batch_size, batch size of input data, + num_threads, number of threads of data processing + """ + + def __init__(self, name, mode, cfg, material=None): + self.name = name + self.mode = mode + self.tscale = cfg[self.name.upper()]['tscale'] # 200 + self.dscale = cfg[self.name.upper()]['dscale'] # 200 + # self.subset = cfg[self.name.upper()]['subset'] + self.tgap = 1. / self.tscale + self.step = cfg[self.name.upper()]['window_step'] + + self.material = material + src_feature = self.material + + image_feature = src_feature['image_feature'] + pcm_feature = src_feature['pcm_feature'] + pcm_feature = pcm_feature.reshape((pcm_feature.shape[0] * 5, 640)) + # print(rgb_feature.shape, audio_feature.shape, pcm_feature.shape) + min_length = min(image_feature.shape[0], pcm_feature.shape[0]) + #if min_length == 0: + # continue + image_feature = image_feature[:min_length, :] + pcm_feature = pcm_feature[:min_length, :] + self.features = np.concatenate((image_feature, pcm_feature), axis=1) + + self.duration = len(self.features) + self.window = self.tscale + + self.get_dataset_dict() + self.get_match_map() + + self.batch_size = cfg[self.name.upper()]['batch_size'] + if (mode == 'test') or (mode == 'infer'): + self.num_threads = 1 # set num_threads as 1 for test and infer + + def get_dataset_dict(self): + """ + get_dataset_dict + """ + self.video_list = get_sw_prop(self.duration, self.window, self.step) + + def get_match_map(self): + """ + get_match_map + """ + match_map = [] + for idx in range(self.tscale): + tmp_match_window = [] + xmin = self.tgap * idx + for jdx in range(1, self.tscale + 1): + xmax = xmin + self.tgap * jdx + tmp_match_window.append([xmin, xmax]) + match_map.append(tmp_match_window) + match_map = np.array(match_map) + match_map = np.transpose(match_map, [1, 0, 2]) + match_map = np.reshape(match_map, [-1, 2]) + self.match_map = match_map + self.anchor_xmin = [self.tgap * i for i in range(self.tscale)] + self.anchor_xmax = [self.tgap * i for i in range(1, self.tscale + 1)] + + + def load_file(self, video_wind): + """ + load_file + """ + start_feat_id = video_wind[0] + end_feat_id = video_wind[1] + video_feat = self.features[video_wind[0]: video_wind[1]] + video_feat = video_feat.T + video_feat = video_feat.astype("float32") + return video_feat + + def create_reader(self): + """ + reader creator for ctcn model + """ + return self.make_infer_reader() + + def make_infer_reader(self): + """ + reader for inference + """ + def reader(): + """ + reader + """ + batch_out = [] + # for video_name in self.video_list: + for video_wind in self.video_list: + video_idx = self.video_list.index(video_wind) + video_feat = self.load_file(video_wind) + batch_out.append((video_feat, video_wind, [self.duration, self.dscale])) + + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + if len(batch_out) > 0: + yield batch_out + + return reader diff --git a/applications/FootballAction/predict/action_detect/reader/feature_reader.py b/applications/FootballAction/predict/action_detect/reader/feature_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..4e406f739980e8db03a7c96f01f1abf5d64294cd --- /dev/null +++ b/applications/FootballAction/predict/action_detect/reader/feature_reader.py @@ -0,0 +1,87 @@ +""" +attention-lstm feature reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle +import numpy as np +import random +import code + +from .reader_utils import DataReader + +class FeatureReader(DataReader): + """ + Data reader for youtube-8M dataset, which was stored as features extracted by prior networks + This is for the three models: lstm, attention cluster, nextvlad + + dataset cfg: num_classes + batch_size + list + NextVlad only: eigen_file + """ + + def __init__(self, name, mode, cfg, material=None): + self.name = name + self.mode = mode + self.batch_size = cfg[self.name.upper()]['batch_size'] + + self.feature = material['feature'] + self.proposal = material['proposal'] + self.fps = 5 + + def create_reader(self): + """ + create_reader + """ + image_feature_list = self.feature['image_feature'] + audio_feature_list = self.feature['audio_feature'] + pcm_feature_list = self.feature['pcm_feature'] + pcm_feature_list = pcm_feature_list.reshape((pcm_feature_list.shape[0] * 5, 640)) + + fl = self.proposal + + if self.mode == 'train': + random.shuffle(fl) + + def reader(): + """ + reader + """ + batch_out = [] + for prop_info in fl: + start_id = int(prop_info['start']) + end_id = int(prop_info['end']) + bmn_score = float(prop_info['score']) + try: + image_feature = image_feature_list[start_id: end_id] + audio_feature = audio_feature_list[int(start_id / self.fps): int(end_id / self.fps)] + pcm_feature = pcm_feature_list[start_id: end_id] + + # image_feature = np.concatenate((image_feature, pcm_feature), axis=1) + + batch_out.append((image_feature, audio_feature, 0, prop_info)) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + except Exception as e: + continue + return reader + diff --git a/applications/FootballAction/predict/action_detect/reader/reader_utils.py b/applications/FootballAction/predict/action_detect/reader/reader_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f76b5d38d10100a4362b8f9fe9a7186f7284e840 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/reader/reader_utils.py @@ -0,0 +1,109 @@ +""" +reader_util +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import numpy as np + + +class ReaderNotFoundError(Exception): + """ + "Error: reader not found" + """ + + def __init__(self, reader_name, avail_readers): + super(ReaderNotFoundError, self).__init__() + self.reader_name = reader_name + self.avail_readers = avail_readers + + def __str__(self): + msg = "Reader {} Not Found.\nAvailiable readers:\n".format( + self.reader_name) + for reader in self.avail_readers: + msg += " {}\n".format(reader) + return msg + + +class DataReader(object): + """ + data reader for video input + """ + + def __init__(self, model_name, mode, cfg): + self.name = model_name + self.mode = mode + self.cfg = cfg + + def create_reader(self): + """ + Not implemented + """ + pass + + def get_config_from_sec(self, sec, item, default=None): + """ + get_config_from_sec + """ + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + +class ReaderZoo(object): + """ + ReaderZoo + """ + def __init__(self): + """ + __init__ + """ + self.reader_zoo = {} + + def regist(self, name, reader): + """ + regist + """ + assert reader.__base__ == DataReader, "Unknow model type {}".format( + type(reader)) + self.reader_zoo[name] = reader + + def get(self, name, mode, cfg, material=None): + """ + get + """ + for k, v in self.reader_zoo.items(): + if k == name: + return v(name, mode, cfg, material) + raise ReaderNotFoundError(name, self.reader_zoo.keys()) + + +# singleton reader_zoo +reader_zoo = ReaderZoo() + + +def regist_reader(name, reader): + """ + regist_reader + """ + reader_zoo.regist(name, reader) + + +def get_reader(name, mode, cfg, material=None): + """ + get_reader + """ + reader_model = reader_zoo.get(name, mode, cfg, material) + return reader_model.create_reader() diff --git a/applications/FootballAction/predict/action_detect/reader/tsminf_reader.py b/applications/FootballAction/predict/action_detect/reader/tsminf_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..9886d54244bfdf53fb5b29c6749cf1ed042b27b0 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/reader/tsminf_reader.py @@ -0,0 +1,358 @@ +""" +tsn frame reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import random +import functools +import concurrent.futures +import multiprocessing + +import numpy as np +import paddle +from PIL import Image, ImageEnhance + +from .reader_utils import DataReader + + +class TSMINFReader(DataReader): + """ + Data reader for video dataset of jpg folder. + """ + + def __init__(self, name, mode, cfg, material=None): + super(TSMINFReader, self).__init__(name, mode, cfg) + name = name.upper() + self.seg_num = cfg[name]['seg_num'] + self.seglen = cfg[name]['seglen'] + self.short_size = cfg[name]['short_size'] + self.target_size = cfg[name]['target_size'] + self.batch_size = cfg[name]['batch_size'] + self.reader_threads = cfg[name]['reader_threads'] + self.buf_size = cfg[name]['buf_size'] + self.video_path = cfg[name]['frame_list'] + + self.img_mean = np.array(cfg[name]['image_mean']).reshape([3, 1, 1]).astype(np.float32) + self.img_std = np.array(cfg[name]['image_std']).reshape([3, 1, 1]).astype(np.float32) + + self.material = material + + def create_reader(self): + """ + batch loader for TSN + """ + _reader = self._inference_reader_creator_longvideo( + self.video_path, + self.mode, + seg_num=self.seg_num, + seglen=self.seglen, + short_size=self.short_size, + target_size=self.target_size, + img_mean=self.img_mean, + img_std=self.img_std, + num_threads = self.reader_threads, + buf_size = self.buf_size) + + def _batch_reader(): + batch_out = [] + for imgs, label in _reader(): + if imgs is None: + continue + batch_out.append((imgs, label)) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + if len(batch_out) > 1: + yield batch_out[:-1] + + return _batch_reader + + + def _inference_reader_creator_longvideo(self, video_path, mode, seg_num, seglen, + short_size, target_size, img_mean, img_std, num_threads, buf_size): + """ + inference reader for video + """ + def reader(): + """ + reader + """ + def image_buf(image_id_path_buf): + """ + image_buf reader + """ + try: + img_path = image_id_path_buf[1] + img = Image.open(img_path).convert("RGB") + image_id_path_buf[2] = img + except: + image_id_path_buf[2] = None + + frame_len = len(video_path) + read_thread_num = seg_num + for i in range(0, frame_len, read_thread_num): + image_list_part = video_path[i: i + read_thread_num] + image_id_path_buf_list = [] + for k in range(len(image_list_part)): + image_id_path_buf_list.append([k, image_list_part[k], None]) + + + with concurrent.futures.ThreadPoolExecutor(max_workers=read_thread_num) as executor: + executor.map(lambda image_id_path_buf: image_buf(image_id_path_buf), image_id_path_buf_list) + imgs_seg_list = [x[2] for x in image_id_path_buf_list] + + # add the fault-tolerant for bad image + for k in range(len(image_id_path_buf_list)): + img_buf = image_id_path_buf_list[k][2] + pad_id = 1 + while pad_id < seg_num and img_buf is None: + img_buf = imgs_seg_list[(k + pad_id)%seg_num][2] + if img_buf is None: + logger.info("read img erro from {} to {}".format(i, i + read_thread_num)) + exit(0) + else: + imgs_seg_list[k] = img_buf + for pad_id in range(len(imgs_seg_list), seg_num): + imgs_seg_list.append(imgs_seg_list[-1]) + yield imgs_seg_list + + + def inference_imgs_transform(imgs_list, mode, seg_num, seglen, short_size,\ + target_size, img_mean, img_std): + """ + inference_imgs_transform + """ + imgs_ret = imgs_transform(imgs_list, mode, seg_num, seglen, short_size, + target_size, img_mean, img_std) + label_ret = 0 + + return imgs_ret, label_ret + + mapper = functools.partial( + inference_imgs_transform, + mode=mode, + seg_num=seg_num, + seglen=seglen, + short_size=short_size, + target_size=target_size, + img_mean=img_mean, + img_std=img_std) + + return paddle.reader.xmap_readers(mapper, reader, num_threads, buf_size, order=True) + + +def imgs_transform(imgs, + mode, + seg_num, + seglen, + short_size, + target_size, + img_mean, + img_std, + name=''): + """ + imgs_transform + """ + imgs = group_scale(imgs, short_size) + + if mode == 'train': + if name == "TSM": + imgs = group_multi_scale_crop(imgs, short_size) + imgs = group_random_crop(imgs, target_size) + imgs = group_random_flip(imgs) + else: + imgs = group_center_crop(imgs, target_size) + + np_imgs = (np.array(imgs[0]).astype('float32').transpose( + (2, 0, 1))).reshape(1, 3, target_size, target_size) / 255 + for i in range(len(imgs) - 1): + img = (np.array(imgs[i + 1]).astype('float32').transpose( + (2, 0, 1))).reshape(1, 3, target_size, target_size) / 255 + np_imgs = np.concatenate((np_imgs, img)) + imgs = np_imgs + imgs -= img_mean + imgs /= img_std + imgs = np.reshape(imgs, (seg_num, seglen * 3, target_size, target_size)) + + return imgs + +def group_multi_scale_crop(img_group, target_size, scales=None, \ + max_distort=1, fix_crop=True, more_fix_crop=True): + """ + group_multi_scale_crop + """ + scales = scales if scales is not None else [1, .875, .75, .66] + input_size = [target_size, target_size] + + im_size = img_group[0].size + + # get random crop offset + def _sample_crop_size(im_size): + """ + _sample_crop_size + """ + image_w, image_h = im_size[0], im_size[1] + + base_size = min(image_w, image_h) + crop_sizes = [int(base_size * x) for x in scales] + crop_h = [ + input_size[1] if abs(x - input_size[1]) < 3 else x + for x in crop_sizes + ] + crop_w = [ + input_size[0] if abs(x - input_size[0]) < 3 else x + for x in crop_sizes + ] + + pairs = [] + for i, h in enumerate(crop_h): + for j, w in enumerate(crop_w): + if abs(i - j) <= max_distort: + pairs.append((w, h)) + + crop_pair = random.choice(pairs) + if not fix_crop: + w_offset = random.randint(0, image_w - crop_pair[0]) + h_offset = random.randint(0, image_h - crop_pair[1]) + else: + w_step = (image_w - crop_pair[0]) / 4 + h_step = (image_h - crop_pair[1]) / 4 + + ret = list() + ret.append((0, 0)) # upper left + if w_step != 0: + ret.append((4 * w_step, 0)) # upper right + if h_step != 0: + ret.append((0, 4 * h_step)) # lower left + if h_step != 0 and w_step != 0: + ret.append((4 * w_step, 4 * h_step)) # lower right + if h_step != 0 or w_step != 0: + ret.append((2 * w_step, 2 * h_step)) # center + + if more_fix_crop: + ret.append((0, 2 * h_step)) # center left + ret.append((4 * w_step, 2 * h_step)) # center right + ret.append((2 * w_step, 4 * h_step)) # lower center + ret.append((2 * w_step, 0 * h_step)) # upper center + + ret.append((1 * w_step, 1 * h_step)) # upper left quarter + ret.append((3 * w_step, 1 * h_step)) # upper right quarter + ret.append((1 * w_step, 3 * h_step)) # lower left quarter + ret.append((3 * w_step, 3 * h_step)) # lower righ quarter + + w_offset, h_offset = random.choice(ret) + crop_info = { + 'crop_w': crop_pair[0], + 'crop_h': crop_pair[1], + 'offset_w': w_offset, + 'offset_h': h_offset + } + + return crop_info + + crop_info = _sample_crop_size(im_size) + crop_w = crop_info['crop_w'] + crop_h = crop_info['crop_h'] + offset_w = crop_info['offset_w'] + offset_h = crop_info['offset_h'] + crop_img_group = [ + img.crop((offset_w, offset_h, offset_w + crop_w, offset_h + crop_h)) + for img in img_group + ] + ret_img_group = [ + img.resize((input_size[0], input_size[1]), Image.BILINEAR) + for img in crop_img_group + ] + + return ret_img_group + + +def group_random_crop(img_group, target_size): + """ + group_random_crop + """ + w, h = img_group[0].size + th, tw = target_size, target_size + + assert (w >= target_size) and (h >= target_size), \ + "image width({}) and height({}) should be larger than crop size".format(w, h) + + out_images = [] + x1 = random.randint(0, w - tw) + y1 = random.randint(0, h - th) + + for img in img_group: + if w == tw and h == th: + out_images.append(img) + else: + out_images.append(img.crop((x1, y1, x1 + tw, y1 + th))) + + return out_images + + +def group_random_flip(img_group): + """ + group_random_flip + """ + v = random.random() + if v < 0.5: + ret = [img.transpose(Image.FLIP_LEFT_RIGHT) for img in img_group] + return ret + else: + return img_group + + +def group_center_crop(img_group, target_size): + """ + group_center_crop + """ + img_crop = [] + for img in img_group: + w, h = img.size + th, tw = target_size, target_size + assert (w >= target_size) and (h >= target_size), \ + "image width({}) and height({}) should be larger than crop size".format(w, h) + x1 = int(round((w - tw) / 2.)) + y1 = int(round((h - th) / 2.)) + img_crop.append(img.crop((x1, y1, x1 + tw, y1 + th))) + + return img_crop + + +def group_scale(imgs, target_size): + """ + group_scale + """ + resized_imgs = [] + for i in range(len(imgs)): + img = imgs[i] + w, h = img.size + if (w <= h and w == target_size) or (h <= w and h == target_size): + resized_imgs.append(img) + continue + + if w < h: + ow = target_size + oh = int(target_size * 4.0 / 3.0) + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + else: + oh = target_size + ow = int(target_size * 4.0 / 3.0) + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + + return resized_imgs + diff --git a/applications/FootballAction/predict/action_detect/utils/__init__.py b/applications/FootballAction/predict/action_detect/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/FootballAction/predict/action_detect/utils/config_utils.py b/applications/FootballAction/predict/action_detect/utils/config_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e5db92b0d8f7ce5d5ff8d14635e53996dafb3ac3 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/utils/config_utils.py @@ -0,0 +1,80 @@ +""" +config_utils +""" +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import yaml +import ast + +import logger + +logger = logger.Logger() + +CONFIG_SECS = [ + 'train', + 'valid', + 'test', + 'infer', +] + +class AttrDict(dict): + """ + AttrDict + """ + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value + + +def parse_config(cfg_file): + """Load a config file into AttrDict""" + import yaml + with open(cfg_file, 'r') as fopen: + yaml_config = AttrDict(yaml.load(fopen, Loader=yaml.Loader)) + create_attr_dict(yaml_config) + return yaml_config + + +def create_attr_dict(yaml_config): + """create_attr_dict""" + for key, value in yaml_config.items(): + if isinstance(value, dict): + yaml_config[key] = value = AttrDict(value) + if isinstance(value, str): + try: + value = ast.literal_eval(value) + except BaseException: + pass + if isinstance(value, AttrDict): + create_attr_dict(yaml_config[key]) + else: + yaml_config[key] = value + return + + +def print_configs(cfg, mode): + """print_configs""" + logger.info("---------------- {:>5} Arguments ----------------".format( + mode)) + for sec, sec_items in cfg.items(): + logger.info("{}:".format(sec)) + for k, v in sec_items.items(): + logger.info(" {}:{}".format(k, v)) + logger.info("-------------------------------------------------") diff --git a/applications/FootballAction/predict/action_detect/utils/preprocess.py b/applications/FootballAction/predict/action_detect/utils/preprocess.py new file mode 100644 index 0000000000000000000000000000000000000000..d14aaf1eecaef7ab853fad2b2ef82ce9077c2677 --- /dev/null +++ b/applications/FootballAction/predict/action_detect/utils/preprocess.py @@ -0,0 +1,36 @@ +""" extract frames and pcm""" +import os +import sys +import shutil + + +def ffmpeg_frames(mp4_addr, frame_out_folder, fps=5): + """ffmpeg_frames""" + if os.path.exists(frame_out_folder): + shutil.rmtree(frame_out_folder) + os.makedirs(frame_out_folder) + cmd = './src/utils/ffmpeg -v 0 -i %s -r %d -q 0 %s/%s.jpg' % (mp4_addr, fps, frame_out_folder, '%08d') + os.system(cmd) + + +def ffmpeg_pcm(mp4_addr, save_file_name): + """ffmpeg_pcm""" + cmd = './src/utils/ffmpeg -y -i %s -acodec pcm_s16le -f s16le -ac 1 -ar 16000 %s -v 0' \ + % (mp4_addr, save_file_name) + os.system(cmd) + + +def ffmpeg_mp4(mp4_url, mp4_addr): + """ffmpeg_mp4""" + cmd = "wget %s -O %s -q" % (mp4_url, mp4_addr) + print ("cmd = ", cmd) + os.system(cmd) + + +def get_images(image_path): + """get_images""" + images = sorted(os.listdir(image_path)) + images = images + images_path_list = [image_path + '/' + im for im in images] + return images_path_list + diff --git a/applications/FootballAction/predict/action_detect/utils/process_result.py b/applications/FootballAction/predict/action_detect/utils/process_result.py new file mode 100644 index 0000000000000000000000000000000000000000..164869696cbbb95d0bbdbf697aec355f7e70b93b --- /dev/null +++ b/applications/FootballAction/predict/action_detect/utils/process_result.py @@ -0,0 +1,144 @@ +""" +# @File : process_result.py +# @Author: macaihong +# @Date : 2019/12/15 +# @Desc : +""" + +import sys +import os +import re +import numpy as np +import pickle +import json +import logger + +logger = logger.Logger() + + +def get_data_res(label_map, data, topk): + """get_data_res""" + sum_vid = len(data) + video_result = [] + for i in range(sum_vid): + vid_name = data[i][0][0] + # true_label predict_start predict_end predict_score predict_len gt_iou gt_start gt_ioa + feature_start_id = float(data[i][0][1]['start']) + feature_end_id = float(data[i][0][1]['end']) + feature_stage1_score = data[i][0][1]['score'] + predict_res = [] + for k in range(topk): + score_top = data[i][1][k] + labelid_top = data[i][2][k] + label_iou = data[i][3] + labelname_top = label_map[str(labelid_top)] + video_result.append([feature_start_id, feature_end_id, labelid_top, labelname_top, score_top, label_iou]) + return video_result + + +def base_nms(bboxes, thresh, delta=0, nms_id=2): + """ + One-dimensional non-maximal suppression + :param bboxes: [[vid, label, st, ed, score, ...], ...] + :param thresh: + :return: + """ + """ + t1 = bboxes[:, 0] + t2 = bboxes[:, 1] + scores = bboxes[:, nms_id] + """ + + t1 = np.array([max(0, x[0] - delta) for x in bboxes]) + t2 = np.array([x[1] + delta for x in bboxes]) + scores = np.array([x[nms_id] for x in bboxes]) + + durations = t2 - t1 + order = scores.argsort()[::-1] + + keep = [] + while order.size > 0: + i = order[0] + keep.append(i) + tt1 = np.maximum(t1[i], t1[order[1:]]) + tt2 = np.minimum(t2[i], t2[order[1:]]) + intersection = tt2 - tt1 + IoU = intersection / (durations[i] + durations[order[1:]] - intersection).astype(float) + + inds = np.where(IoU <= thresh)[0] + order = order[inds + 1] + return [bboxes[i] for i in keep] + + +def process_proposal(source_prop_box, min_frame_thread=5, nms_thresh=0.7, score_thresh=0.01): + """process_video_prop""" + prop_box = [] + for items in source_prop_box: + start_frame = float(items[0]) + end_frame = float(items[1]) + score = float(items[2]) + if end_frame - start_frame < min_frame_thread or score < score_thresh: + continue + prop_box.append([start_frame, end_frame, score]) + + prop_box_keep = base_nms(prop_box, nms_thresh) + + prop_res = [] + for res in prop_box_keep: + prop_res.append({'start': res[0], 'end': res[1], 'score': res[2]}) + + return prop_res + + +def process_video_classify(video_prop, fps, score_thread, iou_thread, \ + nms_id=5, nms_thread=0.01, nms_delta=10, backgroundid=0): + """process_video_classify""" + prop_filter = [] + for item in video_prop: + if item[2] == backgroundid: + continue + prop_filter.append(item) + + # prop_filter = sorted(prop_filter, key=lambda x: x[nms_id], reverse=True) + prop_filter = base_nms(prop_filter, nms_thread, nms_delta, nms_id) + prop_filter = sorted(prop_filter, key=lambda x: x[0]) + + video_results = [] + for item in prop_filter: + start_sec = item[0] / fps + end_sec = item[1] / fps + + start_id_frame = item[0] + end_id_frame = item[1] + # start_time = "%02d:%02d:%02d" % ((start_id_frame / fps) / 3600, \ + # ((start_id_frame / fps) % 3600) / 60, (start_id_frame / fps) % 60) + # end_time = "%02d:%02d:%02d" % ((end_id_frame / fps) / 3600, \ + # ((end_id_frame / fps) % 3600) / 60, (end_id_frame / fps) % 60) + start_time = int(start_id_frame / fps) + end_time = int(end_id_frame / fps) + + label_id = item[2] + label_name = item[3] + label_classify_score = item[4] + label_iou_score = item[5] + if label_classify_score > score_thread and label_iou_score > iou_thread: + video_results.append({"start_time": start_time, + "end_time": end_time, + "label_id": label_id, + "label_name": label_name, + "classify_score": label_classify_score, + "iou_score": label_iou_score}) + + return video_results + + +def get_action_result(result_info, label_map_file, fps, score_thread=0, \ + iou_thread=0, nms_id=5, nms_thread=0.01, frame_offset=10, topk=1): + """get_action_result""" + + label_map = json.load(open(label_map_file, 'r', encoding='utf-8')) + + org_result = get_data_res(label_map, result_info, topk) + nms_result = process_video_classify(org_result, fps, score_thread, iou_thread, nms_id, nms_thread, frame_offset) + + return nms_result diff --git a/applications/FootballAction/predict/configs/configs.yaml b/applications/FootballAction/predict/configs/configs.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0b69c3a4eedfc72f554e15d612a8a3e75c1d9ae4 --- /dev/null +++ b/applications/FootballAction/predict/configs/configs.yaml @@ -0,0 +1,62 @@ +COMMON: + fps: 5 + use_gpu: True + label_dic: '/workspace/PaddleVideo/applications/FootballAction/predict/configs/index_label_football_8.json' + # debug + PCM_ONLY: False + DEBUG: False + BMN_ONLY: False + LSTM_ONLY: False + +PPTSM: + name: "PPTSM" + model_file: "/workspace/PaddleVideo/applications/FootballAction/checkpoints/ppTSM/ppTSM.pdmodel" + params_file: "/workspace/PaddleVideo/applications/FootballAction/checkpoints/ppTSM/ppTSM.pdiparams" + gpu_mem: 8000 + device_id: 0 + seg_num: 8 + seglen: 1 + short_size: 256 + target_size: 224 + batch_size: 32 + image_mean: [0.485, 0.456, 0.406] + image_std: [0.229, 0.224, 0.225] + reader_threads: 12 + buf_size: 1024 + +AUDIO: + name: "AUDIO" + model_file: "/workspace/PaddleVideo/applications/FootballAction/checkpoints/AUDIO/__model__" + params_file: "/workspace/PaddleVideo/applications/FootballAction/checkpoints/AUDIO/__param__" + gpu_mem: 8000 + device_id: 0 + sample_rate: 16000 + batch_size: 32 + +BMN: + name: "BMN" + model_file: "/workspace/PaddleVideo/applications/FootballAction/checkpoints/BMN/__model__" + params_file: "/workspace/PaddleVideo/applications/FootballAction/checkpoints/BMN/__param__" + gpu_mem: 8000 + device_id: 0 + window_step: 200 # 200 + tscale: 200 + dscale: 200 + batch_size: 8 # 8 + nms_thread: 0.7 + score_thread: 0.03 # 0.05 + +ACTION: + name: "ACTION" + model_file: "/workspace/PaddleVideo/applications/FootballAction/checkpoints/LSTM/__model__" + params_file: "/workspace/PaddleVideo/applications/FootballAction/checkpoints/LSTM/__param__" + gpu_mem: 8000 + device_id: 0 + batch_size: 32 + topk: 1 + nms_thread: 0.01 + nms_offset: 10 + + classify_score_thread: 0.05 # 0.15 + iou_score_thread: 0.1 # 0.4 + diff --git a/applications/FootballAction/predict/configs/index_label_football_8.json b/applications/FootballAction/predict/configs/index_label_football_8.json new file mode 100644 index 0000000000000000000000000000000000000000..a6eed11c216b762de2735acafd3cad87cdd340ab --- /dev/null +++ b/applications/FootballAction/predict/configs/index_label_football_8.json @@ -0,0 +1,10 @@ +{ + "0": "背景", + "1": "进球", + "2": "角球", + "3": "任意球", + "4": "黄牌", + "5": "红牌", + "6": "换人", + "7": "界外球" +} diff --git a/applications/FootballAction/predict/eval.py b/applications/FootballAction/predict/eval.py new file mode 100644 index 0000000000000000000000000000000000000000..2f663248619e00f6c4d3166bc40704f7c7de018d --- /dev/null +++ b/applications/FootballAction/predict/eval.py @@ -0,0 +1,239 @@ +""" +get instance for lstm +根据gts计算每个proposal_bmn的iou、ioa、label等信息 +""" +import os +import sys +import json +import random +import pickle +import numpy as np + +import io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding = 'utf-8') + +dataset = "/home/work/datasets" + +label_index_file = './configs/index_label_football_8.json' +eval_datasets = ['EuroCup2016'] +label_files = {'train': 'label_cls8_train.json', + 'validation': 'label_cls8_val.json'} + +global fps, mode +label_index = json.load(open(label_index_file, 'rb')) + +def load_gts(): + global fps + gts_data = {'fps': 0, 'gts': {}} + for eval_data in eval_datasets: + for item, value in label_files.items(): + label_file = '{}/{}/{}'.format(dataset, eval_data, value) + gts = json.load(open(label_file, 'rb')) + gts_data['fps'] = gts['fps'] + fps = gts['fps'] + for gt in gts['gts']: + gt['mode'] = item + basename = '{}/{}/mp4/{}'.format(dataset, eval_data, os.path.basename(gt['url'])) + gts_data['gts'][basename] = gt + return gts_data['gts'] + + +def computeIoU(e1, e2): + """ + clc iou and ioa + """ + if not (e1['label'] == e2['label'] and e1['basename'] == e2['basename']): + return 0. + area1 = e1["end"] - e1["start"] + area2 = e2["end"] - e2["start"] + x1 = np.maximum(e1["start"], e2["start"]) + x2 = np.minimum(e1["end"], e2["end"]) + inter = np.maximum(0.0, x2 - x1) + iou = 0.0 if (area1 + area2 - inter) == 0 else inter * 1.0 / (area1 + area2 - inter) + if not mode == 'proposal': + iou = 0.0 if area2 == 0 else inter * 1.0 / area2 + return iou + + +def convert_proposal(boxes, basename, score_threshold=0.01): + boxes = sorted(boxes, key=lambda x:float(x['score']), reverse=True) + res = [] + for box in boxes: + if not float(box['score']) >= score_threshold: + continue + res.append({'basename': basename, + 'start': int(float(box['start']) / fps), + 'end': int(float(box['end']) / fps), + 'label': 0}) + return res + +def convert_classify(boxes, basename, iou_threshold, score_threshold): + boxes = sorted(boxes, key=lambda x:(float(x['classify_score']), float(x['iou_score'])), reverse=True) + def convert_time_to_frame(time_type): + return int(time_type) + h, m, s = time_type.split(':') + return int(h) * 3600 + int(m) * 60 + int(s) + res = [] + for box in boxes: + if not (box['iou_score'] >= iou_threshold and + box['classify_score'] >= score_threshold): + continue + res.append({'basename': basename, + 'start': convert_time_to_frame(box['start_time']), + 'end': convert_time_to_frame(box['end_time']), + 'label': box['label_id']}) + return res + +def convert_groundtruth(boxes, basename, phase=None): + res = [] + for box in boxes: + for item in box['label_ids']: + label = 0 if phase == 'proposal' else item + res.append({'basename': basename, + 'start': box['start_id'], + 'end': box['end_id'], + 'label': label}) + return res +def print_head(iou): + print("\nioa = {:.1f}".format(iou)) + res_str = '' + for item in ['label_name']: + res_str += '{:<12s}'.format(item) + for item in ['label_id', 'precision', 'recall', 'hit_prop', 'num_prop', 'hit_gts', 'num_gts']: + res_str += '{:<10s}'.format(item) + print(res_str) + +def print_result(res_dict, label='avg'): + if label == 'avg': + res_str = '{:<22s}'.format(str(label)) + else: + res_str = '{0:{2}<6s}{1:<10s}'.format(label_index[str(label)], str(label), chr(12288)) + + for item in ['prec', 'recall']: + res_str += '{:<10.4f}'.format(res_dict[item]) + for item in ['hit_prop', 'num_prop', 'hit_gts', 'num_gts']: + res_str += '{:<10d}'.format(res_dict[item]) + print(res_str) + +def evaluation(res_boxes, gts_boxes, label_range, iou_range, show_sub = False): + iou_map = [computeIoU(resId, gtsId) for resId in res_boxes \ + for gtsId in gts_boxes] + iou_map = np.array(iou_map).reshape((len(res_boxes), len(gts_boxes))) + hit_map_prop_total = np.max(iou_map, axis=1) + hit_map_index_total = np.argmax(iou_map, axis=1) + + res_dict = ['hit_prop', 'num_prop', 'hit_gts', 'num_gts'] + + for iou_threshold in iou_range: + if show_sub: + print_head(iou_threshold) + + iou_prop = np.array([k >= iou_threshold for k in hit_map_prop_total]) + average_results = {} + for label_id in label_range: + sub_results = {} + label_prop = np.array([k['label'] == label_id for k in res_boxes]) + label_gts = np.array([k['label'] == label_id for k in gts_boxes]) + sub_results['num_prop'] = sum(label_prop) + sub_results['num_gts'] = sum(label_gts) + if sub_results['num_prop'] == 0: + hit_prop_index = [] + else: + hit_prop_index = label_prop & iou_prop + sub_results['hit_prop'] = sum(hit_prop_index) + sub_results['hit_gts'] = len(set(hit_map_index_total[hit_prop_index])) + + sub_results['prec'] = 0.0 if sub_results['num_prop'] == 0 \ + else sub_results['hit_prop'] * 1.0 / sub_results['num_prop'] + sub_results['recall'] = 0.0 if sub_results['num_gts'] == 0 \ + else sub_results['hit_gts'] * 1.0 / sub_results['num_gts'] + if show_sub: + print_result(sub_results, label=label_id) + for item in res_dict: + if not item in average_results: + average_results[item] = 0 + average_results[item] += sub_results[item] + if len(label_range) == 1: # proposal 不需要输出average值 + continue + average_results['prec'] = 0.0 if average_results['num_prop'] == 0 \ + else average_results['hit_prop'] * 1.0 / average_results['num_prop'] + average_results['recall'] = 0.0 if average_results['num_gts'] == 0 \ + else average_results['hit_gts'] * 1.0 / average_results['num_gts'] + if show_sub: + print_result(average_results) + + average_results['F1'] = 0.0 if (average_results['prec'] + average_results['recall'] == 0) \ + else 2 * average_results['prec'] * average_results['recall'] / \ + (average_results['prec'] + average_results['recall']) + return average_results + +def get_eval_results(predicts, gts_data, phase, iou_threshold = 0.3, score_threshold = 0.3, show_sub = False): + global mode + mode = phase + res_boxes = [] + gts_boxes = [] + for ped_data in predicts: + basename = ped_data['video_name'] + + # eval sub data + such_eval = False + for eval_name in eval_datasets: + if eval_name in basename: + such_eval = True + break + if not such_eval: + continue + + gts = gts_data[basename]['actions'] + if phase == 'proposal': + res_boxes.extend(convert_proposal(ped_data['bmn_results'], basename, score_threshold)) + gts_boxes.extend(convert_groundtruth(gts, basename, phase='proposal')) + label_range = [0] + iou_range = np.arange(0.1, 1, 0.1) + else: + res_boxes.extend(convert_classify(ped_data['action_results'], basename, iou_threshold, score_threshold)) + gts_boxes.extend(convert_groundtruth(gts, basename)) + label_range = range(1, len(label_index)) + iou_range = np.arange(0.5, 0.6, 0.1) + + eval_results = evaluation(res_boxes, gts_boxes, label_range, iou_range, show_sub = show_sub) + + return eval_results + + +if __name__ == "__main__": + result_file = sys.argv[1] + predicts = json.load(open(result_file, 'r', encoding='utf-8')) + gts_data = load_gts() + + get_eval_results(predicts, gts_data, 'proposal', + score_threshold = 0.03, + show_sub = True) + #get_eval_results(predicts, gts_data, 'actions') + + best_F1 = -0.1 + best_res = {} + best_iou_threshold = 0. + best_score_threshold = 0. + for iou_threshold in np.arange(0.1, 0.9, 0.1): + for score_threshold in np.arange(0.1, 1, 0.1): + avg_res = get_eval_results(predicts, gts_data, 'actions', + iou_threshold = iou_threshold, + score_threshold = score_threshold, + show_sub = False) + if best_F1 < avg_res['F1']: + best_F1 = avg_res['F1'] + best_res = avg_res + best_iou_threshold = iou_threshold + best_score_threshold = score_threshold + print("best iou threshold = {:.1f}".format(best_iou_threshold)) + print("best score threshold = {:.1f}".format(best_score_threshold)) + print('best F1 score = {:.4f}'.format(best_F1)) + print_head(0.5) + print_result(best_res) + + get_eval_results(predicts, gts_data, 'actions', iou_threshold = best_iou_threshold, + score_threshold = best_score_threshold, + show_sub = True) + + diff --git a/applications/FootballAction/predict/predict.py b/applications/FootballAction/predict/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..d1736b4c8df7f6d6ba1a518f837dc43f663ab5ac --- /dev/null +++ b/applications/FootballAction/predict/predict.py @@ -0,0 +1,35 @@ + +import os +import sys +import json + +sys.path.append('action_detect') +from action import ActionDetection + +if __name__ == '__main__': + dataset_dir = "/workspace/PaddleVideo/applications/FootballAction/datasets/EuroCup2016" + + model_predict = ActionDetection(cfg_file="./configs/configs.yaml") + model_predict.load_model() + + video_url = os.path.join(dataset_dir, 'url_val.list') + with open(video_url, 'r') as f: + lines = f.readlines() + lines = [os.path.join(dataset_dir, k.strip()) for k in lines] + + results = [] + for line in lines: + video_name = line + print(video_name) + + imgs_path = video_name.replace(".mp4", "").replace("mp4", "frames") + pcm_path = video_name.replace(".mp4", ".pcm").replace("mp4", "pcm") + + bmn_results, action_results = model_predict.infer(imgs_path, pcm_path) + results.append({'video_name': line, + 'bmn_results': bmn_results, + 'action_results': action_results}) + + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) diff --git a/applications/FootballAction/train_lstm/conf/conf.txt b/applications/FootballAction/train_lstm/conf/conf.txt new file mode 100644 index 0000000000000000000000000000000000000000..2800aeb53fb549910d1303f7f34aa92d1d1a0b40 --- /dev/null +++ b/applications/FootballAction/train_lstm/conf/conf.txt @@ -0,0 +1,37 @@ +[MODEL] +name = "ActionNet" +dataset = "Actiondata" +bone_nework = None +modelbn_min_everygpu_bs = 10 +drop_rate = 0.5 +feature_num = 1 +feature_names = ['rgb', 'audio'] +feature_dims = [2048, 1024] +embedding_size = 512 +lstm_size_img = 2048 +lstm_size_audio = 1024 +num_classes = 8 +save_dir = "." + +[TRAIN] +epoch = 20 +learning_rate = 0.0007 +decay_gamma = 0.2 +l2_weight_decay = 8e-4 +decay_epochs = [5,10,15,20] +num_samples = 268592 +#pretrain = checkpoint_pretain +batch_size = 600 +droplast = False +use_gpu = True +num_gpus = 2 +filelist = "/home/work/datasets/EuroCup2016/input_for_lstm/train.txt" +[VALID] +batch_size = 16 +num_samples = 34487 +droplast = True +filelist = "/home/work/datasets/EuroCup2016/input_for_lstm/val.txt" +[INFER] +batch_size = 1 +droplast = True +filelist = "data_demo/batch_val/val.list" diff --git a/applications/FootballAction/train_lstm/conf/conf.yaml b/applications/FootballAction/train_lstm/conf/conf.yaml new file mode 100644 index 0000000000000000000000000000000000000000..57ac9128259aae57cb754504178708ef13da7230 --- /dev/null +++ b/applications/FootballAction/train_lstm/conf/conf.yaml @@ -0,0 +1,39 @@ +MODEL: + name: "ActionNet" + dataset: "Actiondata" + bone_nework: None + modelbn_min_everygpu_bs: 10 + drop_rate: 0.5 + feature_num: 1 + feature_names: ['rgb', 'audio'] + feature_dims: [2048, 1024] + embedding_size: 512 + lstm_size_img: 2048 + lstm_size_audio: 1024 + num_classes: 8 + save_dir: "." + with_bn: True + +TRAIN: + epoch: 20 + learning_rate: 0.0007 + decay_gamma: 0.2 + l2_weight_decay: 8e-4 + decay_epochs: [5,10,15,20] + num_samples: 268592 + batch_size: 600 + droplast: False + use_gpu: True + num_gpus: 2 + filelist: "/workspace/PaddleVideo/applications/FootballAction/datasets/EuroCup2016/input_for_lstm/train.txt" + +VALID: + batch_size: 16 + num_samples: 34487 + droplast: True + filelist: "/workspace/PaddleVideo/applications/FootballAction/datasets/EuroCup2016/input_for_lstm/val.txt" + +INFER: + batch_size: 1 + droplast: True + filelist: "/workspace/PaddleVideo/applications/FootballAction/datasets/EuroCup2016/input_for_lstm/val.txt" \ No newline at end of file diff --git a/applications/FootballAction/train_lstm/inference_model.py b/applications/FootballAction/train_lstm/inference_model.py new file mode 100644 index 0000000000000000000000000000000000000000..ae734d182a04a98ff32a13d11e26bed4fa987d0a --- /dev/null +++ b/applications/FootballAction/train_lstm/inference_model.py @@ -0,0 +1,126 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import time +import logging +import argparse +import ast +import numpy as np +try: + import cPickle as pickle +except: + import pickle +import paddle +import paddle.fluid as fluid + +from utils.config_utils import * +import scenario_lib.action_net as action_net + +paddle.enable_static() +logging.root.handlers = [] +FORMAT = '[%(levelname)s: %(filename)s: %(lineno)4d]: %(message)s' +logging.basicConfig(level=logging.DEBUG, format=FORMAT, stream=sys.stdout) +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--model_name', + type=str, + default='ActionNet', + help='name of model to train.') + parser.add_argument( + '--config', + type=str, + default='conf/con.txt', + help='path to config file of model') + parser.add_argument( + '--use_gpu', + type=ast.literal_eval, + default=True, + help='default use gpu.') + parser.add_argument( + '--weights', + type=str, + default=None, + help='weight path, None to automatically download weights provided by Paddle.' + ) + parser.add_argument( + '--batch_size', + type=int, + default=1, + help='sample number in a batch for inference.') + parser.add_argument( + '--save_dir', + type=str, + default='./', + help='directory to store model and params file') + args = parser.parse_args() + return args + + +def save_inference_model(args): + # parse config + config = parse_config(args.config) + infer_config = merge_configs(config, 'infer', vars(args)) + print_configs(infer_config, "Infer") + #infer_model = models.get_model(args.model_name, infer_config, mode='infer') + infer_model = action_net.ActionNet(args.model_name, + infer_config, + mode='infer') + infer_model.build_input(use_pyreader=False) + infer_model.build_model() + infer_feeds = infer_model.feeds() + infer_outputs = infer_model.outputs() + + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + exe.run(fluid.default_startup_program()) + + if args.weights: + assert os.path.exists( + args.weights), "Given weight dir {} not exist.".format(args.weights) + # if no weight files specified, download weights from paddle + weights = args.weights or infer_model.get_weights() + + infer_model.load_test_weights_file(exe, weights, + fluid.default_main_program(), place) + + if not os.path.isdir(args.save_dir): + os.makedirs(args.save_dir) + + # saving inference model + fluid.io.save_inference_model( + args.save_dir, + feeded_var_names=[item.name for item in infer_feeds], + target_vars=infer_outputs, + executor=exe, + main_program=fluid.default_main_program(), + model_filename=args.model_name + "_model.pdmodel", + params_filename=args.model_name + "_params.pdparams") + + print("save inference model at %s" % (args.save_dir)) + + +if __name__ == "__main__": + import paddle + paddle.enable_static() + args = parse_args() + + logger.info(args) + + save_inference_model(args) \ No newline at end of file diff --git a/applications/FootballAction/train_lstm/scenario_lib/accuracy_metrics.py b/applications/FootballAction/train_lstm/scenario_lib/accuracy_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..43298be16ef35b4e014601abef19d873c9b7c90c --- /dev/null +++ b/applications/FootballAction/train_lstm/scenario_lib/accuracy_metrics.py @@ -0,0 +1,150 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division + +import numpy as np +import datetime +import logging + +logger = logging.getLogger('LSTM') + + +class MetricsCalculator(): + """MetricsCalculator""" + def __init__(self, name, mode, metrics_args): + """init""" + self.name = name + self.mode = mode # 'train', 'val', 'test' + self.num_classes = metrics_args['MODEL']['num_classes'] + self.reset() + + def reset(self): + """reset""" + logger.info('Resetting {} metrics...'.format(self.mode)) + self.aggr_iou = 0.0 + self.aggr_acc1 = 0.0 + self.aggr_acc5 = 0.0 + self.aggr_loss = 0.0 + self.aggr_batch_size = 0 + + def finalize_metrics(self): + """finalize_metrics""" + self.avg_iou = self.aggr_iou / self.aggr_batch_size + self.avg_acc1 = self.aggr_acc1 / self.aggr_batch_size + self.avg_acc5 = self.aggr_acc5 / self.aggr_batch_size + self.avg_loss = self.aggr_loss / self.aggr_batch_size + + def get_computed_metrics(self): + """get_computed_metrics""" + json_stats = {} + json_stats['avg_iou'] = self.avg_iou + json_stats['avg_loss'] = self.avg_loss + json_stats['avg_acc1'] = self.avg_acc1 + json_stats['avg_acc5'] = self.avg_acc5 + return json_stats + + def calculate_metrics(self, loss, pred, label, pred_info=''): + """calculate_metrics""" + if loss is not None: + loss = np.mean(np.array(loss)) + else: + loss = 0. + + #acc1, acc5 = self.calculate_metrics(loss, pred, label, self.num_classes) + accuracy1 = compute_topk_accuracy(pred, label, top_k=1) * 100. + accuracy5 = compute_topk_accuracy( + pred, label, top_k=min(5, self.num_classes)) * 100. + + def accumulate(self, loss, softmax, labels, regiou, iou): + """accumulate""" + cur_batch_size = softmax.shape[0] + # if returned loss is None for e.g. test, just set loss to be 0. + if loss is None: + cur_loss = 0. + else: + cur_loss = np.mean(np.array(loss)) # + self.aggr_batch_size += cur_batch_size + self.aggr_loss += cur_loss * cur_batch_size + + aggr_iou = compute_iou_sub(regiou, iou) + self.aggr_iou += aggr_iou * cur_batch_size + accuracy1 = compute_topk_accuracy(softmax, labels, top_k=1) * 100. + accuracy5 = compute_topk_accuracy( + softmax, labels, top_k=min(5, self.num_classes)) * 100. + self.aggr_acc1 += accuracy1 * cur_batch_size + self.aggr_acc5 += accuracy5 * cur_batch_size + return + + def finalize_and_log_out(self, info=''): + """finalize_and_log_out""" + self.finalize_metrics() + metrics_dict = self.get_computed_metrics() + loss = metrics_dict['avg_loss'] + iou = metrics_dict['avg_iou'] + acc1 = metrics_dict['avg_acc1'] + acc5 = metrics_dict['avg_acc5'] + logger.info(info + '\tLoss: {},\ttop1_acc: {}, \ttop5_acc: {}, \tiou: {}'.format('%.6f' % loss, \ + '%.2f' % acc1, '%.2f' % acc5, '%.6f' % iou)) + return loss, acc1, iou + + +# ---------------------------------------------- +# other utils +# ---------------------------------------------- +def compute_topk_correct_hits(top_k, preds, labels): + """Compute the number of corret hits""" + batch_size = preds.shape[0] + + top_k_preds = np.zeros((batch_size, top_k), dtype=np.float32) + for i in range(batch_size): + top_k_preds[i, :] = np.argsort(-preds[i, :])[:top_k] + + correctness = np.zeros(batch_size, dtype=np.int32) + for i in range(batch_size): + if labels[i] in top_k_preds[i, :].astype(np.int32).tolist(): + correctness[i] = 1 + correct_hits = sum(correctness) + + return correct_hits + + +def compute_topk_accuracy(softmax, labels, top_k): + """compute_topk_accuracy""" + + computed_metrics = {} + + assert labels.shape[0] == softmax.shape[0], "Batch size mismatch." + aggr_batch_size = labels.shape[0] + aggr_top_k_correct_hits = compute_topk_correct_hits(top_k, softmax, labels) + + # normalize results + computed_metrics = \ + float(aggr_top_k_correct_hits) / aggr_batch_size + + return computed_metrics + + +def compute_iou_sub(regiou, iou): + batch_size = regiou.shape[0] + iou_sub = 0 + for i in range(batch_size): + cur_diff = abs(regiou[i] - iou[i]) + iou_sub += cur_diff + + iou_sub = float(iou_sub) / float(batch_size) + return iou_sub diff --git a/applications/FootballAction/train_lstm/scenario_lib/action_net.py b/applications/FootballAction/train_lstm/scenario_lib/action_net.py new file mode 100644 index 0000000000000000000000000000000000000000..c43acbe72dcb93f3b4a752423a967b7cf71fd043 --- /dev/null +++ b/applications/FootballAction/train_lstm/scenario_lib/action_net.py @@ -0,0 +1,323 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math +import numpy as np +import paddle.fluid as fluid +from paddle.fluid import ParamAttr + +import logging + +logger = logging.getLogger('LSTM') + +def is_parameter(var): + """is_parameter""" + return isinstance(var, fluid.framework.Parameter) + + +class ActionNet: + """ActionNet""" + + def __init__(self, name, cfg, mode='train'): + self.cfg = cfg + self.name = name + self.mode = mode + self.py_reader = None + self.get_config() + + def get_config(self): + """get_config""" + # get model configs + self.feature_num = self.cfg.MODEL.feature_num + self.feature_names = self.cfg.MODEL.feature_names + self.with_bn = self.cfg.MODEL.with_bn + self.feature_dims = self.cfg.MODEL.feature_dims + self.num_classes = self.cfg.MODEL.num_classes + self.embedding_size = self.cfg.MODEL.embedding_size + #self.lstm_size = self.cfg.MODEL.lstm_size + self.lstm_size_img = self.cfg.MODEL.lstm_size_img + self.lstm_size_audio = self.cfg.MODEL.lstm_size_audio + self.drop_rate = self.cfg.MODEL.drop_rate + self.save_dir = self.cfg.MODEL.save_dir + + # get mode configs + self.batch_size = self.get_config_from_sec(self.mode, 'batch_size', 1) + self.num_gpus = self.get_config_from_sec(self.mode, 'num_gpus', 1) + + if self.mode == 'train': + self.learning_rate = self.get_config_from_sec('train', + 'learning_rate', 1e-3) + self.weight_decay = self.get_config_from_sec('train', + 'weight_decay', 8e-4) + self.num_samples = self.get_config_from_sec('train', 'num_samples', + 5000000) + self.decay_epochs = self.get_config_from_sec('train', + 'decay_epochs', [5]) + self.decay_gamma = self.get_config_from_sec('train', 'decay_gamma', + 0.1) + self.droplast = self.get_config_from_sec('train', 'droplast', False) + + def get_config_from_sec(self, sec, item, default=None): + """get_config_from_sec""" + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + def pyreader(self): + """pyreader""" + return self.py_reader + + def load_pretrain_params_file(self, exe, pretrain, prog, place): + logger.info("Load pretrain weights from {}, param".format(pretrain)) + + load_vars = [x for x in prog.list_vars() \ + if isinstance(x, fluid.framework.Parameter) and x.name.find('fc_8') == -1] + fluid.io.load_vars(exe, dirname=pretrain, vars=load_vars, filename="param") + + def load_test_weights_file(self, exe, weights, prog, place): + params_list = list(filter(is_parameter, prog.list_vars())) + fluid.load(prog, weights, executor=exe, var_list=params_list) + #def load_test_weights_file(self, exe, weights, prog, place): + # """load_test_weights_file""" + # load_vars = [x for x in prog.list_vars() \ + # if isinstance(x, fluid.framework.Parameter)] + # fluid.io.load_vars(exe, dirname=weights, vars=load_vars, filename="param") + + def epoch_num(self): + """get train epoch num""" + return self.cfg.TRAIN.epoch + + + def build_input(self, use_pyreader): + """build_input""" + self.feature_input = [] + for name, dim in zip(self.feature_names, self.feature_dims): + self.feature_input.append( + fluid.layers.data( + shape=[dim], lod_level=1, dtype='float32', name=name)) + if self.mode != 'infer': + self.label_id_input = fluid.layers.data( + shape=[1], dtype='int64', name='label_cls') + self.label_iou_input = fluid.layers.data( + shape=[1], dtype='float32', name='label_iou') + else: + self.label_id_input = None + self.label_iou_input = None + if use_pyreader: + assert self.mode != 'infer', \ + 'pyreader is not recommendated when infer, please set use_pyreader to be false.' + self.py_reader = fluid.io.PyReader( + feed_list=self.feature_input + [self.label_id_input] + [self.label_iou_input], + capacity=1024, + iterable=True) + + + def build_model(self): + """build_model""" + # ---------------- transfer from old paddle --------------- + # ------image------ + + lstm_forward_fc = fluid.layers.fc( + input=self.feature_input[0], + size=self.lstm_size_img * 4, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + lstm_forward, _ = fluid.layers.dynamic_lstm(input=lstm_forward_fc, size=self.lstm_size_img * 4, is_reverse=False, + use_peepholes=True) + #lstm_forward_add = fluid.layers.elementwise_add(self.feature_input[0], lstm_forward, act='relu') + #print("lstm_backward_add.shape", lstm_forward_add.shape) + + lstm_backward_fc = fluid.layers.fc( + input=self.feature_input[0], + size=self.lstm_size_img * 4, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + lstm_backward, _ = fluid.layers.dynamic_lstm(input=lstm_backward_fc, size=self.lstm_size_img * 4, is_reverse=True, + use_peepholes=True) + #lstm_backward_add = fluid.layers.elementwise_add(self.feature_input[0], lstm_backward, act='relu') + #print("lstm_backward_add.shape", lstm_backward_add.shape) + + #lstm_img = fluid.layers.concat(input=[lstm_forward_add, lstm_backward_add], axis=1) + lstm_img = fluid.layers.concat(input=[lstm_forward, lstm_backward], axis=1) + print("lstm_img.shape", lstm_img.shape) + + lstm_dropout = fluid.layers.dropout(x=lstm_img, dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + lstm_weight = fluid.layers.fc( + input=lstm_dropout, + size=1, + act='sequence_softmax', + bias_attr=None) + + scaled = fluid.layers.elementwise_mul(x=lstm_dropout, y=lstm_weight, axis=0) + lstm_pool = fluid.layers.sequence_pool(input=scaled, pool_type='sum') + # ------audio------ + lstm_forward_fc_audio = fluid.layers.fc( + input=self.feature_input[1], + size=self.lstm_size_audio * 4, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + lstm_forward_audio, _ = fluid.layers.dynamic_lstm( + input=lstm_forward_fc_audio, size=self.lstm_size_audio * 4, is_reverse=False, use_peepholes=True) + + lsmt_backward_fc_audio = fluid.layers.fc( + input=self.feature_input[1], + size=self.lstm_size_audio * 4, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + lstm_backward_audio, _ = fluid.layers.dynamic_lstm(input=lsmt_backward_fc_audio, size=self.lstm_size_audio * 4, + is_reverse=True, use_peepholes=True) + + lstm_forward_audio = fluid.layers.concat(input=[lstm_forward_audio, lstm_backward_audio], axis=1) + + lstm_dropout_audio = fluid.layers.dropout(x=lstm_forward_audio, dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + lstm_weight_audio = fluid.layers.fc( + input=lstm_dropout_audio, + size=1, + act='sequence_softmax', + bias_attr=None) + + scaled_audio = fluid.layers.elementwise_mul(x=lstm_dropout_audio, y=lstm_weight_audio, axis=0) + lstm_pool_audio = fluid.layers.sequence_pool(input=scaled_audio, pool_type='sum') + # ------ concat ------- + lstm_concat = fluid.layers.concat(input=[lstm_pool, lstm_pool_audio], axis=1) + #print("lstm_concat.shape", lstm_concat.shape) + + input_fc_proj = fluid.layers.fc( + input=lstm_concat, + # input=lstm_pool, # 只用image feature训练 + size=8192, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + input_fc_proj_bn = fluid.layers.batch_norm(input=input_fc_proj, act="relu", + is_test=(not self.mode == 'train')) + # model remove bn when batch_size is small + if not self.with_bn: + input_fc_proj_bn = 0 * input_fc_proj_bn + input_fc_proj + input_fc_proj_dropout = fluid.layers.dropout(x=input_fc_proj_bn, dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + + input_fc_hidden = fluid.layers.fc( + input=input_fc_proj_dropout, + size=4096, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + input_fc_hidden_bn = fluid.layers.batch_norm(input=input_fc_hidden, act="relu", + is_test=(not self.mode == 'train')) + # model remove bn when batch_size is small + if not self.with_bn: + input_fc_hidden_bn = 0 * input_fc_hidden_bn + input_fc_hidden + input_fc_hidden_dropout = fluid.layers.dropout(x=input_fc_hidden_bn, dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + self.fc = fluid.layers.fc( + input=input_fc_hidden_dropout, + size=self.num_classes, + act='softmax') + self.fc_iou = fluid.layers.fc( + input=input_fc_hidden_dropout, + size=1, + act="sigmoid") + self.network_outputs = [self.fc, self.fc_iou] + + def optimizer(self): + """optimizer""" + assert self.mode == 'train', "optimizer only can be get in train mode" + values = [ + self.learning_rate * (self.decay_gamma**i) + for i in range(len(self.decay_epochs) + 1) + ] + if self.droplast: + self.num_samples = math.floor(float(self.num_samples) / float(self.batch_size)) * self.batch_size + iter_per_epoch = math.floor(float(self.num_samples) / self.batch_size) + else: + self.num_samples = math.ceil(float(self.num_samples) / float(self.batch_size)) * self.batch_size + iter_per_epoch = math.ceil(float(self.num_samples) / self.batch_size) + + boundaries = [e * iter_per_epoch for e in self.decay_epochs] + logger.info("num_sample = {}, batchsize = {}, iter_per_epoch = {}, lr_int = {}, boundaries = {} " + .format(self.num_samples, self.batch_size, \ + iter_per_epoch, self.learning_rate, np.array(boundaries))) + + return fluid.optimizer.RMSProp( + learning_rate=fluid.layers.piecewise_decay( + values=values, boundaries=boundaries), + centered=True, + regularization=fluid.regularizer.L2Decay(regularization_coeff=self.weight_decay)) + + def _calc_label_smoothing_loss(self, softmax_out, label, class_dim, epsilon=0.1): + """Calculate label smoothing loss + Returns: + label smoothing loss + """ + label_one_hot = fluid.layers.one_hot(input=label, depth=class_dim) + smooth_label = fluid.layers.label_smooth( + label=label_one_hot, epsilon=epsilon, dtype="float32") + loss = fluid.layers.cross_entropy( + input=softmax_out, label=smooth_label, soft_label=True) + return loss + + def loss(self): + """ + loss + """ + assert self.mode != 'infer', "invalid loss calculationg in infer mode" + cost_cls = fluid.layers.cross_entropy(input=self.network_outputs[0], label=self.label_id_input) + cost_cls = fluid.layers.reduce_sum(cost_cls, dim=-1) + sum_cost_cls = fluid.layers.reduce_sum(cost_cls) + self.loss_cls_ = fluid.layers.scale(sum_cost_cls, scale=self.num_gpus, bias_after_scale=False) + cost_iou = fluid.layers.square_error_cost(input=self.network_outputs[1], label=self.label_iou_input) + cost_iou = fluid.layers.reduce_sum(cost_iou, dim=-1) + sum_cost_iou = fluid.layers.reduce_sum(cost_iou) + self.loss_iou_ = fluid.layers.scale(sum_cost_iou, scale=self.num_gpus, bias_after_scale=False) + alpha = 10 + self.loss_ = self.loss_cls_ + alpha * self.loss_iou_ + #self.loss_ = self.loss_cls_ + return self.loss_ + + def outputs(self): + """outputs""" + return self.network_outputs + + def feeds(self): + """ + feeds + """ + return self.feature_input if self.mode == 'infer' else self.feature_input + [ + self.label_id_input, self.label_iou_input] + + def fetches(self): + """fetches""" + if self.mode == 'train' or self.mode == 'valid': + losses = self.loss() + fetch_list = [losses, self.network_outputs[0], self.network_outputs[1], \ + self.label_id_input, self.label_iou_input] + elif self.mode == 'infer': + fetch_list = [self.network_outputs[0], self.network_outputs[1]] + else: + raise NotImplementedError('mode {} not implemented'.format( + self.mode)) + + return fetch_list diff --git a/applications/FootballAction/train_lstm/scenario_lib/config.py b/applications/FootballAction/train_lstm/scenario_lib/config.py new file mode 100644 index 0000000000000000000000000000000000000000..e43158408eeddeef15eec0da14d8d806208e87f3 --- /dev/null +++ b/applications/FootballAction/train_lstm/scenario_lib/config.py @@ -0,0 +1,84 @@ +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + from configparser import ConfigParser +except BaseException: + from ConfigParser import ConfigParser + +import logging + +CONFIG_SECS = [ + 'train', + 'valid', + 'test', + 'infer', +] + + +def parse_config(cfg_file): + """parse_config""" + parser = ConfigParser() + cfg = AttrDict() + parser.read(cfg_file) + for sec in parser.sections(): + sec_dict = AttrDict() + for k, v in parser.items(sec): + try: + v = eval(v) + except BaseException: + pass + setattr(sec_dict, k, v) + setattr(cfg, sec.upper(), sec_dict) + + return cfg + + +def merge_configs(cfg, sec, args_dict): + """merge_configs""" + assert sec in CONFIG_SECS, "invalid config section {}".format(sec) + sec_dict = getattr(cfg, sec.upper()) + for k, v in args_dict.items(): + if v is None: + continue + try: + if hasattr(sec_dict, k): + setattr(sec_dict, k, v) + except BaseException: + pass + return cfg + + +def print_configs(cfg, mode): + """print_configs""" + logger = logging.getLogger('LSTM') + logger.info( + "---------------- {:>5} Arguments ----------------".format(mode)) + for sec, sec_items in cfg.items(): + logger.info("{}:".format(sec)) + for k, v in sec_items.items(): + logger.info(" {}:{}".format(k, v)) + logger.info("-------------------------------------------------") + + +class AttrDict(dict): + """AttrDict""" + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value diff --git a/applications/FootballAction/train_lstm/scenario_lib/feature_reader.py b/applications/FootballAction/train_lstm/scenario_lib/feature_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..24862fe2f3e82c66d64f409dbe1f63d44fe9b1a4 --- /dev/null +++ b/applications/FootballAction/train_lstm/scenario_lib/feature_reader.py @@ -0,0 +1,137 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import os +#from .reader_utils import DataReader +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle + from io import BytesIO +import numpy as np +import random +import code +import logging + +python_ver = sys.version_info +logger = logging.getLogger('LSTM') + + +class FeatureReader: + """ + Data reader for youtube-8M dataset, which was stored as features extracted by prior networks + This is for the three models: lstm, attention cluster, nextvlad + + dataset cfg: num_classes + batch_size + list + NextVlad only: eigen_file + """ + def __init__(self, name, mode, cfg, bs_denominator): + self.name = name + self.mode = mode + self.num_classes = cfg.MODEL.num_classes + + # set batch size and file list + self.batch_size = cfg[mode.upper()]['batch_size'] + self.droplast = cfg[mode.upper()]['droplast'] + self.filelist = cfg[mode.upper()]['filelist'] + self.eigen_file = cfg.MODEL.get('eigen_file', None) + self.seg_num = cfg.MODEL.get('seg_num', None) + self.num_gpus = bs_denominator + + def create_reader(self): + """create_reader""" + fin = open(self.filelist, 'r') + lines = fin.readlines() + fin.close() + data = [] + for line in lines: + items = line.strip().split() + data.append(items[0]) + + if self.mode == 'train': + random.shuffle(data) + + def reader(): + """reader""" + batch_out = [] + batch_out_pre = [] + yield_cnt = 0 + for i in range(len(data)): + record = data[i] + try: + pkl_filepath = record + pkl_data = pickle.load(open(pkl_filepath, 'rb')) + rgb_feature = pkl_data['image_feature'].astype(float) + audio_feature = pkl_data['audio_feature'].astype(float) + #print(pkl_filepath) + #print(rgb_feature, audio_feature) + video = pkl_filepath + if self.mode != 'infer': + label_id_info = pkl_data['label_info'] + label_cls = [label_id_info['label']] + label_one = int(label_cls[0]) + if len(label_cls) > 1: + label_index = random.randint(0, 1) + label_one = int(label_cls[label_index]) + #one_hot_label = make_one_hot(label_cls, self.num_classes) + iou_norm = float(label_id_info['norm_iou']) + batch_out.append( + (rgb_feature, audio_feature, label_one, iou_norm)) + else: + batch_out.append( + (rgb_feature, audio_feature, pkl_filepath)) + + if len(batch_out) == self.batch_size: + yield_cnt += 1 + yield batch_out + batch_out_pre = batch_out[:] + batch_out = [] + except Exception as e: + logger.warn("warning: load data filed {}".format(record)) + # padding: + # 1.0 0): + # If one (or some, yield_cnt % self.num_gpus) batch has been yielded but other gpus are + # empty, data from batch_out_pre are yielded to other gpus. If gpu=2 and yield_cnt % self.num_gpus + # =1, then k=0, batch_out_new will be yielded once. + if self.droplast == False and ((len(batch_out) > 0 and len(batch_out) < self.batch_size) or ( \ + len(batch_out) == 0 and yield_cnt % self.num_gpus > 0)): + batch_out_new = batch_out[:] + if len(batch_out_pre) == 0: + batch_out_pre = batch_out[:] + # return last batch k times + for i in range(self.num_gpus - yield_cnt % self.num_gpus - 1): + yield batch_out_pre + + len_batch_out_pre = len(batch_out_pre) + while len(batch_out_new) < self.batch_size: + index = random.randint(0, len_batch_out_pre - 1) + batch_out_new.append(batch_out_pre[index]) + yield batch_out_new + + return reader + + +def make_one_hot(label, dim=16): + one_hot_label = np.zeros(dim) + one_hot_label = one_hot_label.astype(float) + for ind in label: + one_hot_label[int(ind)] = 1 + return one_hot_label diff --git a/applications/FootballAction/train_lstm/scenario_lib/train.py b/applications/FootballAction/train_lstm/scenario_lib/train.py new file mode 100644 index 0000000000000000000000000000000000000000..abae594a9d79888a39d66e1d5dd0714877c77382 --- /dev/null +++ b/applications/FootballAction/train_lstm/scenario_lib/train.py @@ -0,0 +1,300 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import sys +import time +import argparse +import ast +import six +import numpy as np +import paddle.fluid as fluid +import accuracy_metrics +import feature_reader +import config +import action_net +import utils +import logging +import paddle + +paddle.enable_static() +o_path = os.getcwd() +sys.path.append(o_path) +#logger = loginfo.Logger() +logger = logging.getLogger('LSTM') +logger.setLevel(logging.INFO) + + +def parse_args(): + """parse_args""" + parser = argparse.ArgumentParser("Paddle Video train script") + parser.add_argument('--model_name', + type=str, + default='BaiduNet', + help='name of model to train.') + parser.add_argument('--config', + type=str, + default='configs/conf.txt', + help='path to config file of model') + parser.add_argument( + '--batch_size', + type=int, + default=None, + help='training batch size. None to use config file setting.') + parser.add_argument( + '--learning_rate', + type=float, + default=None, + help='learning rate use for training. None to use config file setting.') + parser.add_argument( + '--pretrain', + type=str, + default=None, + help= + 'path to pretrain weights. None to use default weights path in ~/.paddle/weights.' + ) + parser.add_argument( + '--resume', + type=str, + default=None, + help='path to resume training based on previous checkpoints. ' + 'None for not resuming any checkpoints.') + parser.add_argument('--use_gpu', + type=ast.literal_eval, + default=True, + help='default use gpu.') + parser.add_argument('--no_memory_optimize', + action='store_true', + default=False, + help='whether to use memory optimize in train') + parser.add_argument('--epoch_num', + type=int, + default=0, + help='epoch number, 0 for read from config file') + parser.add_argument('--valid_interval', + type=int, + default=1, + help='validation epoch interval, 0 for no validation.') + parser.add_argument('--save_dir', + type=str, + default='checkpoints', + help='directory name to save train snapshoot') + parser.add_argument('--log_interval', + type=int, + default=10, + help='mini-batch interval to log.') + args = parser.parse_args() + return args + + +def print_prog(prog): + """print_prog""" + for name, value in sorted(six.iteritems(prog.block(0).vars)): + logger.info(value) + for op in prog.block(0).ops: + logger.info("op type is {}".format(op.type)) + logger.info("op inputs are {}".format(op.input_arg_names)) + logger.info("op outputs are {}".format(op.output_arg_names)) + for key, value in sorted(six.iteritems(op.all_attrs())): + if key not in ['op_callstack', 'op_role_var']: + logger.info(" [ attrs: {}: {} ]".format(key, value)) + + +def train(args): + """train""" + logger.info("Start train program") + # parse config + config_info = config.parse_config(args.config) + train_config = config.merge_configs(config_info, 'train', vars(args)) + valid_config = config.merge_configs(config_info, 'valid', vars(args)) + valid_config['MODEL']['save_dir'] = args.save_dir + + bs_denominator = 1 + if args.use_gpu: + # check number of GPUs + gpus = os.getenv("CUDA_VISIBLE_DEVICES", "") + if gpus == "": + pass + else: + gpus = gpus.split(",") + num_gpus = len(gpus) + assert num_gpus == train_config.TRAIN.num_gpus, \ + "num_gpus({}) set by CUDA_VISIBLE_DEVICES" \ + "shoud be the same as that" \ + "set in {}({})".format( + num_gpus, args.config, train_config.TRAIN.num_gpus) + bs_denominator = train_config.TRAIN.num_gpus + + # adaptive batch size + train_batch_size_in = train_config.TRAIN.batch_size + # train_learning_rate_in = train_config.TRAIN.learning_rate + train_config.TRAIN.batch_size = min( + int(train_config.TRAIN.num_samples / 10), train_batch_size_in) + train_config.TRAIN.batch_size = int( + train_config.TRAIN.batch_size / bs_denominator) * bs_denominator + train_config.TRAIN.batch_size = max(train_config.TRAIN.batch_size, + bs_denominator) + # train_config.TRAIN.learning_rate = float(train_learning_rate_in) / float(train_batch_size_in) \ + # * train_config.TRAIN.batch_size + + val_batch_size_in = valid_config.VALID.batch_size + valid_config.VALID.batch_size = min( + int(valid_config.VALID.num_samples / 10), val_batch_size_in) + valid_config.VALID.batch_size = int( + valid_config.VALID.batch_size / bs_denominator) * bs_denominator + valid_config.VALID.batch_size = max(valid_config.VALID.batch_size, + bs_denominator) + + # model remove bn when train every gpu batch_size is small + if int(train_config.TRAIN.batch_size / + bs_denominator) < train_config.MODEL.modelbn_min_everygpu_bs: + train_config.MODEL.with_bn = False + valid_config.MODEL.with_bn = False + else: + train_config.MODEL.with_bn = True + valid_config.MODEL.with_bn = True + + config.print_configs(train_config, 'Train') + train_model = action_net.ActionNet(args.model_name, + train_config, + mode='train') + valid_model = action_net.ActionNet(args.model_name, + valid_config, + mode='valid') + + # build model + startup = fluid.Program() + train_prog = fluid.Program() + with fluid.program_guard(train_prog, startup): + with fluid.unique_name.guard(): + train_model.build_input(use_pyreader=True) + train_model.build_model() + # for the input, has the form [data1, data2,..., label], so train_feeds[-1] is label + train_feeds = train_model.feeds() + train_fetch_list = train_model.fetches() + train_loss = train_fetch_list[0] + for item in train_fetch_list: + item.persistable = True + optimizer = train_model.optimizer() + optimizer.minimize(train_loss) + train_pyreader = train_model.pyreader() + + valid_prog = fluid.Program() + with fluid.program_guard(valid_prog, startup): + with fluid.unique_name.guard(): + valid_model.build_input(use_pyreader=True) + valid_model.build_model() + valid_feeds = valid_model.feeds() + valid_fetch_list = valid_model.fetches() + valid_pyreader = valid_model.pyreader() + for item in valid_fetch_list: + item.persistable = True + + valid_prog = valid_prog.clone(for_test=True) + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + exe.run(startup) + + #print_prog(train_prog) + #print_prog(valid_prog) + + if args.resume: + # if resume weights is given, load resume weights directly + assert os.path.exists(args.resume), \ + "Given resume weight dir {} not exist.".format(args.resume) + + def if_exist(var): + return os.path.exists(os.path.join(args.resume, var.name)) + + fluid.io.load_vars(exe, + args.resume, + predicate=if_exist, + main_program=train_prog) + else: + # if not in resume mode, load pretrain weights + if args.pretrain: + assert os.path.exists(args.pretrain), \ + "Given pretrain weight dir {} not exist.".format(args.pretrain) + pretrain = args.pretrain or train_model.get_pretrain_weights() + if pretrain: + train_model.load_pretrain_params_file(exe, pretrain, train_prog, + place) + + build_strategy = fluid.BuildStrategy() + build_strategy.enable_inplace = True + + compiled_train_prog = fluid.compiler.CompiledProgram( + train_prog).with_data_parallel(loss_name=train_loss.name, + build_strategy=build_strategy) + compiled_valid_prog = fluid.compiler.CompiledProgram( + valid_prog).with_data_parallel(share_vars_from=compiled_train_prog, + build_strategy=build_strategy) + # get reader + train_config.TRAIN.batch_size = int(train_config.TRAIN.batch_size / + bs_denominator) + valid_config.VALID.batch_size = int(valid_config.VALID.batch_size / + bs_denominator) + print("config setting") + train_dataload = feature_reader.FeatureReader(args.model_name.upper(), + 'train', train_config, + bs_denominator) + train_reader = train_dataload.create_reader() + print("train reader") + valid_dataload = feature_reader.FeatureReader(args.model_name.upper(), + 'valid', valid_config, + bs_denominator) + valid_reader = valid_dataload.create_reader() + + # get metrics + train_metrics = accuracy_metrics.MetricsCalculator(args.model_name.upper(), + 'train', train_config) + valid_metrics = accuracy_metrics.MetricsCalculator(args.model_name.upper(), + 'valid', valid_config) + + epochs = args.epoch_num or train_model.epoch_num() + print("epoch is ", epochs) + + exe_places = fluid.cuda_places() if args.use_gpu else fluid.cpu_places() + train_pyreader.decorate_sample_list_generator(train_reader, + places=exe_places) + valid_pyreader.decorate_sample_list_generator(valid_reader, + places=exe_places) + + utils.train_with_pyreader( + exe, + train_prog, + compiled_train_prog, # train_exe, + train_pyreader, + train_fetch_list, + train_metrics, + epochs=epochs, + log_interval=args.log_interval, + valid_interval=args.valid_interval, + save_dir=args.save_dir, + save_model_name=args.model_name, + compiled_test_prog=compiled_valid_prog, # test_exe=valid_exe, + test_pyreader=valid_pyreader, + test_fetch_list=valid_fetch_list, + test_metrics=valid_metrics) + + logger.info("Finish program") + + +if __name__ == "__main__": + args = parse_args() + logger.info(args) + + if not os.path.exists(args.save_dir): + os.makedirs(args.save_dir) + + train(args) diff --git a/applications/FootballAction/train_lstm/scenario_lib/utils.py b/applications/FootballAction/train_lstm/scenario_lib/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c65e2268928461bc9c169bc1597e95b1d5a7f33d --- /dev/null +++ b/applications/FootballAction/train_lstm/scenario_lib/utils.py @@ -0,0 +1,189 @@ +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import time +import shutil +import numpy as np +import paddle.fluid as fluid +import logging + +#logger = loginfo.Logger() +logger = logging.getLogger('LSTM') +best_test_acc1 = 0 +min_test_loss = float("inf") + + +def log_lr_and_step(): + """log_lr_and_step""" + try: + # In optimizers, if learning_rate is set as constant, lr_var + # name is 'learning_rate_0', and iteration counter is not + # recorded. If learning_rate is set as decayed values from + # learning_rate_scheduler, lr_var name is 'learning_rate', + # and iteration counter is recorded with name '@LR_DECAY_COUNTER@', + # better impliment is required here + lr_var = fluid.global_scope().find_var("learning_rate") + if not lr_var: + lr_var = fluid.global_scope().find_var("learning_rate_0") + lr = np.array(lr_var.get_tensor()) + + lr_count = '[-]' + lr_count_var = fluid.global_scope().find_var("@LR_DECAY_COUNTER@") + if lr_count_var: + lr_count = np.array(lr_count_var.get_tensor()) + # logger.info("------- learning rate {}, learning rate counter {} -----" + # .format(np.array(lr), np.array(lr_count))) + except BaseException: + logger.warn("Unable to get learning_rate and LR_DECAY_COUNTER.") + + +def test_with_pyreader(exe, + compiled_test_prog, + test_pyreader, + test_fetch_list, + test_metrics, + epoch, + log_interval=0, + save_model_name=''): + """test_with_pyreader""" + if not test_pyreader: + logger.error("[TEST] get pyreader failed.") + + test_loss = -1 + test_acc1 = 0 + test_status = False + for retry in range(3): + test_metrics.reset() + test_iter = 0 + + try: + for data in test_pyreader(): + test_outs = exe.run(compiled_test_prog, + fetch_list=test_fetch_list, + feed=data) + loss = np.array(test_outs[0]) + pred_label = np.array(test_outs[1]) + pred_iou = np.array(test_outs[2]) + label = np.array(test_outs[-2]) + iou = np.array(test_outs[-1]) + + test_metrics.accumulate(loss, pred_label, label, pred_iou, iou) + test_iter += 1 + test_loss, test_acc1, test_iou = test_metrics.finalize_and_log_out( \ + info='[TEST] Finish Epoch {}: '.format(epoch)) + test_status = True + return test_status, test_loss, test_acc1, test_iou + except Exception as e: + logger.warn( + "[TEST] Epoch {} fail to execute test or calculate metrics: {}". + format(epoch, e)) + logger.warn( + "[TEST] Finish... Epoch {} fail to execute test or calculate metrics.". + format(epoch)) + return test_status, test_loss, test_acc1, test_iou + + +def train_with_pyreader(exe, train_prog, compiled_train_prog, train_pyreader, \ + train_fetch_list, train_metrics, epochs=10, \ + log_interval=0, valid_interval=0, save_dir='./', \ + save_model_name='model', \ + compiled_test_prog=None, test_pyreader=None, \ + test_fetch_list=None, test_metrics=None): + """train_with_pyreader""" + if not train_pyreader: + logger.error("[TRAIN] get pyreader failed.") + epoch_periods = [] + train_loss = 0 + for epoch in range(epochs): + + train_metrics.reset() + train_iter = 0 + epoch_periods = [] + + for data in train_pyreader(): + # logger.info("epoch = {} train_iter = {}".format(epoch, train_iter)) + try: + cur_time = time.time() + train_outs = exe.run(compiled_train_prog, + fetch_list=train_fetch_list, + feed=data) + log_lr_and_step() + period = time.time() - cur_time + epoch_periods.append(period) + loss = np.array(train_outs[0]) + pred_label = np.array(train_outs[1]) + pred_iou = np.array(train_outs[2]) + label = np.array(train_outs[-2]) + iou = np.array(train_outs[-1]) + train_metrics.accumulate(loss, pred_label, label, pred_iou, iou) + if log_interval > 0 and (train_iter % log_interval == 0): + train_metrics.finalize_and_log_out( \ + info='[TRAIN] Epoch {}, iter {} average: '.format(epoch, train_iter)) + except Exception as e: + logger.info( + "[TRAIN] Epoch {}, iter {} data training failed: {}".format( + epoch, train_iter, str(e))) + train_iter += 1 + + if len(epoch_periods) < 1: + logger.info( + 'No iteration was executed, please check the data reader') + sys.exit(1) + + logger.info( + '[TRAIN] Epoch {} training finished, average time: {}'.format( + epoch, np.mean(epoch_periods))) + train_metrics.finalize_and_log_out( \ + info='[TRAIN] Finished ... Epoch {} all iters average: '.format(epoch)) + + #save_postfix = "_epoch{}".format(epoch) + #save_model(exe, train_prog, save_dir, save_model_name, save_postfix) + + # save models of min loss in best acc epochs + if compiled_test_prog and valid_interval > 0 and ( + epoch + 1) % valid_interval == 0: + test_status, test_loss, test_acc1, test_iou = test_with_pyreader( + exe, compiled_test_prog, test_pyreader, test_fetch_list, + test_metrics, epoch, log_interval, save_model_name) + global best_test_acc1 + global min_test_loss + if test_status and (test_acc1 > best_test_acc1 or + (test_acc1 == best_test_acc1 + and test_loss < min_test_loss)): + best_test_acc1 = test_acc1 + min_test_loss = test_loss + save_postfix = "_epoch{}_acc{}".format(epoch, best_test_acc1) + save_model(exe, train_prog, save_dir, save_model_name, + save_postfix) + + +def save_model(exe, program, save_dir, model_name, postfix=None): + """save_model""" + #model_path = os.path.join(save_dir, model_name + postfix) + #if os.path.isdir(model_path): + # shutil.rmtree(model_path) + ##fluid.io.save_persistables(exe, model_path, main_program=program) + #save_vars = [x for x in program.list_vars() \ + # if isinstance(x, fluid.framework.Parameter)] + #fluid.io.save_vars(exe, dirname=model_path, main_program=program, vars=save_vars, filename="param") + + if not os.path.isdir(save_dir): + os.makedirs(save_dir) + saved_model_name = model_name + postfix + + fluid.save(program, os.path.join(save_dir, saved_model_name)) + + return diff --git a/applications/FootballAction/train_lstm/utils/config_utils.py b/applications/FootballAction/train_lstm/utils/config_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..1acb9d28fc0bf15c54b210f6ed11c13d803c1043 --- /dev/null +++ b/applications/FootballAction/train_lstm/utils/config_utils.py @@ -0,0 +1,75 @@ +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import yaml +from .utility import AttrDict +import logging +logger = logging.getLogger(__name__) + +CONFIG_SECS = [ + 'train', + 'valid', + 'test', + 'infer', +] + + +def parse_config(cfg_file): + """Load a config file into AttrDict""" + import yaml + with open(cfg_file, 'r') as fopen: + yaml_config = AttrDict(yaml.load(fopen, Loader=yaml.Loader)) + create_attr_dict(yaml_config) + return yaml_config + + +def create_attr_dict(yaml_config): + from ast import literal_eval + for key, value in yaml_config.items(): + if type(value) is dict: + yaml_config[key] = value = AttrDict(value) + if isinstance(value, str): + try: + value = literal_eval(value) + except BaseException: + pass + if isinstance(value, AttrDict): + create_attr_dict(yaml_config[key]) + else: + yaml_config[key] = value + return + + +def merge_configs(cfg, sec, args_dict): + assert sec in CONFIG_SECS, "invalid config section {}".format(sec) + sec_dict = getattr(cfg, sec.upper()) + for k, v in args_dict.items(): + if v is None: + continue + try: + if hasattr(sec_dict, k): + setattr(sec_dict, k, v) + except: + pass + return cfg + + +def print_configs(cfg, mode): + logger.info("---------------- {:>5} Arguments ----------------".format( + mode)) + for sec, sec_items in cfg.items(): + logger.info("{}:".format(sec)) + for k, v in sec_items.items(): + logger.info(" {}:{}".format(k, v)) + logger.info("-------------------------------------------------") diff --git a/applications/FootballAction/train_lstm/utils/utility.py b/applications/FootballAction/train_lstm/utils/utility.py new file mode 100644 index 0000000000000000000000000000000000000000..ced1e7d757ff5697c0fe61f130849524491da3b0 --- /dev/null +++ b/applications/FootballAction/train_lstm/utils/utility.py @@ -0,0 +1,71 @@ +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import signal +import logging +import paddle +import paddle.fluid as fluid + +__all__ = ['AttrDict'] + +logger = logging.getLogger(__name__) + + +def _term(sig_num, addition): + print('current pid is %s, group id is %s' % (os.getpid(), os.getpgrp())) + os.killpg(os.getpgid(os.getpid()), signal.SIGKILL) + + +signal.signal(signal.SIGTERM, _term) +signal.signal(signal.SIGINT, _term) + + +class AttrDict(dict): + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value + +def check_cuda(use_cuda, err = \ + "\nYou can not set use_gpu = True in the model because you are using paddlepaddle-cpu.\n \ + Please: 1. Install paddlepaddle-gpu to run your models on GPU or 2. Set use_gpu = False to run models on CPU.\n" + ): + try: + if use_cuda == True and fluid.is_compiled_with_cuda() == False: + print(err) + sys.exit(1) + except Exception as e: + pass + + +def check_version(): + """ + Log error and exit when the installed version of paddlepaddle is + not satisfied. + """ + err = "PaddlePaddle version 1.6 or higher is required, " \ + "or a suitable develop version is satisfied as well. \n" \ + "Please make sure the version is good with your code." \ + + try: + fluid.require_version('1.6.0') + except Exception as e: + logger.error(err) + sys.exit(1) diff --git a/applications/FootballAction/train_proposal/configs/bmn_football_v2.0.yaml b/applications/FootballAction/train_proposal/configs/bmn_football_v2.0.yaml new file mode 100644 index 0000000000000000000000000000000000000000..87a8f5d49448f5bc6af43c6cebd749967e119e6d --- /dev/null +++ b/applications/FootballAction/train_proposal/configs/bmn_football_v2.0.yaml @@ -0,0 +1,89 @@ +MODEL: #MODEL field + framework: "BMNLocalizer" + backbone: + name: "BMN" + feat_dim: 2688 + tscale: 200 + dscale: 200 + prop_boundary_ratio: 0.5 + num_sample: 32 + num_sample_perbin: 3 + loss: + name: "BMNLoss" + tscale: 200 + dscale: 200 + +DATASET: #DATASET field + batch_size: 4 # 4 #single card bacth size + test_batch_size: 1 + num_workers: 8 + train: + format: "BMNDataset" + file_path: "/home/work/PaddleVideo/applications/FootballAction/datasets/train_list/football_bmn_label.json" + subset: "train" + valid: + format: "BMNDataset" + file_path: "/home/work/PaddleVideo/applications/FootballAction/datasets/train_list/football_bmn_label.json" + subset: "validation" + test: + format: "BMNDataset" + test_mode: True + file_path: "/home/work/PaddleVideo/applications/FootballAction/datasets/train_list/football_bmn_label.json" + subset: "validation" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data + load_feat: + name: "LoadFeat" + feat_path: "/home/work/PaddleVideo/applications/FootballAction/datasets" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + + valid: #Mandotary, indicate the pipeline to deal with the training data + load_feat: + name: "LoadFeat" + feat_path: "/home/work/PaddleVideo/applications/FootballAction/datasets" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + + test: #Mandatory, indicate the pipeline to deal with the validing data + load_feat: + name: "LoadFeat" + feat_path: "/home/work/PaddleVideo/applications/FootballAction/datasets" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + iter_step: True + name: 'CustomPiecewiseDecay' + boundaries: [4200] + values: [0.001, 0.0001] + weight_decay: + name: 'L2' + value: 1e-4 + +INFERENCE: + name: 'BMN_Inference_helper' + feat_dim: 2688 + dscale: 100 + tscale: 100 + result_path: "/workspace/PaddleVideo/applications/FootballAction/BMN_INFERENCE_results" + +model_name: BMN +epochs: 20 #Mandatory, total epoch +log_level: "INFO" +resume_from: "" #checkpoint path. diff --git a/applications/FootballAction/train_proposal/configs/pptsm_football_v2.0.yaml b/applications/FootballAction/train_proposal/configs/pptsm_football_v2.0.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dea156226465c508f4c99b43da5ddaaa1cf057f3 --- /dev/null +++ b/applications/FootballAction/train_proposal/configs/pptsm_football_v2.0.yaml @@ -0,0 +1,130 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network. + backbone: #Mandatory, indicate the type of backbone. + name: "ResNetTweaksTSM" #Mandatory, The name of backbone. + pretrained: "/home/work/PaddleVideo/applications/FootballAction/pretrain/ResNet50_vd_ssld_v2_pretrained.pdparams" + num_seg: 8 + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSMHead" #Mandatory, indicate the type of head + num_classes: 8 # 16 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + test_batch_size: 16 + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameDataset_Sport" #Mandatory, indicate the type of dataset + data_prefix: "" #Mandatory, train data root path + file_path: "/home/work/PaddleVideo/applications/FootballAction/datasets/train_list/football_tsn_train.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset_Sport" #Mandatory, indicate the type of dataset + data_prefix: "" #Mandatory, valid data root path + file_path: "/home/work/PaddleVideo/applications/FootballAction/datasets/train_list/football_tsn_val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset_Sport" #Mandatory, indicate the type of dataset + data_prefix: "" #Mandatory, valid data root path + file_path: "/home/work/PaddleVideo/applications/FootballAction/datasets/train_list/football_tsn_val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data + decode: + name: "FrameDecoder" + sample: + name: "SamplerPkl" + num_seg: 8 + seg_len: 1 + valid_mode: False + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + - MultiScaleCrop: + target_size: 256 + - RandomCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data + decode: + name: "FrameDecoder" + sample: + name: "SamplerPkl" + valid_mode: True + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "FrameDecoder" + sample: + name: "SamplerPkl" + valid_mode: True + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler + name: 'PiecewiseDecay' + boundaries: [40, 60] + values: [0.01, 0.001, 0.0001] #4 cards * 16 batch size + weight_decay: + name: 'L2' + value: 2e-4 + +MIX: + name: "Mixup" + alpha: 0.2 + +METRIC: + name: 'CenterCropMetric' + + +PRECISEBN: + preciseBN_interval: 5 # epoch interval to do preciseBN, default 1. + num_iters_preciseBN: 80 # how many batches used to do preciseBN, default 200. + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "ppTSM" +log_interval: 20 #Optional, the interal of logger, default:10 +epochs: 80 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" +resume_from: "" #checkpoint path. diff --git a/applications/Ma-Net/README.md b/applications/Ma-Net/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6b741a29a49126c5a47f3418855f96bb6fd8e838 --- /dev/null +++ b/applications/Ma-Net/README.md @@ -0,0 +1,47 @@ +[简体中文](README_cn.md) | English + +# Ma-Net + +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) + + + + +## Introduction + +This is the paddle implementation of the CVPR2020 paper "[Memory aggregation networks for efficient interactive video object segmentation](https://arxiv.org/abs/2003.13246)". + +![avatar](images/1836-teaser.gif) + +This code currently supports model test and model training on DAVIS dataset, and model inference on any given video will be provided in few days. + + + +## Data + +Please refer to DAVIS data download and preparation doc [DAVIS-data](dataloaders/DAVIS2017.md) + +## Train and Test +- You can download [pertained model for stage1](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/DeeplabV3_coco.pdparams) decompress it for stage1 training。 + +- You can download [trained model of stage1](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MaNet_davis2017_stage1.pdparams) decompress it for stage2 training directly skipping stage1 training。 + +``` +sh run_local.sh +``` + +- You can download [our model](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MANet_davis2017.pdparams) decompress it for testing. + + + +Test accuracy in DAVIS2017: + +| J@60 | AUC | +| :---: | :---: | +| 0.761 | 0.749 | diff --git a/applications/Ma-Net/README_cn.md b/applications/Ma-Net/README_cn.md new file mode 100644 index 0000000000000000000000000000000000000000..78d4d1c832be716e14286eaec090867e0b782f4f --- /dev/null +++ b/applications/Ma-Net/README_cn.md @@ -0,0 +1,46 @@ +[English](README.md) | 简体中文 + +# Ma-Net视频切分模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) + + + + +## 模型简介 + +这是CVPR2020论文"[Memory aggregation networks for efficient interactive video object segmentation](https://arxiv.org/abs/2003.13246)"的Paddle实现。 + +![avatar](images/1836-teaser.gif) + +此代码目前支持在 DAVIS 数据集上进行模型测试和模型训练,并且将在之后提供对任何给定视频的模型推理。 + + +## 数据准备 + +DAVIS数据下载及准备请参考[DAVIS2017数据准备](dataloaders/DAVIS2017_cn.md) + + +## 模型训练与测试 +- 您可以下载[paddle版本的stage1预训练模型](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/DeeplabV3_coco.pdparams) 解压缩它以用于训练的第一阶段。 + +- 您可以下载[stage1训练结果模型](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MaNet_davis2017_stage1.pdparams) 解压缩它以直接训练的第二阶段跳过第一阶段的训练。 + + ```bash + sh run.sh + ``` + +- 您可以下载[我们的模型](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MANet_davis2017.pdparams) 解压缩它以用于测试。 + + +在 DAVIS2017上的测试精度: + +| J@60 | AUC | +| :---: | :---: | +| 0.761 | 0.749 | diff --git a/applications/Ma-Net/config.py b/applications/Ma-Net/config.py new file mode 100644 index 0000000000000000000000000000000000000000..d584c273f7697c4c83d8e1f314a69b5e36e7006c --- /dev/null +++ b/applications/Ma-Net/config.py @@ -0,0 +1,96 @@ +import paddle +import argparse +import os +import sys +import cv2 +import time + + +def str2bool(v): + if isinstance(v, bool): + return v + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + + +parser = argparse.ArgumentParser(description='intvos config') +parser.add_argument('--ROOT_DIR', + type=str, + default=os.path.abspath( + os.path.join(os.path.dirname("__file__")))) +parser.add_argument('--EXP_NAME', type=str, default='deeplabv3+coco') +parser.add_argument('--SAVE_RESULT_DIR', type=str, default='../afs/result/') +parser.add_argument('--SAVE_VOS_RESULT_DIR', type=str, default='') +parser.add_argument('--NUM_WORKER', type=int, default=4) +parser.add_argument('--KNNS', type=int, default=1) +parser.add_argument('--PRETRAINED_MODEL', + type=str, + default='./model_best.pth.tar') +parser.add_argument( + '--RESULT_ROOT', + type=str, + default=os.path.join('../afs/vos_result/result_total_80000')) +######DATA_CONFIG +parser.add_argument('--DATA_NAME', type=str, default='COCO2017') +parser.add_argument('--DATA_AUG', type=str2bool, default=True) +parser.add_argument('--DATA_WORKERS', type=int, default=4) +parser.add_argument('--DATA_RESCALE', type=int, default=416) +parser.add_argument('--DATA_RANDOMCROP', type=int, default=416) +parser.add_argument('--DATA_RANDOMROTATION', type=int, default=0) +parser.add_argument('--DATA_RANDOM_H', type=int, default=10) +parser.add_argument('--DATA_RANDOM_S', type=int, default=10) +parser.add_argument('--DATA_RANDOM_V', type=int, default=10) +parser.add_argument('--DATA_RANDOMFLIP', type=float, default=0.5) +parser.add_argument('--DATA_ROOT', type=str, default='../data/DAVIS') + +######MODEL_CONFIG +parser.add_argument('--MODEL_NAME', type=str, default='deeplabv3plus') +parser.add_argument('--MODEL_BACKBONE', type=str, default='res101_atrous') +parser.add_argument('--MODEL_OUTPUT_STRIDE', type=int, default=16) +parser.add_argument('--MODEL_ASPP_OUTDIM', type=int, default=256) +parser.add_argument('--MODEL_SHORTCUT_DIM', type=int, default=48) +parser.add_argument('--MODEL_SHORTCUT_KERNEL', type=int, default=1) +parser.add_argument('--MODEL_NUM_CLASSES', type=int, default=21) +parser.add_argument('--MODEL_SEMANTIC_EMBEDDING_DIM', type=int, default=100) +parser.add_argument('--MODEL_HEAD_EMBEDDING_DIM', type=int, default=256) +parser.add_argument('--MODEL_LOCAL_DOWNSAMPLE', type=str2bool, default=True) +parser.add_argument('--MODEL_MAX_LOCAL_DISTANCE', type=int, default=12) +parser.add_argument('--MODEL_SELECT_PERCENT', type=float, default=0.8) +parser.add_argument('--MODEL_USEIntSeg', type=str2bool, default=False) + +######TRAIN_CONFIG +parser.add_argument('--TRAIN_LR', type=float, default=0.0007) +parser.add_argument('--TRAIN_LR_GAMMA', type=float, default=0.1) +parser.add_argument('--TRAIN_MOMENTUM', type=float, default=0.9) +parser.add_argument('--TRAIN_WEIGHT_DECAY', type=float, default=0.00004) +parser.add_argument('--TRAIN_POWER', type=float, default=0.9) +parser.add_argument('--TRAIN_BATCH_SIZE', type=int, default=2) +parser.add_argument('--TRAIN_SHUFFLE', type=str2bool, default=True) +parser.add_argument('--TRAIN_CLIP_GRAD_NORM', type=float, default=5.) +parser.add_argument('--TRAIN_MINEPOCH', type=int, default=9) +parser.add_argument('--TRAIN_TOTAL_STEPS', type=int, default=101000) +parser.add_argument('--TRAIN_LOSS_LAMBDA', type=int, default=0) +parser.add_argument('--TRAIN_TBLOG', type=str2bool, default=False) +parser.add_argument('--TRAIN_BN_MOM', type=float, + default=0.9997) # fixed. difs between paddle and torch. +parser.add_argument('--TRAIN_TOP_K_PERCENT_PIXELS', type=float, default=0.15) +parser.add_argument('--TRAIN_HARD_MINING_STEP', type=int, default=50000) +parser.add_argument('--TRAIN_LR_STEPSIZE', type=int, default=2000) +parser.add_argument('--TRAIN_INTER_USE_TRUE_RESULT', + type=str2bool, + default=True) +parser.add_argument('--TRAIN_RESUME_DIR', type=str, default='') + +parser.add_argument('--LOG_DIR', type=str, default=os.path.join('./log')) + +parser.add_argument('--TEST_CHECKPOINT', + type=str, + default='save_step_100000.pth') +parser.add_argument('--TEST_MODE', type=str2bool, default=False) + +cfg = parser.parse_args() +cfg.TRAIN_EPOCHS = int(200000 * cfg.TRAIN_BATCH_SIZE / 60.) diff --git a/applications/Ma-Net/dataloaders/DAVIS2017.md b/applications/Ma-Net/dataloaders/DAVIS2017.md new file mode 100644 index 0000000000000000000000000000000000000000..d3202331ba37cc9f4b76da90293573ae7e0cd2c5 --- /dev/null +++ b/applications/Ma-Net/dataloaders/DAVIS2017.md @@ -0,0 +1,27 @@ +[简体中文](../../zh-CN/dataset/DAVIS2017.md) | English + +# DAVIS2017 Data Preparation + +## 1.Data Download + +Download [DAVIS2017](https://data.vision.ee.ethz.ch/csergi/share/davis/DAVIS-2017-trainval-480p.zip) and [scribbles](https://data.vision.ee.ethz.ch/csergi/share/DAVIS-Interactive/DAVIS-2017-scribbles-trainval.zip) into one folder. Please refer to [DAVIS](https://davischallenge.org/davis2017/code.html). + +If you need the file "DAVIS2017/ImageSets/2017/v_a_l_instances.txt", please refer to the link [google]( https://drive.google.com/file/d/1aLPaQ_5lyAi3Lk3d2fOc_xewSrfcrQlc/view?usp=sharing) + +## 2.Folder Structure + +In the context of the whole project (for Ma-Net only), the folder structure will look like: + +```shell +PaddleVideo +├── configs +├── paddlevideo +├── docs +├── tools +├── data +│ └── DAVIS2017 +│ │ ├── Annotations +│ │ ├── ImageSets +│ │ ├── JPEGImages +│ │ └── Scribbles +``` diff --git a/applications/Ma-Net/dataloaders/DAVIS2017_cn.md b/applications/Ma-Net/dataloaders/DAVIS2017_cn.md new file mode 100644 index 0000000000000000000000000000000000000000..018e948355ccb54782c608c0a4f1264f10654948 --- /dev/null +++ b/applications/Ma-Net/dataloaders/DAVIS2017_cn.md @@ -0,0 +1,27 @@ +[English](../../en/dataset/DAVIS2017.md) | 简体中文 + +# DAVIS2017 数据集准备 + +## 1.数据下载 + +下载 [DAVIS2017](https://data.vision.ee.ethz.ch/csergi/share/davis/DAVIS-2017-trainval-480p.zip) 和 [scribbles](https://data.vision.ee.ethz.ch/csergi/share/DAVIS-Interactive/DAVIS-2017-scribbles-trainval.zip)到同一个文件夹中。请参阅[DAVIS](https://davischallenge.org/davis2017/code.html). + +如果您需要文件"DAVIS2017/ImageSets/2017/v_a_l_instances.txt",请参阅[google](https://drive.google.com/file/d/1aLPaQ_5lyAi3Lk3d2fOc_xewSrfcrQlc/view?usp=sharing)链接 + +## 2.目录结构 + +整个项目(Ma-Net)的目录结构如下所示: + +```shell +PaddleVideo +├── configs +├── paddlevideo +├── docs +├── tools +├── data +│ └── DAVIS2017 +│ │ ├── Annotations +│ │ ├── ImageSets +│ │ ├── JPEGImages +│ │ └── Scribbles +``` diff --git a/applications/Ma-Net/dataloaders/__init__.py b/applications/Ma-Net/dataloaders/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/Ma-Net/dataloaders/custom_transforms_f.py b/applications/Ma-Net/dataloaders/custom_transforms_f.py new file mode 100644 index 0000000000000000000000000000000000000000..3a2890983bda88351f3f782240ccc34201d6ac1a --- /dev/null +++ b/applications/Ma-Net/dataloaders/custom_transforms_f.py @@ -0,0 +1,416 @@ +import os +import random +import cv2 +import numpy as np +import paddle +from PIL import Image +import dataloaders.helpers as helpers +from davisinteractive.utils.operations import bresenham +from paddle.vision.transforms import functional as F + +cv2.setNumThreads(0) +NEW_BRANCH = True + + +class Resize(object): + """Rescale the image in a sample to a given size. + + Args: + output_size (tuple or int): Desired output size. If tuple, output is + matched to output_size. If int, smaller of image edges is matched + to output_size keeping aspect ratio the same. + """ + def __init__(self, output_size): + assert isinstance(output_size, (int, tuple)) + if isinstance(output_size, int): + self.output_size = (output_size, output_size) + else: + self.output_size = output_size + + # self.seg_interpolation = cv2.INTER_CUBIC if is_continuous else cv2.INTER_NEAREST + # self.fix = fix + + def __call__(self, sample): + img1 = sample['img1'] + # img2 = sample['img2'] + # ref_img=sample['ref_img'] + h, w = img1.shape[:2] + if self.output_size == (h, w): + return sample + + else: + new_h, new_w = self.output_size + new_h, new_w = int(new_h), int(new_w) + for elem in sample.keys(): + if 'meta' in elem: + continue + tmp = sample[elem] + if elem == 'img1' or elem == 'img2' or elem == 'ref_img': + flagval = cv2.INTER_CUBIC + else: + flagval = cv2.INTER_NEAREST + + tmp = cv2.resize(tmp, dsize=(new_w, new_h), interpolation=flagval) + sample[elem] = tmp + + return sample + + +class RandomCrop(object): + """Crop randomly the image in a sample. + + Args: + output_size (tuple or int): Desired output size. If int, square crop + is made. + """ + def __init__(self, output_size, step=None): + assert isinstance(output_size, (int, tuple)) + if isinstance(output_size, int): + self.output_size = (output_size, output_size) + else: + assert len(output_size) == 2 + self.output_size = output_size + self.step = step + + def __call__(self, sample): + + image = sample['img1'] + h, w = image.shape[:2] + new_h, new_w = self.output_size + + new_h = h if new_h >= h else new_h + new_w = w if new_w >= w else new_w + is_contain_obj = False + + if self.step is None: + while not is_contain_obj: + # step += 1 + top = np.random.randint(0, h - new_h + 1) + left = np.random.randint(0, w - new_w + 1) + ref_scribble_label = sample['ref_scribble_label'] + new_ref_scribble_label = ref_scribble_label[top:top + new_h, + left:left + new_w] + if len(np.unique(new_ref_scribble_label)) == 1: + continue + else: + + for elem in sample.keys(): + if 'meta' in elem: + continue + + tmp = sample[elem] + tmp = tmp[top:top + new_h, left:left + new_w] + sample[elem] = tmp + break + else: + st = 0 + while not is_contain_obj and st < self.step: + st += 1 + top = np.random.randint(0, h - new_h + 1) + left = np.random.randint(0, w - new_w + 1) + ref_scribble_label = sample['ref_scribble_label'] + new_ref_scribble_label = ref_scribble_label[top:top + new_h, + left:left + new_w] + if len(np.unique( + new_ref_scribble_label)) == 1 or st < self.step - 1: + continue + else: + + for elem in sample.keys(): + if 'meta' in elem: + continue + + tmp = sample[elem] + tmp = tmp[top:top + new_h, left:left + new_w] + sample[elem] = tmp + break + + return sample + + +class ScaleNRotate(object): + """Scale (zoom-in, zoom-out) and Rotate the image and the ground truth. + Args: + two possibilities: + 1. rots (tuple): (minimum, maximum) rotation angle + scales (tuple): (minimum, maximum) scale + 2. rots [list]: list of fixed possible rotation angles + scales [list]: list of fixed possible scales + """ + def __init__(self, rots=(-30, 30), scales=(.75, 1.25)): + assert (isinstance(rots, type(scales))) + self.rots = rots + self.scales = scales + + def __call__(self, sample): + + if type(self.rots) == tuple: + # Continuous range of scales and rotations + rot = (self.rots[1] - self.rots[0]) * random.random() - \ + (self.rots[1] - self.rots[0]) / 2 + + sc = (self.scales[1] - self.scales[0]) * random.random() - \ + (self.scales[1] - self.scales[0]) / 2 + 1 + elif type(self.rots) == list: + # Fixed range of scales and rotations + rot = self.rots[random.randint(0, len(self.rots))] + sc = self.scales[random.randint(0, len(self.scales))] + + for elem in sample.keys(): + if 'meta' in elem: + continue + + tmp = sample[elem] + + h, w = tmp.shape[:2] + center = (w / 2, h / 2) + assert (center != 0) # Strange behaviour warpAffine + M = cv2.getRotationMatrix2D(center, rot, sc) + + if ((tmp == 0) | (tmp == 1)).all(): + flagval = cv2.INTER_NEAREST + else: + flagval = cv2.INTER_CUBIC + tmp = cv2.warpAffine(tmp, M, (w, h), flags=flagval) + + sample[elem] = tmp + + return sample + + +class RandomScale(object): + """Randomly resize the image and the ground truth to specified scales. + Args: + scales (list): the list of scales + """ + def __init__(self, scales=[0.75, 1, 1.25]): + self.scales = scales + + def __call__(self, sample): + + # Fixed range of scales + sc = self.scales[random.randint(0, len(self.scales) - 1)] + + for elem in sample.keys(): + if 'meta' in elem: + continue + tmp = sample[elem] + + if elem == 'img1' or elem == 'img2' or elem == 'ref_img': + flagval = cv2.INTER_CUBIC + else: + flagval = cv2.INTER_NEAREST + + tmp = cv2.resize(tmp, None, fx=sc, fy=sc, interpolation=flagval) + + sample[elem] = tmp + + return sample + + +class RandomHorizontalFlip(object): + """Horizontally flip the given image and ground truth randomly with a probability of 0.5.""" + def __init__(self, prob): + self.p = prob + + def __call__(self, sample): + + if random.random() < self.p: + for elem in sample.keys(): + if 'meta' in elem: + continue + tmp = sample[elem] + tmp = cv2.flip(tmp, flipCode=1) + sample[elem] = tmp + + return sample + + +class SubtractMeanImage(object): + def __init__(self, mean, change_channels=False): + self.mean = mean + self.change_channels = change_channels + + def __call__(self, sample): + for elem in sample.keys(): + if 'image' in elem: + if self.change_channels: + sample[elem] = sample[elem][:, :, [2, 1, 0]] + sample[elem] = np.subtract( + sample[elem], np.array(self.mean, dtype=np.float32)) + return sample + + def __str__(self): + return 'SubtractMeanImage' + str(self.mean) + + +class CustomScribbleInteractive(object): + def __init__(self, + scribbles, + first_frame, + dilation=9, + nocare_area=None, + bresenham=True, + use_previous_mask=False, + previous_mask_path=None): + + self.scribbles = scribbles + self.dilation = dilation + self.nocare_area = nocare_area + self.bresenham = bresenham + self.first_frame = first_frame + self.use_previous_mask = use_previous_mask + self.previous_mask_path = previous_mask_path + + def __call__(self, sample): + meta = sample['meta'] + frame_num = int(meta['frame_id']) + + im_size = meta['im_size'] + + # Initialize gt to zeros, no-care areas to ones + scr_gt = np.zeros(im_size) + scr_nocare = np.ones(im_size) + mask = np.zeros(im_size) + mask_neg = np.zeros(im_size) + + # Get all the scribbles for the current frame + for scribble in self.scribbles[frame_num]: + points_scribble = np.round( + np.array(scribble['path']) * np.array( + (im_size[1], im_size[0]))).astype(int) + if self.bresenham and len(points_scribble) > 1: + all_points = bresenham(points_scribble) + else: + all_points = points_scribble + + # Check if scribble is of same id to mark as foreground, otherwise as background + if scribble['object_id'] == meta['obj_id']: + mask[all_points[:, 1] - 1, all_points[:, 0] - 1] = 1 + else: + mask_neg[all_points[:, 1] - 1, all_points[:, 0] - 1] = 1 + if self.nocare_area is None: + nz = np.where(mask > 0) + nocare_area = int(.5 * np.sqrt( + (nz[0].max() - nz[0].min()) * (nz[1].max() - nz[1].min()))) + else: + nocare_area = 100 + + # In case we are reading the first human annotation round + if frame_num == self.first_frame: + # Compute dilated foreground, background, and no-care area + scr_gt, scr_nocare = helpers.gt_from_scribble( + mask, dilation=self.dilation, nocare_area=nocare_area) + scr_gt_neg, _ = helpers.gt_from_scribble(mask_neg, + dilation=self.dilation, + nocare_area=None) + + # Negative examples included in the training + scr_gt[scr_gt_neg > 0] = 0 + scr_nocare[scr_gt_neg > 0] = 0 + + # For annotation rounds generated by the robot + else: + # Compute dilated foreground, background, and no-care area + scr_gt_extra, _ = helpers.gt_from_scribble(mask, + dilation=self.dilation, + nocare_area=None) + scr_gt_neg, _ = helpers.gt_from_scribble(mask_neg, + dilation=self.dilation, + nocare_area=None) + + # Ignore pixels that are not foreground + if not self.use_previous_mask: + scr_nocare_extra = 1. - scr_gt_extra + else: + scr_nocare_extra = \ + (cv2.imread(os.path.join(self.previous_mask_path, meta['seq_name'], str(meta['obj_id']), + meta['frame_id'] + '.png'), 0) > 0.8 * 255).astype(np.float32) + + # Negative examples included in training + scr_gt_extra[scr_gt_neg > 0] = 0 + scr_nocare_extra[scr_gt_neg > 0] = 0 + + scr_gt = np.maximum(scr_gt, scr_gt_extra) + scr_nocare_extra[scr_gt > 0] = 0 + scr_nocare = np.minimum(scr_nocare, scr_nocare_extra) + + sample['scribble_gt'] = scr_gt + sample['scribble_void_pixels'] = scr_nocare + + return sample + + +class ToTensor(object): + """Convert ndarrays in sample to Tensors.""" + def __call__(self, sample): + + for elem in sample.keys(): + if 'meta' in elem: + continue + tmp = sample[elem] + + if tmp.ndim == 2: + tmp = tmp[:, :, np.newaxis] + else: + tmp = tmp / 255. + tmp -= (0.485, 0.456, 0.406) + tmp /= (0.229, 0.224, 0.225) + + # swap color axis because + # numpy image: H x W x C + # paddle image: C X H X W + + tmp = tmp.transpose([2, 0, 1]) + sample[elem] = paddle.to_tensor(tmp) + return sample + + +class GenerateEdge(object): + """ + """ + def __init__(self, edgesize=1): + self.edgesize = edgesize + + def __call__(self, sample): + """ + """ + if "label2" in sample: + label2 = sample['label2'] + kernel_size = 2 * self.edgesize + 1 + maskedge = np.zeros_like(label2) + + maskedge[np.where(label2[:, 1:] != label2[:, :-1])] = 1 + maskedge[np.where(label2[1:, :] != label2[:-1, :])] = 1 + maskedge = cv2.dilate( + maskedge, np.ones((kernel_size, kernel_size), dtype=np.uint8)) + sample["edge_mask"] = maskedge + else: + raise RuntimeError( + "We need parsing mask to generate the edge mask.") + return sample + + +class GenerateEdge_2(object): + """ + """ + def __init__(self, edgesize=1): + self.edgesize = edgesize + + def __call__(self, sample): + """ + """ + if "ref_frame_gt" in sample: + label2 = sample['ref_frame_gt'] + kernel_size = 2 * self.edgesize + 1 + maskedge = np.zeros_like(label2) + + maskedge[np.where(label2[:, 1:] != label2[:, :-1])] = 1 + maskedge[np.where(label2[1:, :] != label2[:-1, :])] = 1 + maskedge = cv2.dilate( + maskedge, np.ones((kernel_size, kernel_size), dtype=np.uint8)) + sample["edge_mask"] = maskedge + else: + raise RuntimeError( + "We need parsing mask to generate the edge mask.") + return sample diff --git a/applications/Ma-Net/dataloaders/davis_2017_f.py b/applications/Ma-Net/dataloaders/davis_2017_f.py new file mode 100644 index 0000000000000000000000000000000000000000..ef16a14c448620be3567362d51c946dbc85c9314 --- /dev/null +++ b/applications/Ma-Net/dataloaders/davis_2017_f.py @@ -0,0 +1,672 @@ +from __future__ import division +import json +import os +import shutil +import numpy as np +import paddle, cv2 +from random import choice +from paddle.io import Dataset +import json +from PIL import Image +from davisinteractive.utils.scribbles import scribbles2mask, annotated_frames +import sys + +sys.path.append("..") +from config import cfg +import time + + +class DAVIS2017_Test_Manager(): + def __init__(self, + split='val', + root=cfg.DATA_ROOT, + transform=None, + rgb=False, + seq_name=None): + self.split = split + self.db_root_dir = root + + self.rgb = rgb + self.transform = transform + self.seq_name = seq_name + + def get_image(self, idx): + frame_name = str(idx) + while len(frame_name) != 5: + frame_name = '0' + frame_name + imgpath = os.path.join(self.db_root_dir, 'JPEGImages/480p/', + str(self.seq_name), frame_name + '.jpg') + img = cv2.imread(imgpath) + img = np.array(img, dtype=np.float32) + sample = {'img': img} + if self.transform is not None: + sample = self.transform(sample) + return sample + + +class DAVIS2017_Feature_Extract(Dataset): + def __init__(self, + split='val', + root=cfg.DATA_ROOT, + transform=None, + rgb=False, + seq_name=None): + self.split = split + self.db_root_dir = root + + self.rgb = rgb + self.transform = transform + self.seq_name = seq_name + self.img_list = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'JPEGImages/480p/', + str(seq_name)))) + + def __len__(self): + return len(self.img_list) + + def __getitem__(self, idx): + img = self.img_list[idx] + imgpath = os.path.join(self.db_root_dir, 'JPEGImages/480p/', + str(self.seq_name), img) + current_img = cv2.imread(imgpath) + current_img = np.array(current_img, dtype=np.float32) + h, w, _ = current_img.shape + sample = {'img1': current_img} + sample['meta'] = { + 'seq_name': self.seq_name, + 'h_w': (h, w), + 'img_path': imgpath + } + if self.transform is not None: + sample = self.transform(sample) + return sample + + +class DAVIS2017_VOS_Test(Dataset): + """ + """ + def __init__(self, + split='val', + root=cfg.DATA_ROOT, + transform=None, + rgb=False, + result_root=None, + seq_name=None): + self.split = split + self.db_root_dir = root + self.result_root = result_root + self.rgb = rgb + self.transform = transform + self.seq_name = seq_name + self.seq_list_file = os.path.join( + self.db_root_dir, 'ImageSets', '2017', + '_'.join(self.split) + '_instances.txt') + + self.seqs = [] + for splt in self.split: + with open( + os.path.join(self.db_root_dir, 'ImageSets', '2017', + self.split + '.txt')) as f: + seqs_tmp = f.readlines() + seqs_tmp = list(map(lambda elem: elem.strip(), seqs_tmp)) + self.seqs.extend(seqs_tmp) + + if not self._check_preprocess(): + self._preprocess() + + assert self.seq_name in self.seq_dict.keys( + ), '{} not in {} set.'.format(self.seq_name, '_'.join(self.split)) + names_img = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'JPEGImages/480p/', + str(seq_name)))) + img_list = list( + map(lambda x: os.path.join('JPEGImages/480p/', str(seq_name), x), + names_img)) + name_label = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'Annotations/480p/', + str(seq_name)))) + labels = list( + map(lambda x: os.path.join('Annotations/480p/', str(seq_name), x), + name_label)) + + if not os.path.isfile( + os.path.join(self.result_root, seq_name, name_label[0])): + if not os.path.exists(os.path.join(self.result_root, seq_name)): + os.makedirs(os.path.join(self.result_root, seq_name)) + + shutil.copy( + os.path.join(self.db_root_dir, labels[0]), + os.path.join(self.result_root, seq_name, name_label[0])) + else: + shutil.copy( + os.path.join(self.db_root_dir, labels[0]), + os.path.join(self.result_root, seq_name, name_label[0])) + self.first_img = names_img[0] + self.first_label = name_label[0] + self.img_list = names_img[1:] + + def __len__(self): + return len(self.img_list) + + def __getitem__(self, idx): + + img = self.img_list[idx] + imgpath = os.path.join(self.db_root_dir, 'JPEGImages/480p/', + str(self.seq_name), img) + + num_frame = int(img.split('.')[0]) + ref_img = os.path.join(self.db_root_dir, 'JPEGImages/480p/', + str(self.seq_name), self.first_img) + prev_frame = num_frame - 1 + prev_frame = str(prev_frame) + while len(prev_frame) != 5: + prev_frame = '0' + prev_frame + prev_img = os.path.join(self.db_root_dir, 'JPEGImages/480p/', + str(self.seq_name), + prev_frame + '.' + img.split('.')[-1]) + + current_img = cv2.imread(imgpath) + current_img = np.array(current_img, dtype=np.float32) + + ref_img = cv2.imread(ref_img) + ref_img = np.array(ref_img, dtype=np.float32) + + prev_img = cv2.imread(prev_img) + prev_img = np.array(prev_img, dtype=np.float32) + + ref_label = os.path.join(self.db_root_dir, 'Annotations/480p/', + str(self.seq_name), self.first_label) + ref_label = Image.open(ref_label) + ref_label = np.array(ref_label, dtype=np.uint8) + + prev_label = os.path.join( + self.result_root, str(self.seq_name), + prev_frame + '.' + self.first_label.split('.')[-1]) + prev_label = Image.open(prev_label) + prev_label = np.array(prev_label, dtype=np.uint8) + + obj_num = self.seq_dict[self.seq_name][-1] + sample = { + 'ref_img': ref_img, + 'prev_img': prev_img, + 'current_img': current_img, + 'ref_label': ref_label, + 'prev_label': prev_label + } + sample['meta'] = { + 'seq_name': self.seq_name, + 'frame_num': num_frame, + 'obj_num': obj_num, + 'current_name': img + } + if self.transform is not None: + sample = self.transform(sample) + return sample + + def _check_preprocess(self): + _seq_list_file = self.seq_list_file + if not os.path.isfile(_seq_list_file): + return False + else: + self.seq_dict = json.load(open(self.seq_list_file, 'r')) + return True + + def _preprocess(self): + self.seq_dict = {} + for seq in self.seqs: + # Read object masks and get number of objects + name_label = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'Annotations/480p/', seq))) + label_path = os.path.join(self.db_root_dir, 'Annotations/480p/', + seq, name_label[0]) + _mask = np.array(Image.open(label_path)) + _mask_ids = np.unique(_mask) + n_obj = _mask_ids[-1] + + self.seq_dict[seq] = list(range(1, n_obj + 1)) + + with open(self.seq_list_file, 'w') as outfile: + outfile.write('{{\n\t"{:s}": {:s}'.format( + self.seqs[0], json.dumps(self.seq_dict[self.seqs[0]]))) + for ii in range(1, len(self.seqs)): + outfile.write(',\n\t"{:s}": {:s}'.format( + self.seqs[ii], json.dumps(self.seq_dict[self.seqs[ii]]))) + outfile.write('\n}\n') + + print('Preprocessing finished') + + +class DAVIS2017_VOS_Train(Dataset): + """DAVIS2017 dataset for training + + Return: imgs: N*2*3*H*W,label: N*2*1*H*W, seq-name: N, frame_num:N + """ + def __init__(self, + split='train', + root=cfg.DATA_ROOT, + transform=None, + rgb=False): + self.split = split + self.db_root_dir = root + self.rgb = rgb + self.transform = transform + self.seq_list_file = os.path.join( + self.db_root_dir, 'ImageSets', '2017', + '_'.join(self.split) + '_instances.txt') + self.seqs = [] + for splt in self.split: + with open( + os.path.join(self.db_root_dir, 'ImageSets', '2017', + self.split + '.txt')) as f: + seqs_tmp = f.readlines() + seqs_tmp = list(map(lambda elem: elem.strip(), seqs_tmp)) + self.seqs.extend(seqs_tmp) + self.imglistdic = {} + if not self._check_preprocess(): + self._preprocess() + self.sample_list = [] + for seq_name in self.seqs: + images = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'JPEGImages/480p/', + seq_name.strip()))) + images_path = list( + map( + lambda x: os.path.join('JPEGImages/480p/', seq_name.strip(), + x), images)) + lab = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'Annotations/480p/', + seq_name.strip()))) + lab_path = list( + map( + lambda x: os.path.join('Annotations/480p/', seq_name.strip( + ), x), lab)) + self.imglistdic[seq_name] = (images, lab) + + def __len__(self): + return len(self.seqs) + + def __getitem__(self, idx): + seqname = self.seqs[idx] + imagelist, lablist = self.imglistdic[seqname] + prev_img = np.random.choice(imagelist[:-1], 1) + prev_img = prev_img[0] + frame_num = int(prev_img.split('.')[0]) + 1 + next_frame = str(frame_num) + while len(next_frame) != 5: + next_frame = '0' + next_frame + + ###############################Processing two adjacent frames and labels + img2path = os.path.join('JPEGImages/480p/', seqname, + next_frame + '.' + prev_img.split('.')[-1]) + img2 = cv2.imread(os.path.join(self.db_root_dir, img2path)) + img2 = np.array(img2, dtype=np.float32) + + imgpath = os.path.join('JPEGImages/480p/', seqname, prev_img) + img1 = cv2.imread(os.path.join(self.db_root_dir, imgpath)) + img1 = np.array(img1, dtype=np.float32) + ############### + labelpath = os.path.join( + 'Annotations/480p/', seqname, + prev_img.split('.')[0] + '.' + lablist[0].split('.')[-1]) + label1 = Image.open(os.path.join(self.db_root_dir, labelpath)) + label2path = os.path.join('Annotations/480p/', seqname, + next_frame + '.' + lablist[0].split('.')[-1]) + label2 = Image.open(os.path.join(self.db_root_dir, label2path)) + + label1 = np.array(label1, dtype=np.uint8) + label2 = np.array(label2, dtype=np.uint8) + + ################### + ref_img = np.random.choice(imagelist, 1) + ref_img = ref_img[0] + ref_img_name = ref_img + ref_scribble_label = Image.open( + os.path.join( + self.db_root_dir, 'Annotations/480p/', seqname, + ref_img_name.split('.')[0] + '.' + lablist[0].split('.')[-1])) + ref_scribble_label = np.array(ref_scribble_label, dtype=np.uint8) + + while len(np.unique(ref_scribble_label)) < self.seq_dict[seqname][ + -1] + 1 or ref_img == prev_img or ref_img == ( + next_frame + '.' + prev_img.split('.')[-1]): + ref_img = np.random.choice(imagelist, 1) + ref_img = ref_img[0] + ref_img_name = ref_img + ref_scribble_label = Image.open( + os.path.join( + self.db_root_dir, 'Annotations/480p/', seqname, + ref_img_name.split('.')[0] + '.' + + lablist[0].split('.')[-1])) + ref_scribble_label = np.array(ref_scribble_label, dtype=np.int64) + ref_img = os.path.join('JPEGImages/480p/', seqname, ref_img) + ref_img = cv2.imread(os.path.join(self.db_root_dir, ref_img)) + ref_img = np.array(ref_img, dtype=np.float32) + #### + ################### + if self.rgb: + img1 = img1[:, :, [2, 1, 0]] + img2 = img2[:, :, [2, 1, 0]] + ref_img = ref_img[:, :, [2, 1, 0]] + obj_num = self.seq_dict[seqname][-1] + + sample = { + 'ref_img': ref_img, + 'img1': img1, + 'img2': img2, + 'ref_scribble_label': ref_scribble_label, + 'label1': label1, + 'label2': label2 + } + + sample['meta'] = { + 'seq_name': seqname, + 'frame_num': frame_num, + 'obj_num': obj_num + } + if self.transform is not None: + sample = self.transform(sample) + sample['ref_scribble_label'] = paddle.to_tensor( + sample['ref_scribble_label'], dtype='int64') + sample['label1'] = paddle.to_tensor(sample['label1'], dtype='int64') + sample['label2'] = paddle.to_tensor(sample['label2'], dtype='int64') + return sample + + ######################## + + def _check_preprocess(self): + _seq_list_file = self.seq_list_file + if not os.path.isfile(_seq_list_file): + return False + else: + self.seq_dict = json.load(open(self.seq_list_file, 'r')) + return True + + def _preprocess(self): + self.seq_dict = {} + for seq in self.seqs: + # Read object masks and get number of objects + name_label = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'Annotations/480p/', seq))) + label_path = os.path.join(self.db_root_dir, 'Annotations/480p/', + seq, name_label[0]) + _mask = np.array(Image.open(label_path)) + _mask_ids = np.unique(_mask) + n_obj = _mask_ids[-1] + + self.seq_dict[seq] = list(range(1, n_obj + 1)) + + with open(self.seq_list_file, 'w') as outfile: + outfile.write('{{\n\t"{:s}": {:s}'.format( + self.seqs[0], json.dumps(self.seq_dict[self.seqs[0]]))) + for ii in range(1, len(self.seqs)): + outfile.write(',\n\t"{:s}": {:s}'.format( + self.seqs[ii], json.dumps(self.seq_dict[self.seqs[ii]]))) + outfile.write('\n}\n') + + print('Preprocessing finished') + + +class DAVIS2017_Train(Dataset): + """DAVIS2017 dataset for training + + Return: imgs: N*2*3*H*W,label: N*2*1*H*W, seq-name: N, frame_num:N + """ + def __init__(self, + split='train', + root=cfg.DATA_ROOT, + transform=None, + rgb=False): + self.split = split + self.db_root_dir = root + self.rgb = rgb + self.transform = transform + self.seq_list_file = os.path.join( + self.db_root_dir, 'ImageSets', '2017', + '_'.join(self.split) + '_instances.txt') + self.seqs = [] + for splt in self.split: + with open( + os.path.join(self.db_root_dir, 'ImageSets', '2017', + self.split + '.txt')) as f: + seqs_tmp = f.readlines() + seqs_tmp = list(map(lambda elem: elem.strip(), seqs_tmp)) + self.seqs.extend(seqs_tmp) + + if not self._check_preprocess(): + self._preprocess() + self.sample_list = [] + for seq_name in self.seqs: + images = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'JPEGImages/480p/', + seq_name.strip()))) + images_path = list( + map( + lambda x: os.path.join('JPEGImages/480p/', seq_name.strip(), + x), images)) + lab = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'Annotations/480p/', + seq_name.strip()))) + lab_path = list( + map( + lambda x: os.path.join('Annotations/480p/', seq_name.strip( + ), x), lab)) + + for img_path, label_path in zip(images_path[:-1], lab_path[:-1]): + tmp_dic = { + 'img': img_path, + 'label': label_path, + 'seq_name': seq_name, + 'frame_num': img_path.split('/')[-1].split('.')[0] + } + self.sample_list.append(tmp_dic) + + def __len__(self): + return len(self.sample_list) + + def __getitem__(self, idx): + tmp_sample = self.sample_list[idx] + imgpath = tmp_sample['img'] + labelpath = tmp_sample['label'] + seqname = tmp_sample['seq_name'] + frame_num = int(tmp_sample['frame_num']) + 1 + + next_frame = str(frame_num) + while len(next_frame) != 5: + next_frame = '0' + next_frame + ###############################Processing two adjacent frames and labels + img2path = os.path.join('JPEGImages/480p/', seqname, + next_frame + '.' + imgpath.split('.')[-1]) + img2 = cv2.imread(os.path.join(self.db_root_dir, img2path)) + img2 = np.array(img2, dtype=np.float32) + + img1 = cv2.imread(os.path.join(self.db_root_dir, imgpath)) + img1 = np.array(img1, dtype=np.float32) + ############### + label1 = Image.open(os.path.join(self.db_root_dir, labelpath)) + label2path = os.path.join('Annotations/480p/', seqname, + next_frame + '.' + labelpath.split('.')[-1]) + label2 = Image.open(os.path.join(self.db_root_dir, label2path)) + + label1 = np.array( + label1, dtype=np.int32 + ) # fixed, uint8->int32, because layers.stack does not support uint8 + label2 = np.array( + label2, dtype=np.int32 + ) # fixed, uint8->int32, because layers.stack does not support uint8 + ################### + ref_tmp_dic = self.ref_frame_dic[seqname] + ref_img = ref_tmp_dic['ref_frame'] + ref_scribble_label = ref_tmp_dic['scribble_label'] + ref_img = cv2.imread(os.path.join(self.db_root_dir, ref_img)) + ref_img = np.array(ref_img, dtype=np.float32) + ref_frame_gt = ref_tmp_dic['ref_frame_gt'] + ref_frame_gt = Image.open(os.path.join(self.db_root_dir, ref_frame_gt)) + ref_frame_gt = np.array( + ref_frame_gt, dtype=np.int32 + ) # fixed, uint8->int32, because layers.stack does not support uint8 + ref_frame_num = ref_tmp_dic['ref_frame_num'] + + ################### + if self.rgb: + img1 = img1[:, :, [2, 1, 0]] + img2 = img2[:, :, [2, 1, 0]] + ref_img = ref_img[:, :, [2, 1, 0]] + obj_num = self.seq_dict[seqname][-1] + sample = { + 'ref_img': ref_img, + 'img1': img1, + 'img2': img2, + 'ref_scribble_label': ref_scribble_label, + 'label1': label1, + 'label2': label2, + 'ref_frame_gt': ref_frame_gt + } + if 'prev_round_label' in ref_tmp_dic: + prev_round_label = ref_tmp_dic['prev_round_label'] + prev_round_label = prev_round_label.squeeze() + prev_round_label = prev_round_label.numpy() + sample = { + 'ref_img': ref_img, + 'img1': img1, + 'img2': img2, + 'ref_scribble_label': ref_scribble_label, + 'label1': label1, + 'label2': label2, + 'ref_frame_gt': ref_frame_gt, + 'prev_round_label': prev_round_label + } + + sample['meta'] = { + 'seq_name': seqname, + 'frame_num': frame_num, + 'obj_num': obj_num, + 'ref_frame_num': ref_frame_num + } + if self.transform is not None: + sample = self.transform(sample) + + return sample + + def update_ref_frame_and_label(self, + round_scribble=None, + frame_num=None, + prev_round_label_dic=None): + ##########Update reference frame and scribbles + for seq in self.seqs: + scribble = round_scribble[seq] + if frame_num is None: + scr_frame = annotated_frames(scribble)[0] + else: + scr_frame = frame_num[seq] + scr_frame = int(scr_frame) + scr_f = str(scr_frame) + while len(scr_f) != 5: + scr_f = '0' + scr_f + ref_frame_path = os.path.join('JPEGImages/480p', seq, + scr_f + '.jpg') + ####################### + ref_frame_gt = os.path.join('Annotations/480p/', seq, + scr_f + '.png') + ######################### + ref_tmp = cv2.imread(os.path.join(self.db_root_dir, ref_frame_path)) + h_, w_ = ref_tmp.shape[:2] + scribble_masks = scribbles2mask(scribble, (h_, w_)) + if frame_num is None: + + scribble_label = scribble_masks[scr_frame] + else: + scribble_label = scribble_masks[0] + self.ref_frame_dic[seq] = { + 'ref_frame': ref_frame_path, + 'scribble_label': scribble_label, + 'ref_frame_gt': ref_frame_gt, + 'ref_frame_num': scr_frame + } + if prev_round_label_dic is not None: + self.ref_frame_dic[seq] = { + 'ref_frame': ref_frame_path, + 'scribble_label': scribble_label, + 'ref_frame_gt': ref_frame_gt, + 'ref_frame_num': scr_frame, + 'prev_round_label': prev_round_label_dic[seq] + } + + def init_ref_frame_dic(self): + self.ref_frame_dic = {} + scribbles_path = os.path.join(self.db_root_dir, 'Scribbles') + for seq in self.seqs: + selected_json = np.random.choice( + ['001.json', '002.json', '003.json'], 1) + selected_json = selected_json[0] + scribble = os.path.join(self.db_root_dir, 'Scribbles', seq, + selected_json) + with open(scribble) as f: + scribble = json.load(f) + # print(scribble) + scr_frame = annotated_frames(scribble)[0] + scr_f = str(scr_frame) + while len(scr_f) != 5: + scr_f = '0' + scr_f + + ref_frame_path = os.path.join('JPEGImages/480p', seq, + scr_f + '.jpg') + ref_tmp = cv2.imread( + os.path.join(self.db_root_dir, ref_frame_path)) + h_, w_ = ref_tmp.shape[:2] + scribble_masks = scribbles2mask(scribble, (h_, w_)) + ######################## + ref_frame_gt = os.path.join('Annotations/480p/', seq, + scr_f + '.png') + ######################## + + scribble_label = scribble_masks[scr_frame] + self.ref_frame_dic[seq] = { + 'ref_frame': ref_frame_path, + 'scribble_label': scribble_label, + 'ref_frame_gt': ref_frame_gt, + 'ref_frame_num': scr_frame + } + + ######################## + + def _check_preprocess(self): + _seq_list_file = self.seq_list_file + if not os.path.isfile(_seq_list_file): + return False + else: + self.seq_dict = json.load(open(self.seq_list_file, 'r')) + return True + + def _preprocess(self): + self.seq_dict = {} + for seq in self.seqs: + # Read object masks and get number of objects + name_label = np.sort( + os.listdir( + os.path.join(self.db_root_dir, 'Annotations/480p/', seq))) + label_path = os.path.join(self.db_root_dir, 'Annotations/480p/', + seq, name_label[0]) + _mask = np.array(Image.open(label_path)) + _mask_ids = np.unique(_mask) + n_obj = _mask_ids[-1] + + self.seq_dict[seq] = list(range(1, n_obj + 1)) + + with open(self.seq_list_file, 'w') as outfile: + outfile.write('{{\n\t"{:s}": {:s}'.format( + self.seqs[0], json.dumps(self.seq_dict[self.seqs[0]]))) + for ii in range(1, len(self.seqs)): + outfile.write(',\n\t"{:s}": {:s}'.format( + self.seqs[ii], json.dumps(self.seq_dict[self.seqs[ii]]))) + outfile.write('\n}\n') + + print('Preprocessing finished') diff --git a/applications/Ma-Net/dataloaders/helpers.py b/applications/Ma-Net/dataloaders/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..2bef5a84f7f86297524c3fc2fa8485e3cf4e2db3 --- /dev/null +++ b/applications/Ma-Net/dataloaders/helpers.py @@ -0,0 +1,81 @@ +import numpy as np +import cv2 + + +def tens2image(im): + tmp = np.squeeze(im.numpy()) + if tmp.ndim == 2: + return tmp + else: + return tmp.transpose((1, 2, 0)) + + +def overlay_mask(im, ma, color=np.array([255, 0, 0]) / 255.0): + assert np.max(im) <= 1.0 + + ma = ma.astype(np.bool) + im = im.astype(np.float32) + + alpha = 0.5 + + fg = im * alpha + np.ones( + im.shape) * (1 - alpha) * color # np.array([0,0,255])/255.0 + + # Whiten background + alpha = 1.0 + bg = im.copy() + bg[ma == 0] = im[ma == 0] * alpha + np.ones(im[ma == 0].shape) * (1 - alpha) + bg[ma == 1] = fg[ma == 1] + + # [-2:] is s trick to be compatible both with opencv 2 and 3 + contours = cv2.findContours(ma.copy().astype(np.uint8), cv2.RETR_TREE, + cv2.CHAIN_APPROX_SIMPLE)[-2:] + cv2.drawContours(bg, contours[0], -1, (0.0, 0.0, 0.0), 1) + + return bg + + +def im_normalize(im): + """ + Normalize image + """ + imn = (im - im.min()) / max((im.max() - im.min()), 1e-8) + return imn + + +def construct_name(p, prefix): + """ + Construct the name of the model + p: dictionary of parameters + prefix: the prefix + name: the name of the model - manually add ".pth" to follow the convention + """ + name = prefix + for key in p.keys(): + if (type(p[key]) != tuple) and (type(p[key]) != list): + name = name + '_' + str(key) + '-' + str(p[key]) + else: + name = name + '_' + str(key) + '-' + str(p[key][0]) + return name + + +def gt_from_scribble(scr, dilation=11, nocare_area=21): + + # Compute foreground + if scr.max() == 1: + kernel_fg = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, + (dilation, dilation)) + fg = cv2.dilate(scr.astype(np.uint8), + kernel=kernel_fg).astype(scr.dtype) + else: + fg = scr + + # Compute nocare area + if nocare_area is None: + nocare = None + else: + kernel_nc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, + (nocare_area, nocare_area)) + nocare = cv2.dilate(fg, kernel=kernel_nc) - fg + + return fg, nocare diff --git a/applications/Ma-Net/dataloaders/samplers.py b/applications/Ma-Net/dataloaders/samplers.py new file mode 100644 index 0000000000000000000000000000000000000000..c50260b336ae188d521e35edda0e5283faa0e9cf --- /dev/null +++ b/applications/Ma-Net/dataloaders/samplers.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import +from collections import defaultdict +import numpy as np + +import paddle +from paddle.io import Sampler + + +class RandomIdentitySampler(Sampler): + """ + Randomly sample N identities, then for each identity, + randomly sample K instances, therefore batch size is N*K. + + Code imported from https://github.com/Cysu/open-reid/blob/master/reid/utils/data/sampler.py. + + Args: + data_source (Dataset): dataset to sample from. + num_instances (int): number of instances per identity. + """ + def __init__(self, sample_list, num_instances=1): + self.sample_list = sample_list + self.num_instances = num_instances + self.index_dic = defaultdict(list) + for index, tmp_dic in enumerate(self.sample_list): + pid = tmp_dic['seq_name'] + self.index_dic[pid].append(index) + self.pids = list(self.index_dic.keys()) + self.num_identities = len(self.pids) + + def __iter__(self): + indices = np.random.permutation(self.num_identities) + ret = [] + for i in indices: + pid = self.pids[i] + t = self.index_dic[pid] + replace = False if len(t) >= self.num_instances else True + t = np.random.choice(t, size=self.num_instances, replace=replace) + ret.extend(t) + return iter(ret) + + def __len__(self): + return self.num_identities * self.num_instances diff --git a/applications/Ma-Net/images/1836-teaser.gif b/applications/Ma-Net/images/1836-teaser.gif new file mode 100644 index 0000000000000000000000000000000000000000..aeda7533df644922ef22016063e63a5b39e2f22e Binary files /dev/null and b/applications/Ma-Net/images/1836-teaser.gif differ diff --git a/applications/Ma-Net/networks/IntVOS.py b/applications/Ma-Net/networks/IntVOS.py new file mode 100644 index 0000000000000000000000000000000000000000..aa526b65ba8720cd0829b59985787aa4e788c4b2 --- /dev/null +++ b/applications/Ma-Net/networks/IntVOS.py @@ -0,0 +1,927 @@ +import os +import numpy as np +import paddle +import paddle.nn as nn +import sys + +sys.path.append("..") + +from config import cfg +import time +import paddle.nn.functional as F +from utils.api import int_, float_, long_ +from utils.api import kaiming_normal_ + +#############################################################GLOBAL_DIST_MAP +MODEL_UNFOLD = True +WRONG_LABEL_PADDING_DISTANCE = 1e20 + + +def _pairwise_distances(x, y, ys=None): + """Computes pairwise squared l2 distances between tensors x and y. + Args: + x: Tensor of shape [n, feature_dim]. + y: Tensor of shape [m, feature_dim]. + Returns: + Float32 distances tensor of shape [n, m]. + """ + + xs = paddle.sum(x * x, 1) + xs = xs.unsqueeze(1) + if ys is None: + ys = paddle.sum(y * y, 1) + ys = ys.unsqueeze(0) + else: + ys = ys + d = xs + ys - 2. * paddle.matmul(x, paddle.t(y)) + return d, ys + + +################## +def _flattened_pairwise_distances(reference_embeddings, query_embeddings, ys): + """Calculates flattened tensor of pairwise distances between ref and query. + Args: + reference_embeddings: Tensor of shape [..., embedding_dim], + the embedding vectors for the reference frame + query_embeddings: Tensor of shape [n_query_images, height, width, + embedding_dim], the embedding vectors for the query frames. + Returns: + A distance tensor of shape [reference_embeddings.size / embedding_dim, + query_embeddings.size / embedding_dim] + """ + embedding_dim = query_embeddings.shape[-1] + reference_embeddings = reference_embeddings.reshape([-1, embedding_dim]) + first_dim = -1 + query_embeddings = query_embeddings.reshape([first_dim, embedding_dim]) + dists, ys = _pairwise_distances(query_embeddings, reference_embeddings, ys) + return dists, ys + + +def _nn_features_per_object_for_chunk(reference_embeddings, query_embeddings, + wrong_label_mask, k_nearest_neighbors, + ys): + """Extracts features for each object using nearest neighbor attention. + Args: + reference_embeddings: Tensor of shape [n_chunk, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings: Tensor of shape [m_chunk, embedding_dim], the embedding + vectors for the query frames. + wrong_label_mask: + k_nearest_neighbors: Integer, the number of nearest neighbors to use. + Returns: + nn_features: A float32 tensor of nearest neighbor features of shape + [m_chunk, n_objects, feature_dim]. + """ + # reference_embeddings_key = reference_embeddings + # query_embeddings_key = query_embeddings + dists, ys = _flattened_pairwise_distances(reference_embeddings, + query_embeddings, ys) + + dists = (paddle.unsqueeze(dists, 1) + + paddle.unsqueeze(float_(wrong_label_mask), 0) * + WRONG_LABEL_PADDING_DISTANCE) + if k_nearest_neighbors == 1: + features = paddle.min(dists, 2, keepdim=True) + else: + dists, _ = paddle.topk(-dists, k=k_nearest_neighbors, axis=2) + dists = -dists + valid_mask = (dists < WRONG_LABEL_PADDING_DISTANCE) + masked_dists = dists * valid_mask.float() + pad_dist = paddle.max(masked_dists, axis=2, keepdim=True)[0].tile( + (1, 1, masked_dists.shape[-1])) + dists = paddle.where(valid_mask, dists, pad_dist) + # take mean of distances + features = paddle.mean(dists, axis=2, keepdim=True) + + return features, ys + + +### +def _selected_pixel(ref_labels_flat, ref_emb_flat): + index_list = paddle.arange(len(ref_labels_flat)) + index_list = index_list + index_ = paddle.masked_select(index_list, ref_labels_flat != -1) + + index_ = long_(index_) + ref_labels_flat = paddle.index_select(ref_labels_flat, index_, 0) + ref_emb_flat = paddle.index_select(ref_emb_flat, index_, 0) + + return ref_labels_flat, ref_emb_flat + + +### + + +def _nearest_neighbor_features_per_object_in_chunks( + reference_embeddings_flat, query_embeddings_flat, reference_labels_flat, + ref_obj_ids, k_nearest_neighbors, n_chunks): + """Calculates the nearest neighbor features per object in chunks to save mem. + Uses chunking to bound the memory use. + Args: + reference_embeddings_flat: Tensor of shape [n, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings_flat: Tensor of shape [m, embedding_dim], the embedding + vectors for the query frames. + reference_labels_flat: Tensor of shape [n], the class labels of the + reference frame. + ref_obj_ids: int tensor of unique object ids in the reference labels. + k_nearest_neighbors: Integer, the number of nearest neighbors to use. + n_chunks: Integer, the number of chunks to use to save memory + (set to 1 for no chunking). + Returns: + nn_features: A float32 tensor of nearest neighbor features of shape + [m, n_objects, feature_dim]. + """ + + chunk_size = int_( + np.ceil((float_(query_embeddings_flat.shape[0]) / n_chunks).numpy())) + if cfg.TEST_MODE: + reference_labels_flat, reference_embeddings_flat = _selected_pixel( + reference_labels_flat, reference_embeddings_flat) + wrong_label_mask = (reference_labels_flat != paddle.unsqueeze( + ref_obj_ids, 1)) + all_features = [] + for n in range(n_chunks): + if n == 0: + ys = None + if n_chunks == 1: + query_embeddings_flat_chunk = query_embeddings_flat + else: + chunk_start = n * chunk_size + chunk_end = (n + 1) * chunk_size + query_embeddings_flat_chunk = query_embeddings_flat[ + chunk_start:chunk_end] + features, ys = _nn_features_per_object_for_chunk( + reference_embeddings_flat, query_embeddings_flat_chunk, + wrong_label_mask, k_nearest_neighbors, ys) + all_features.append(features) + if n_chunks == 1: + nn_features = all_features[0] + else: + nn_features = paddle.concat(all_features, axis=0) + return nn_features + + +def nearest_neighbor_features_per_object(reference_embeddings, + query_embeddings, + reference_labels, + k_nearest_neighbors, + gt_ids=None, + n_chunks=100): + """Calculates the distance to the nearest neighbor per object. + For every pixel of query_embeddings calculate the distance to the + nearest neighbor in the (possibly subsampled) reference_embeddings per object. + Args: + reference_embeddings: Tensor of shape [height, width, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings: Tensor of shape [n_query_images, height, width, + embedding_dim], the embedding vectors for the query frames. + reference_labels: Tensor of shape [height, width, 1], the class labels of + the reference frame. + max_neighbors_per_object: Integer, the maximum number of candidates + for the nearest neighbor query per object after subsampling, + or 0 for no subsampling. + k_nearest_neighbors: Integer, the number of nearest neighbors to use. + gt_ids: Int tensor of shape [n_objs] of the sorted unique ground truth + ids in the first frame. If None, it will be derived from + reference_labels. + n_chunks: Integer, the number of chunks to use to save memory + (set to 1 for no chunking). + Returns: + nn_features: A float32 tensor of nearest neighbor features of shape + [n_query_images, height, width, n_objects, feature_dim]. + gt_ids: An int32 tensor of the unique sorted object ids present + in the reference labels. + """ + + assert (reference_embeddings.shape[:2] == reference_labels.shape[:2]) + h, w, _ = query_embeddings.shape + reference_labels_flat = reference_labels.reshape([-1]) + if gt_ids is None: + ref_obj_ids = paddle.unique(reference_labels_flat)[-1] + ref_obj_ids = np.arange(0, ref_obj_ids + 1) + gt_ids = paddle.to_tensor(ref_obj_ids) + gt_ids = int_(gt_ids) + else: + gt_ids = int_(paddle.arange(0, gt_ids + 1)) + + embedding_dim = query_embeddings.shape[-1] + query_embeddings_flat = query_embeddings.reshape([-1, embedding_dim]) + reference_embeddings_flat = reference_embeddings.reshape( + [-1, embedding_dim]) + nn_features = _nearest_neighbor_features_per_object_in_chunks( + reference_embeddings_flat, query_embeddings_flat, reference_labels_flat, + gt_ids, k_nearest_neighbors, n_chunks) + nn_features_dim = nn_features.shape[-1] + nn_features = nn_features.reshape( + [1, h, w, gt_ids.shape[0], nn_features_dim]) + return nn_features.cuda(), gt_ids + + +########################################################################LOCAL_DIST_MAP +def local_pairwise_distances(x, y, max_distance=9): + """Computes pairwise squared l2 distances using a local search window. + Optimized implementation using correlation_cost. + Args: + x: Float32 tensor of shape [height, width, feature_dim]. + y: Float32 tensor of shape [height, width, feature_dim]. + max_distance: Integer, the maximum distance in pixel coordinates + per dimension which is considered to be in the search window. + Returns: + Float32 distances tensor of shape + [height, width, (2 * max_distance + 1) ** 2]. + + """ + if cfg.MODEL_LOCAL_DOWNSAMPLE: + ##### + ori_h, ori_w, _ = x.shape + x = x.transpose([2, 0, 1]).unsqueeze(0) + + x = F.avg_pool2d(x, (2, 2), (2, 2)) + y = y.transpose([2, 0, 1]).unsqueeze(0) + y = F.avg_pool2d(y, (2, 2), (2, 2)) + + x = x.squeeze(0).transpose([1, 2, 0]) + y = y.squeeze(0).transpose([1, 2, 0]) + corr = cross_correlate(x, y, max_distance=max_distance) + xs = paddle.sum(x * x, 2, keepdim=True) + + ys = paddle.sum(y * y, 2, keepdim=True) + ones_ys = paddle.ones_like(ys) + ys = cross_correlate(ones_ys, ys, max_distance=max_distance) + d = xs + ys - 2 * corr + # Boundary should be set to Inf. + tmp = paddle.zeros_like(d) + boundary = paddle.equal( + cross_correlate(ones_ys, ones_ys, max_distance=max_distance), 0) + d = paddle.where(boundary, tmp.fill_(float_('inf')), d) + d = (paddle.nn.functional.sigmoid(d) - 0.5) * 2 + d = d.transpose([2, 0, 1]).unsqueeze(0) + d = F.interpolate(d, + size=(ori_h, ori_w), + mode='bilinear', + align_corners=True) + d = d.squeeze(0).transpose([1, 2, 0]) + else: + corr = cross_correlate(x, y, max_distance=max_distance) + xs = paddle.sum(x * x, 2, keepdim=True) + + ys = paddle.sum(y * y, 2, keepdim=True) + ones_ys = paddle.ones_like(ys) + ys = cross_correlate(ones_ys, ys, max_distance=max_distance) + d = xs + ys - 2 * corr + # Boundary should be set to Inf. + tmp = paddle.zeros_like(d) + boundary = paddle.equal( + cross_correlate(ones_ys, ones_ys, max_distance=max_distance), 0) + d = paddle.where(boundary, tmp.fill_(float_('inf')), d) + return d + + +def local_pairwise_distances2(x, y, max_distance=9): + """Computes pairwise squared l2 distances using a local search window. + Naive implementation using map_fn. + Used as a slow fallback for when correlation_cost is not available. + Args: + x: Float32 tensor of shape [height, width, feature_dim]. + y: Float32 tensor of shape [height, width, feature_dim]. + max_distance: Integer, the maximum distance in pixel coordinates + per dimension which is considered to be in the search window. + Returns: + Float32 distances tensor of shape + [height, width, (2 * max_distance + 1) ** 2]. + """ + if cfg.MODEL_LOCAL_DOWNSAMPLE: + ori_h, ori_w, _ = x.shape + x = paddle.transpose(x, [2, 0, 1]).unsqueeze(0) + x = F.avg_pool2d(x, (2, 2), (2, 2)) + y = paddle.transpose(y, [2, 0, 1]).unsqueeze(0) + y = F.avg_pool2d(y, (2, 2), (2, 2)) + + _, channels, height, width = x.shape + padding_val = 1e20 + padded_y = F.pad( + y, (max_distance, max_distance, max_distance, max_distance), + mode='constant', + value=padding_val) + offset_y = F.unfold(padded_y, kernel_sizes=[height, width]).reshape( + [1, channels, height, width, -1]) + x = x.reshape([1, channels, height, width, 1]) + minus = x - offset_y + dists = paddle.sum(paddle.multiply(minus, minus), + axis=1).reshape([1, height, width, + -1]).transpose([0, 3, 1, 2]) + dists = (paddle.nn.functional.sigmoid(dists) - 0.5) * 2 + dists = F.interpolate(dists, + size=[ori_h, ori_w], + mode='bilinear', + align_corners=True) + dists = dists.squeeze(0).transpose([1, 2, 0]) + + else: + padding_val = 1e20 + padded_y = nn.functional.pad( + y, (0, 0, max_distance, max_distance, max_distance, max_distance), + mode='constant', + value=padding_val) + height, width, _ = x.shape + dists = [] + for y_start in range(2 * max_distance + 1): + y_end = y_start + height + y_slice = padded_y[y_start:y_end] + for x_start in range(2 * max_distance + 1): + x_end = x_start + width + offset_y = y_slice[:, x_start:x_end] + dist = paddle.sum(paddle.pow((x - offset_y), 2), dim=2) + dists.append(dist) + dists = paddle.stack(dists, dim=2) + + return dists + + +class SpatialCorrelationSampler: + pass + + +def cross_correlate(x, y, max_distance=9): + """Efficiently computes the cross correlation of x and y. + Optimized implementation using correlation_cost. + Note that we do not normalize by the feature dimension. + Args: + x: Float32 tensor of shape [height, width, feature_dim]. + y: Float32 tensor of shape [height, width, feature_dim]. + max_distance: Integer, the maximum distance in pixel coordinates + per dimension which is considered to be in the search window. + Returns: + Float32 tensor of shape [height, width, (2 * max_distance + 1) ** 2]. + """ + corr_op = SpatialCorrelationSampler(kernel_size=1, + patch_size=2 * max_distance + 1, + stride=1, + dilation_patch=1, + padding=0) + + xs = x.transpose(2, 0, 1) + xs = paddle.unsqueeze(xs, 0) + ys = y.transpose(2, 0, 1) + ys = paddle.unsqueeze(ys, 0) + corr = corr_op(xs, ys) + bs, _, _, hh, ww = corr.shape + corr = corr.reshape([bs, -1, hh, ww]) + corr = paddle.squeeze(corr, 0) + corr = corr.transpose(1, 2, 0) + return corr + + +def local_previous_frame_nearest_neighbor_features_per_object( + prev_frame_embedding, + query_embedding, + prev_frame_labels, + gt_ids, + max_distance=12): + """Computes nearest neighbor features while only allowing local matches. + Args: + prev_frame_embedding: Tensor of shape [height, width, embedding_dim], + the embedding vectors for the last frame. + query_embedding: Tensor of shape [height, width, embedding_dim], + the embedding vectors for the query frames. + prev_frame_labels: Tensor of shape [height, width, 1], the class labels of + the previous frame. + gt_ids: Int Tensor of shape [n_objs] of the sorted unique ground truth + ids in the first frame. + max_distance: Integer, the maximum distance allowed for local matching. + Returns: + nn_features: A float32 np.array of nearest neighbor features of shape + [1, height, width, n_objects, 1]. + """ + + d = local_pairwise_distances2(query_embedding, + prev_frame_embedding, + max_distance=max_distance) + height, width = prev_frame_embedding.shape[:2] + + if MODEL_UNFOLD: + + labels = float_(prev_frame_labels).transpose([2, 0, 1]).unsqueeze(0) + padded_labels = F.pad(labels, ( + 2 * max_distance, + 2 * max_distance, + 2 * max_distance, + 2 * max_distance, + )) + offset_labels = F.unfold(padded_labels, + kernel_sizes=[height, width], + strides=[2, 2]).reshape([height, width, -1, 1]) + offset_masks = paddle.equal( + offset_labels, + float_(gt_ids).unsqueeze(0).unsqueeze(0).unsqueeze(0)) + else: + + masks = paddle.equal(prev_frame_labels, + gt_ids.unsqueeze(0).unsqueeze(0)) + padded_masks = nn.functional.pad(masks, ( + 0, + 0, + max_distance, + max_distance, + max_distance, + max_distance, + )) + offset_masks = [] + for y_start in range(2 * max_distance + 1): + y_end = y_start + height + masks_slice = padded_masks[y_start:y_end] + for x_start in range(2 * max_distance + 1): + x_end = x_start + width + offset_mask = masks_slice[:, x_start:x_end] + offset_masks.append(offset_mask) + offset_masks = paddle.stack(offset_masks, axis=2) + + d_tiled = d.unsqueeze(-1).tile((1, 1, 1, gt_ids.shape[0])) + pad = paddle.ones_like(d_tiled) + d_masked = paddle.where(offset_masks, d_tiled, pad) + dists = paddle.min(d_masked, axis=2) + dists = dists.reshape([1, height, width, gt_ids.shape[0], 1]) + + return dists + + +############################################################## + + +################# +class _res_block(nn.Layer): + def __init__(self, in_dim, out_dim): + super(_res_block, self).__init__() + self.conv1 = nn.Conv2D(in_dim, + out_dim, + kernel_size=3, + stride=1, + padding=1) + self.relu1 = nn.ReLU() + self.bn1 = paddle.nn.BatchNorm2D(out_dim, momentum=cfg.TRAIN_BN_MOM) + self.conv2 = nn.Conv2D(out_dim, + out_dim, + kernel_size=3, + stride=1, + padding=1) + self.relu2 = nn.ReLU() + self.bn2 = paddle.nn.BatchNorm2D(out_dim, momentum=cfg.TRAIN_BN_MOM) + + def forward(self, x): + res = x + x = self.conv1(x) + x = self.bn1(x) + x = self.relu1(x) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu2(x) + x += res + return x + + +#################### +class IntSegHead(nn.Layer): + def __init__(self, + in_dim=(cfg.MODEL_SEMANTIC_EMBEDDING_DIM + 3), + emb_dim=cfg.MODEL_HEAD_EMBEDDING_DIM): + super(IntSegHead, self).__init__() + self.conv1 = nn.Conv2D(in_dim, + emb_dim, + kernel_size=7, + stride=1, + padding=3) + self.bn1 = paddle.nn.BatchNorm2D(emb_dim, momentum=cfg.TRAIN_BN_MOM) + self.relu1 = nn.ReLU(True) + self.res1 = _res_block(emb_dim, emb_dim) + self.res2 = _res_block(emb_dim, emb_dim) + self.conv2 = nn.Conv2D(256, emb_dim, kernel_size=3, stride=1, padding=1) + self.bn2 = paddle.nn.BatchNorm2D(emb_dim, momentum=cfg.TRAIN_BN_MOM) + self.relu2 = nn.ReLU(True) + self.conv3 = nn.Conv2D(emb_dim, 1, 1, 1) + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu1(x) + x = self.res1(x) + x = self.res2(x) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu2(x) + x = self.conv3(x) + return x + + +class _split_separable_conv2d(nn.Layer): + def __init__(self, in_dim, out_dim, kernel_size=7): + super(_split_separable_conv2d, self).__init__() + self.conv1 = nn.Conv2D(in_dim, + in_dim, + kernel_size=kernel_size, + stride=1, + padding=int((kernel_size - 1) / 2), + groups=in_dim) + self.relu1 = nn.ReLU(True) + self.bn1 = paddle.nn.BatchNorm2D(in_dim, momentum=cfg.TRAIN_BN_MOM) + self.conv2 = nn.Conv2D(in_dim, out_dim, kernel_size=1, stride=1) + self.relu2 = nn.ReLU(True) + self.bn2 = paddle.nn.BatchNorm2D(out_dim, momentum=cfg.TRAIN_BN_MOM) + kaiming_normal_(self.conv1.weight, mode='fan_out', nonlinearity='relu') + kaiming_normal_(self.conv2.weight, mode='fan_out', nonlinearity='relu') + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu1(x) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu2(x) + return x + + +class DynamicSegHead(nn.Layer): + def __init__(self, + in_dim=(cfg.MODEL_SEMANTIC_EMBEDDING_DIM + 3), + embed_dim=cfg.MODEL_HEAD_EMBEDDING_DIM, + kernel_size=1): + super(DynamicSegHead, self).__init__() + self.layer1 = _split_separable_conv2d(in_dim, embed_dim) + self.layer2 = _split_separable_conv2d(embed_dim, embed_dim) + self.layer3 = _split_separable_conv2d(embed_dim, embed_dim) + self.layer4 = _split_separable_conv2d(embed_dim, embed_dim) + self.conv = nn.Conv2D(embed_dim, 1, 1, 1) + kaiming_normal_(self.conv.weight, mode='fan_out', nonlinearity='relu') + + def forward(self, x): + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + x = self.conv(x) + return x + + +################## + + +############### +class IntVOS(nn.Layer): + def __init__(self, cfg, feature_extracter): + super(IntVOS, self).__init__() + self.feature_extracter = feature_extracter ##embedding extractor + self.feature_extracter.cls_conv = nn.Sequential() + self.feature_extracter.upsample4 = nn.Sequential() + self.semantic_embedding = None + self.seperate_conv = nn.Conv2D(cfg.MODEL_ASPP_OUTDIM, + cfg.MODEL_ASPP_OUTDIM, + kernel_size=3, + stride=1, + padding=1, + groups=cfg.MODEL_ASPP_OUTDIM) + self.bn1 = paddle.nn.BatchNorm2D(cfg.MODEL_ASPP_OUTDIM, + momentum=cfg.TRAIN_BN_MOM) + self.relu1 = nn.ReLU(True) + self.embedding_conv = nn.Conv2D(cfg.MODEL_ASPP_OUTDIM, + cfg.MODEL_SEMANTIC_EMBEDDING_DIM, 1, 1) + self.relu2 = nn.ReLU(True) + self.bn2 = paddle.nn.BatchNorm2D(cfg.MODEL_SEMANTIC_EMBEDDING_DIM, + momentum=cfg.TRAIN_BN_MOM) + self.semantic_embedding = nn.Sequential(*[ + self.seperate_conv, self.bn1, self.relu1, self.embedding_conv, + self.bn2, self.relu2 + ]) + + for m in self.semantic_embedding: + if isinstance(m, nn.Conv2D): + kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') + + self.dynamic_seghead = DynamicSegHead() # propagation segm head + if cfg.MODEL_USEIntSeg: + self.inter_seghead = IntSegHead( + in_dim=cfg.MODEL_SEMANTIC_EMBEDDING_DIM + 3) + else: + self.inter_seghead = DynamicSegHead( + in_dim=cfg.MODEL_SEMANTIC_EMBEDDING_DIM + + 2) # interaction segm head + + def forward(self, + x=None, + ref_scribble_label=None, + previous_frame_mask=None, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=None, + gt_ids=None, + k_nearest_neighbors=1, + global_map_tmp_dic=None, + local_map_dics=None, + interaction_num=None, + start_annotated_frame=None, + frame_num=None): + + x = self.extract_feature(x) + # print('extract_feature:', x.mean().item()) + ref_frame_embedding, previous_frame_embedding, current_frame_embedding = paddle.split( + x, num_or_sections=3, axis=0) + + if global_map_tmp_dic is None: + dic = self.prop_seghead( + ref_frame_embedding, previous_frame_embedding, + current_frame_embedding, ref_scribble_label, + previous_frame_mask, normalize_nearest_neighbor_distances, + use_local_map, seq_names, gt_ids, k_nearest_neighbors, + global_map_tmp_dic, local_map_dics, interaction_num, + start_annotated_frame, frame_num, self.dynamic_seghead) + return dic + + else: + dic, global_map_tmp_dic = self.prop_seghead( + ref_frame_embedding, previous_frame_embedding, + current_frame_embedding, ref_scribble_label, + previous_frame_mask, normalize_nearest_neighbor_distances, + use_local_map, seq_names, gt_ids, k_nearest_neighbors, + global_map_tmp_dic, local_map_dics, interaction_num, + start_annotated_frame, frame_num, self.dynamic_seghead) + return dic, global_map_tmp_dic + + def extract_feature(self, x): + x = self.feature_extracter(x) + x = self.semantic_embedding(x) + return x + + def prop_seghead(self, + ref_frame_embedding=None, + previous_frame_embedding=None, + current_frame_embedding=None, + ref_scribble_label=None, + previous_frame_mask=None, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=None, + gt_ids=None, + k_nearest_neighbors=1, + global_map_tmp_dic=None, + local_map_dics=None, + interaction_num=None, + start_annotated_frame=None, + frame_num=None, + dynamic_seghead=None): + """return: feature_embedding,global_match_map,local_match_map,previous_frame_mask""" + ############### + + global_map_tmp_dic = global_map_tmp_dic + dic_tmp = {} + bs, c, h, w = current_frame_embedding.shape + if cfg.TEST_MODE: + scale_ref_scribble_label = float_(ref_scribble_label) + else: + scale_ref_scribble_label = paddle.nn.functional.interpolate( + float_(ref_scribble_label), size=(h, w), mode='nearest') + scale_ref_scribble_label = int_(scale_ref_scribble_label) + scale_previous_frame_label = paddle.nn.functional.interpolate( + float_(previous_frame_mask), size=(h, w), mode='nearest') + # print(scale_previous_frame_label.sum()) # xx + # print(previous_frame_mask.sum().item()) # xx + scale_previous_frame_label = int_(scale_previous_frame_label) + # print(scale_previous_frame_label.sum().item()) # xx + for n in range(bs): + seq_current_frame_embedding = current_frame_embedding[n] + seq_ref_frame_embedding = ref_frame_embedding[n] + seq_prev_frame_embedding = previous_frame_embedding[n] + + seq_ref_frame_embedding = seq_ref_frame_embedding.transpose( + [1, 2, 0]) + seq_current_frame_embedding = seq_current_frame_embedding.transpose( + [1, 2, 0]) + seq_ref_scribble_label = scale_ref_scribble_label[n].transpose( + [1, 2, 0]) + #########Global Map + nn_features_n, ref_obj_ids = nearest_neighbor_features_per_object( + reference_embeddings=seq_ref_frame_embedding, + query_embeddings=seq_current_frame_embedding, + reference_labels=seq_ref_scribble_label, + k_nearest_neighbors=k_nearest_neighbors, + gt_ids=gt_ids[n], + n_chunks=10) + if normalize_nearest_neighbor_distances: + nn_features_n = (paddle.nn.functional.sigmoid(nn_features_n) - + 0.5) * 2 + + if global_map_tmp_dic is not None: ###when testing, use global map memory + if seq_names[n] not in global_map_tmp_dic: + global_map_tmp_dic[seq_names[n]] = paddle.ones_like( + nn_features_n).tile([104, 1, 1, 1, 1]) + nn_features_n = paddle.where( + nn_features_n <= + global_map_tmp_dic[seq_names[n]][frame_num[n]].unsqueeze(0), + nn_features_n, + global_map_tmp_dic[seq_names[n]][frame_num[n]].unsqueeze(0)) + + global_map_tmp_dic[seq_names[n]][ + frame_num[n]] = nn_features_n.detach()[0] + + #########################Local dist map + seq_prev_frame_embedding = seq_prev_frame_embedding.transpose( + [1, 2, 0]) + seq_previous_frame_label = scale_previous_frame_label[n].transpose( + [1, 2, 0]) + + if use_local_map: + prev_frame_nn_features_n = local_previous_frame_nearest_neighbor_features_per_object( + prev_frame_embedding=seq_prev_frame_embedding, + query_embedding=seq_current_frame_embedding, + prev_frame_labels=seq_previous_frame_label, + gt_ids=ref_obj_ids, + max_distance=cfg.MODEL_MAX_LOCAL_DISTANCE) + else: + prev_frame_nn_features_n, _ = nearest_neighbor_features_per_object( + reference_embeddings=seq_prev_frame_embedding, + query_embeddings=seq_current_frame_embedding, + reference_labels=seq_previous_frame_label, + k_nearest_neighbors=k_nearest_neighbors, + gt_ids=gt_ids[n], + n_chunks=20) + prev_frame_nn_features_n = ( + paddle.nn.functional.sigmoid(prev_frame_nn_features_n) - + 0.5) * 2 + + +# print(prev_frame_nn_features_n.mean().item(), prev_frame_nn_features_n.shape, interaction_num) # o +############# + if local_map_dics is not None: ##When testing, use local map memory + local_map_tmp_dic, local_map_dist_dic = local_map_dics + if seq_names[n] not in local_map_dist_dic: + print(seq_names[n], 'not in local_map_dist_dic') + local_map_dist_dic[seq_names[n]] = paddle.zeros(104, 9) + if seq_names[n] not in local_map_tmp_dic: + print(seq_names[n], 'not in local_map_tmp_dic') + local_map_tmp_dic[seq_names[n]] = paddle.zeros_like( + prev_frame_nn_features_n).unsqueeze(0).tile( + [104, 9, 1, 1, 1, 1]) + local_map_dist_dic[seq_names[n]][ + frame_num[n], interaction_num - + 1] = 1.0 / (abs(frame_num[n] - start_annotated_frame) + ) # bugs fixed. + local_map_tmp_dic[seq_names[n]][ + frame_num[n], + interaction_num - 1] = prev_frame_nn_features_n.squeeze( + 0).detach() # bugs fixed. + if interaction_num == 1: + prev_frame_nn_features_n = local_map_tmp_dic[seq_names[n]][ + frame_num[n]][interaction_num - 1] + prev_frame_nn_features_n = prev_frame_nn_features_n.unsqueeze( + 0) + else: + if local_map_dist_dic[seq_names[n]][frame_num[n]][interaction_num - 1] > \ + local_map_dist_dic[seq_names[n]][frame_num[n]][interaction_num - 2]: + prev_frame_nn_features_n = local_map_tmp_dic[ + seq_names[n]][frame_num[n]][interaction_num - 1] + prev_frame_nn_features_n = prev_frame_nn_features_n.unsqueeze( + 0) + else: + prev_frame_nn_features_n = local_map_tmp_dic[ + seq_names[n]][frame_num[n]][interaction_num - 2] + prev_frame_nn_features_n = prev_frame_nn_features_n.unsqueeze( + 0) + + local_map_dics = (local_map_tmp_dic, local_map_dist_dic) + + to_cat_previous_frame = ( + float_(seq_previous_frame_label) == float_(ref_obj_ids) + ) # float comparision? + + to_cat_current_frame_embedding = current_frame_embedding[ + n].unsqueeze(0).tile((ref_obj_ids.shape[0], 1, 1, 1)) + + to_cat_nn_feature_n = nn_features_n.squeeze(0).transpose( + [2, 3, 0, 1]) + to_cat_previous_frame = float_( + to_cat_previous_frame.unsqueeze(-1).transpose([2, 3, 0, 1])) + to_cat_prev_frame_nn_feature_n = prev_frame_nn_features_n.squeeze( + 0).transpose([2, 3, 0, 1]) + to_cat = paddle.concat( + (to_cat_current_frame_embedding, to_cat_nn_feature_n, + to_cat_prev_frame_nn_feature_n, to_cat_previous_frame), 1) + pred_ = dynamic_seghead(to_cat) + pred_ = pred_.transpose([1, 0, 2, 3]) + dic_tmp[seq_names[n]] = pred_ + + if global_map_tmp_dic is None: + return dic_tmp + else: + if local_map_dics is None: + return dic_tmp, global_map_tmp_dic + else: + return dic_tmp, global_map_tmp_dic, local_map_dics + + def int_seghead(self, + ref_frame_embedding=None, + ref_scribble_label=None, + prev_round_label=None, + normalize_nearest_neighbor_distances=True, + global_map_tmp_dic=None, + local_map_dics=None, + interaction_num=None, + seq_names=None, + gt_ids=None, + k_nearest_neighbors=1, + frame_num=None, + first_inter=True): + dic_tmp = {} + bs, c, h, w = ref_frame_embedding.shape + scale_ref_scribble_label = paddle.nn.functional.interpolate( + float_(ref_scribble_label), size=(h, w), mode='nearest') + scale_ref_scribble_label = int_(scale_ref_scribble_label) + if not first_inter: + scale_prev_round_label = paddle.nn.functional.interpolate( + float_(prev_round_label), size=(h, w), mode='nearest') + scale_prev_round_label = int_(scale_prev_round_label) + n_chunks = 500 + for n in range(bs): + + gt_id = paddle.arange(0, gt_ids[n] + 1) + + gt_id = int_(gt_id) + + seq_ref_frame_embedding = ref_frame_embedding[n] + + ########################Local dist map + seq_ref_frame_embedding = paddle.transpose(seq_ref_frame_embedding, + [1, 2, 0]) + seq_ref_scribble_label = paddle.transpose( + scale_ref_scribble_label[n], [1, 2, 0]) + nn_features_n = local_previous_frame_nearest_neighbor_features_per_object( + prev_frame_embedding=seq_ref_frame_embedding, + query_embedding=seq_ref_frame_embedding, + prev_frame_labels=seq_ref_scribble_label, + gt_ids=gt_id, + max_distance=cfg.MODEL_MAX_LOCAL_DISTANCE) + + ####### + ######################Global map update + if seq_names[n] not in global_map_tmp_dic: + global_map_tmp_dic[seq_names[n]] = paddle.ones_like( + nn_features_n).tile([104, 1, 1, 1, 1]) + nn_features_n_ = paddle.where( + nn_features_n <= + global_map_tmp_dic[seq_names[n]][frame_num[n]].unsqueeze(0), + nn_features_n, + global_map_tmp_dic[seq_names[n]][frame_num[n]].unsqueeze(0)) + + ### + + ### + global_map_tmp_dic[seq_names[n]][ + frame_num[n]] = nn_features_n_.detach()[0] + ##################Local map update + if local_map_dics is not None: + local_map_tmp_dic, local_map_dist_dic = local_map_dics + if seq_names[n] not in local_map_dist_dic: + local_map_dist_dic[seq_names[n]] = paddle.zeros([104, 9]) + if seq_names[n] not in local_map_tmp_dic: + local_map_tmp_dic[seq_names[n]] = paddle.ones_like( + nn_features_n).unsqueeze(0).tile([104, 9, 1, 1, 1, 1]) + local_map_dist_dic[seq_names[n]][frame_num[n]][interaction_num - + 1] = 0 + + local_map_dics = (local_map_tmp_dic, local_map_dist_dic) + + ################## + to_cat_current_frame_embedding = ref_frame_embedding[n].unsqueeze( + 0).tile((gt_id.shape[0], 1, 1, 1)) + to_cat_nn_feature_n = nn_features_n.squeeze(0).transpose( + [2, 3, 0, 1]) + + to_cat_scribble_mask_to_cat = ( + float_(seq_ref_scribble_label) == float_(gt_id) + ) # float comparision? + to_cat_scribble_mask_to_cat = float_( + to_cat_scribble_mask_to_cat.unsqueeze(-1).transpose( + [2, 3, 0, 1])) + if not first_inter: + seq_prev_round_label = scale_prev_round_label[n].transpose( + [1, 2, 0]) + + to_cat_prev_round_to_cat = ( + float_(seq_prev_round_label) == float_(gt_id) + ) # float comparision? + to_cat_prev_round_to_cat = float_( + to_cat_prev_round_to_cat.unsqueeze(-1).transpose( + [2, 3, 0, 1])) + else: + to_cat_prev_round_to_cat = paddle.zeros_like( + to_cat_scribble_mask_to_cat) + to_cat_prev_round_to_cat[0] = 1. + + to_cat = paddle.concat( + (to_cat_current_frame_embedding, to_cat_scribble_mask_to_cat, + to_cat_prev_round_to_cat), 1) + + pred_ = self.inter_seghead(to_cat) + pred_ = pred_.transpose([1, 0, 2, 3]) + dic_tmp[seq_names[n]] = pred_ + if local_map_dics is None: + return dic_tmp + else: + return dic_tmp, local_map_dics diff --git a/applications/Ma-Net/networks/__init__.py b/applications/Ma-Net/networks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/Ma-Net/networks/aspp.py b/applications/Ma-Net/networks/aspp.py new file mode 100644 index 0000000000000000000000000000000000000000..a4f289f77e55593c199123db84863522f7af78ea --- /dev/null +++ b/applications/Ma-Net/networks/aspp.py @@ -0,0 +1,123 @@ +import math +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from utils.api import kaiming_normal_ + + +class _ASPPModule(nn.Layer): + def __init__(self, inplanes, planes, kernel_size, padding, dilation, + BatchNorm): + super(_ASPPModule, self).__init__() + self.atrous_conv = nn.Conv2D(inplanes, + planes, + kernel_size=kernel_size, + stride=1, + padding=padding, + dilation=dilation, + bias_attr=False) + self.bn = BatchNorm(planes) + self.relu = nn.ReLU(True) + + self._init_weight() + + def forward(self, x): + x = self.atrous_conv(x) + x = self.bn(x) + + return self.relu(x) + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + kaiming_normal_(m.weight) + elif isinstance(m, nn.BatchNorm2D): + from utils.api import fill_ + fill_(m.weight, 1) + from utils.api import zero_ + zero_(m.bias) + + +class ASPP(nn.Layer): + def __init__(self, backbone, output_stride, BatchNorm): + super(ASPP, self).__init__() + if backbone == 'drn': + inplanes = 512 + elif backbone == 'mobilenet': + inplanes = 320 + else: + inplanes = 2048 + if output_stride == 16: + dilations = [1, 6, 12, 18] + elif output_stride == 8: + dilations = [1, 12, 24, 36] + else: + raise NotImplementedError + + self.aspp1 = _ASPPModule(inplanes, + 256, + 1, + padding=0, + dilation=dilations[0], + BatchNorm=BatchNorm) + self.aspp2 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[1], + dilation=dilations[1], + BatchNorm=BatchNorm) + self.aspp3 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[2], + dilation=dilations[2], + BatchNorm=BatchNorm) + self.aspp4 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[3], + dilation=dilations[3], + BatchNorm=BatchNorm) + + self.global_avg_pool = nn.Sequential( + nn.AdaptiveAvgPool2D((1, 1)), + nn.Conv2D(inplanes, 256, 1, stride=1, bias_attr=False), + BatchNorm(256), nn.ReLU()) + self.conv1 = nn.Conv2D(1280, 256, 1, bias_attr=False) + self.bn1 = BatchNorm(256) + self.relu = nn.ReLU(True) + self.dropout = nn.Dropout(0.1) + self._init_weight() + + def forward(self, x): + x1 = self.aspp1(x) + x2 = self.aspp2(x) + x3 = self.aspp3(x) + x4 = self.aspp4(x) + x5 = self.global_avg_pool(x) + x5 = F.interpolate(x5, + size=x4.shape[2:], + mode='bilinear', + align_corners=True) + x = paddle.concat((x1, x2, x3, x4, x5), axis=1) + + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + return x + return self.dropout(x) + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + kaiming_normal_(m.weight) + elif isinstance(m, nn.BatchNorm2D): + from utils.api import fill_ + fill_(m.weight, 1) + from utils.api import zero_ + zero_(m.bias) + + +def build_aspp(backbone, output_stride, BatchNorm): + return ASPP(backbone, output_stride, BatchNorm) diff --git a/applications/Ma-Net/networks/backbone/__init__.py b/applications/Ma-Net/networks/backbone/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ea7dd8f5f5bb6cf9ad63edb948c116e910260598 --- /dev/null +++ b/applications/Ma-Net/networks/backbone/__init__.py @@ -0,0 +1,14 @@ +from networks.backbone import resnet, xception, drn, mobilenet + + +def build_backbone(backbone, output_stride, BatchNorm): + if backbone == 'resnet': + return resnet.ResNet101(output_stride, BatchNorm) + elif backbone == 'xception': + return xception.AlignedXception(output_stride, BatchNorm) + elif backbone == 'drn': + return drn.drn_d_54(BatchNorm) + elif backbone == 'mobilenet': + return mobilenet.MobileNetV2(output_stride, BatchNorm) + else: + raise NotImplementedError diff --git a/applications/Ma-Net/networks/backbone/drn.py b/applications/Ma-Net/networks/backbone/drn.py new file mode 100644 index 0000000000000000000000000000000000000000..18d764ef3b7694ed2793458048dae5a3148d7836 --- /dev/null +++ b/applications/Ma-Net/networks/backbone/drn.py @@ -0,0 +1,400 @@ +import paddle.nn as nn +import math + +webroot = 'https://tigress-web.princeton.edu/~fy/drn/models/' + +model_urls = { + 'resnet50': 'https://download.pypaddle.org/models/resnet50-19c8e357.pth', + 'drn-c-26': webroot + 'drn_c_26-ddedf421.pth', + 'drn-c-42': webroot + 'drn_c_42-9d336e8c.pth', + 'drn-c-58': webroot + 'drn_c_58-0a53a92c.pth', + 'drn-d-22': webroot + 'drn_d_22-4bd2f8ea.pth', + 'drn-d-38': webroot + 'drn_d_38-eebb45f0.pth', + 'drn-d-54': webroot + 'drn_d_54-0e0534ff.pth', + 'drn-d-105': webroot + 'drn_d_105-12b40979.pth' +} + + +def conv3x3(in_planes, out_planes, stride=1, padding=1, dilation=1): + return nn.Conv2D(in_planes, out_planes, kernel_size=3, stride=stride, + padding=padding, bias_attr=False, dilation=dilation) + + +class BasicBlock(nn.Layer): + expansion = 1 + + def __init__(self, inplanes, planes, stride=1, downsample=None, + dilation=(1, 1), residual=True, BatchNorm=None): + super(BasicBlock, self).__init__() + self.conv1 = conv3x3(inplanes, planes, stride, + padding=dilation[0], dilation=dilation[0]) + self.bn1 = BatchNorm(planes) + self.relu = nn.ReLU() + self.conv2 = conv3x3(planes, planes, + padding=dilation[1], dilation=dilation[1]) + self.bn2 = BatchNorm(planes) + self.downsample = downsample + self.stride = stride + self.residual = residual + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + + if self.downsample is not None: + residual = self.downsample(x) + if self.residual: + out += residual + out = self.relu(out) + + return out + + +class Bottleneck(nn.Layer): + expansion = 4 + + def __init__(self, inplanes, planes, stride=1, downsample=None, + dilation=(1, 1), residual=True, BatchNorm=None): + super(Bottleneck, self).__init__() + self.conv1 = nn.Conv2D(inplanes, planes, kernel_size=1, bias_attr=False) + self.bn1 = BatchNorm(planes) + self.conv2 = nn.Conv2D(planes, planes, kernel_size=3, stride=stride, + padding=dilation[1], bias_attr=False, + dilation=dilation[1]) + self.bn2 = BatchNorm(planes) + self.conv3 = nn.Conv2D(planes, planes * 4, kernel_size=1, bias_attr=False) + self.bn3 = BatchNorm(planes * 4) + self.relu = nn.ReLU() + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class DRN(nn.Layer): + + def __init__(self, block, layers, arch='D', + channels=(16, 32, 64, 128, 256, 512, 512, 512), + BatchNorm=None): + super(DRN, self).__init__() + self.inplanes = channels[0] + self.out_dim = channels[-1] + self.arch = arch + + if arch == 'C': + self.conv1 = nn.Conv2D(3, channels[0], kernel_size=7, stride=1, + padding=3, bias_attr=False) + self.bn1 = BatchNorm(channels[0]) + self.relu = nn.ReLU() + + self.layer1 = self._make_layer( + BasicBlock, channels[0], layers[0], stride=1, BatchNorm=BatchNorm) + self.layer2 = self._make_layer( + BasicBlock, channels[1], layers[1], stride=2, BatchNorm=BatchNorm) + + elif arch == 'D': + self.layer0 = nn.Sequential( + nn.Conv2D(3, channels[0], kernel_size=7, stride=1, padding=3, + bias_attr=False), + BatchNorm(channels[0]), + nn.ReLU() + ) + + self.layer1 = self._make_conv_layers( + channels[0], layers[0], stride=1, BatchNorm=BatchNorm) + self.layer2 = self._make_conv_layers( + channels[1], layers[1], stride=2, BatchNorm=BatchNorm) + + self.layer3 = self._make_layer(block, channels[2], layers[2], stride=2, BatchNorm=BatchNorm) + self.layer4 = self._make_layer(block, channels[3], layers[3], stride=2, BatchNorm=BatchNorm) + self.layer5 = self._make_layer(block, channels[4], layers[4], + dilation=2, new_level=False, BatchNorm=BatchNorm) + self.layer6 = None if layers[5] == 0 else \ + self._make_layer(block, channels[5], layers[5], dilation=4, + new_level=False, BatchNorm=BatchNorm) + + if arch == 'C': + self.layer7 = None if layers[6] == 0 else \ + self._make_layer(BasicBlock, channels[6], layers[6], dilation=2, + new_level=False, residual=False, BatchNorm=BatchNorm) + self.layer8 = None if layers[7] == 0 else \ + self._make_layer(BasicBlock, channels[7], layers[7], dilation=1, + new_level=False, residual=False, BatchNorm=BatchNorm) + elif arch == 'D': + self.layer7 = None if layers[6] == 0 else \ + self._make_conv_layers(channels[6], layers[6], dilation=2, BatchNorm=BatchNorm) + self.layer8 = None if layers[7] == 0 else \ + self._make_conv_layers(channels[7], layers[7], dilation=1, BatchNorm=BatchNorm) + + self._init_weight() + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + n = m._kernel_size[0] * m._kernel_size[1] * m._out_channels + m.weight.normal_(0, math.sqrt(2. / n)) + elif isinstance(m, nn.BatchNorm2D): + from manet_paddle.utils.api import fill_ + fill_(m.weight, 1) + from manet_paddle.utils.api import zero_ + zero_(m.bias) + + + def _make_layer(self, block, planes, blocks, stride=1, dilation=1, + new_level=True, residual=True, BatchNorm=None): + assert dilation == 1 or dilation % 2 == 0 + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D(self.inplanes, planes * block.expansion, + kernel_size=1, stride=stride, bias_attr=False), + BatchNorm(planes * block.expansion), + ) + + layers = list() + layers.append(block( + self.inplanes, planes, stride, downsample, + dilation=(1, 1) if dilation == 1 else ( + dilation // 2 if new_level else dilation, dilation), + residual=residual, BatchNorm=BatchNorm)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes, residual=residual, + dilation=(dilation, dilation), BatchNorm=BatchNorm)) + + return nn.Sequential(*layers) + + def _make_conv_layers(self, channels, convs, stride=1, dilation=1, BatchNorm=None): + modules = [] + for i in range(convs): + modules.extend([ + nn.Conv2D(self.inplanes, channels, kernel_size=3, + stride=stride if i == 0 else 1, + padding=dilation, bias_attr=False, dilation=dilation), + BatchNorm(channels), + nn.ReLU()]) + self.inplanes = channels + return nn.Sequential(*modules) + + def forward(self, x): + if self.arch == 'C': + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + elif self.arch == 'D': + x = self.layer0(x) + + x = self.layer1(x) + x = self.layer2(x) + + x = self.layer3(x) + low_level_feat = x + + x = self.layer4(x) + x = self.layer5(x) + + if self.layer6 is not None: + x = self.layer6(x) + + if self.layer7 is not None: + x = self.layer7(x) + + if self.layer8 is not None: + x = self.layer8(x) + + return x, low_level_feat + + +class DRN_A(nn.Layer): + + def __init__(self, block, layers, BatchNorm=None): + self.inplanes = 64 + super(DRN_A, self).__init__() + self.out_dim = 512 * block.expansion + self.conv1 = nn.Conv2D(3, 64, kernel_size=7, stride=2, padding=3, + bias_attr=False) + self.bn1 = BatchNorm(64) + self.relu = nn.ReLU() + self.maxpool = nn.MaxPool2D(kernel_size=3, stride=2, padding=1) + self.layer1 = self._make_layer(block, 64, layers[0], BatchNorm=BatchNorm) + self.layer2 = self._make_layer(block, 128, layers[1], stride=2, BatchNorm=BatchNorm) + self.layer3 = self._make_layer(block, 256, layers[2], stride=1, + dilation=2, BatchNorm=BatchNorm) + self.layer4 = self._make_layer(block, 512, layers[3], stride=1, + dilation=4, BatchNorm=BatchNorm) + + self._init_weight() + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + n = m._kernel_size[0] * m._kernel_size[1] * m._out_channels + m.weight.normal_(0, math.sqrt(2. / n)) + elif isinstance(m, nn.BatchNorm2D): + from manet_paddle.utils.api import fill_ + fill_(m.weight, 1) + from manet_paddle.utils.api import zero_ + zero_(m.bias) + + def _make_layer(self, block, planes, blocks, stride=1, dilation=1, BatchNorm=None): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D(self.inplanes, planes * block.expansion, + kernel_size=1, stride=stride, bias_attr=False), + BatchNorm(planes * block.expansion), + ) + + layers = [] + layers.append(block(self.inplanes, planes, stride, downsample, BatchNorm=BatchNorm)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes, + dilation=(dilation, dilation, ), BatchNorm=BatchNorm)) + + return nn.Sequential(*layers) + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + + return x + +def drn_a_50(BatchNorm, pretrained=True): + model = DRN_A(Bottleneck, [3, 4, 6, 3], BatchNorm=BatchNorm) + + if pretrained: + import paddlehub as hub + model.set_state_dict(hub.Module(name="resnet50_vd_animals")) + return model + + +def drn_c_26(BatchNorm, pretrained=True): + model = DRN(BasicBlock, [1, 1, 2, 2, 2, 2, 1, 1], arch='C', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-c-26']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + + +def drn_c_42(BatchNorm, pretrained=True): + model = DRN(BasicBlock, [1, 1, 3, 4, 6, 3, 1, 1], arch='C', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-c-42']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + + +def drn_c_58(BatchNorm, pretrained=True): + model = DRN(Bottleneck, [1, 1, 3, 4, 6, 3, 1, 1], arch='C', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-c-58']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + + +def drn_d_22(BatchNorm, pretrained=True): + model = DRN(BasicBlock, [1, 1, 2, 2, 2, 2, 1, 1], arch='D', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-d-22']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + + +def drn_d_24(BatchNorm, pretrained=True): + model = DRN(BasicBlock, [1, 1, 2, 2, 2, 2, 2, 2], arch='D', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-d-24']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + + +def drn_d_38(BatchNorm, pretrained=True): + model = DRN(BasicBlock, [1, 1, 3, 4, 6, 3, 1, 1], arch='D', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-d-38']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + + +def drn_d_40(BatchNorm, pretrained=True): + model = DRN(BasicBlock, [1, 1, 3, 4, 6, 3, 2, 2], arch='D', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-d-40']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + + +def drn_d_54(BatchNorm, pretrained=True): + model = DRN(Bottleneck, [1, 1, 3, 4, 6, 3, 1, 1], arch='D', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-d-54']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + + +def drn_d_105(BatchNorm, pretrained=True): + model = DRN(Bottleneck, [1, 1, 3, 4, 23, 3, 1, 1], arch='D', BatchNorm=BatchNorm) + if pretrained: + pretrained = model_zoo.load_url(model_urls['drn-d-105']) + del pretrained['fc.weight'] + del pretrained['fc.bias'] + model.set_state_dict(pretrained) + return model + +if __name__ == "__main__": + import paddle + model = drn_a_50(BatchNorm=nn.BatchNorm2D, pretrained=True) + input = paddle.rand([1, 3, 512, 512]) + output, low_level_feat = model(input) + print(output.shape) + print(low_level_feat.shape) diff --git a/applications/Ma-Net/networks/backbone/mobilenet.py b/applications/Ma-Net/networks/backbone/mobilenet.py new file mode 100644 index 0000000000000000000000000000000000000000..affe97991c0babae5f5f39aae4937e147f61a044 --- /dev/null +++ b/applications/Ma-Net/networks/backbone/mobilenet.py @@ -0,0 +1,163 @@ +import paddle +import paddle.nn.functional as F +import paddle.nn as nn +import math +from utils.api import kaiming_normal_ + + +def conv_bn(inp, oup, stride, BatchNorm): + return nn.Sequential(nn.Conv2D(inp, oup, 3, stride, 1, bias_attr=False), + BatchNorm(oup), nn.ReLU6()) + + +def fixed_padding(inputs, kernel_size, dilation): + kernel_size_effective = kernel_size + (kernel_size - 1) * (dilation - 1) + pad_total = kernel_size_effective - 1 + pad_beg = pad_total // 2 + pad_end = pad_total - pad_beg + padded_inputs = F.pad(inputs, (pad_beg, pad_end, pad_beg, pad_end)) + return padded_inputs + + +class InvertedResidual(nn.Layer): + def __init__(self, inp, oup, stride, dilation, expand_ratio, BatchNorm): + super(InvertedResidual, self).__init__() + self.stride = stride + assert stride in [1, 2] + + hidden_dim = round(inp * expand_ratio) + self.use_res_connect = self.stride == 1 and inp == oup + self.kernel_size = 3 + self.dilation = dilation + + if expand_ratio == 1: + self.conv = nn.Sequential( + # dw + nn.Conv2D(hidden_dim, + hidden_dim, + 3, + stride, + 0, + dilation, + groups=hidden_dim, + bias_attr=False), + BatchNorm(hidden_dim), + nn.ReLU6(), + # pw-linear + nn.Conv2D(hidden_dim, oup, 1, 1, 0, 1, 1, bias_attr=False), + BatchNorm(oup), + ) + else: + self.conv = nn.Sequential( + # pw + nn.Conv2D(inp, hidden_dim, 1, 1, 0, 1, bias_attr=False), + BatchNorm(hidden_dim), + nn.ReLU6(), + # dw + nn.Conv2D(hidden_dim, + hidden_dim, + 3, + stride, + 0, + dilation, + groups=hidden_dim, + bias_attr=False), + BatchNorm(hidden_dim), + nn.ReLU6(), + # pw-linear + nn.Conv2D(hidden_dim, oup, 1, 1, 0, 1, bias_attr=False), + BatchNorm(oup), + ) + + def forward(self, x): + x_pad = fixed_padding(x, self.kernel_size, dilation=self.dilation) + if self.use_res_connect: + x = x + self.conv(x_pad) + else: + x = self.conv(x_pad) + return x + + +class MobileNetV2(nn.Layer): + def __init__(self, + output_stride=8, + BatchNorm=None, + width_mult=1., + pretrained=True): + super(MobileNetV2, self).__init__() + block = InvertedResidual + input_channel = 32 + current_stride = 1 + rate = 1 + interverted_residual_setting = [ + # t, c, n, s + [1, 16, 1, 1], + [6, 24, 2, 2], + [6, 32, 3, 2], + [6, 64, 4, 2], + [6, 96, 3, 1], + [6, 160, 3, 2], + [6, 320, 1, 1], + ] + + # building first layer + input_channel = int(input_channel * width_mult) + self.features = [conv_bn(3, input_channel, 2, BatchNorm)] + current_stride *= 2 + # building inverted residual blocks + for t, c, n, s in interverted_residual_setting: + if current_stride == output_stride: + stride = 1 + dilation = rate + rate *= s + else: + stride = s + dilation = 1 + current_stride *= s + output_channel = int(c * width_mult) + for i in range(n): + if i == 0: + self.features.append( + block(input_channel, output_channel, stride, dilation, + t, BatchNorm)) + else: + self.features.append( + block(input_channel, output_channel, 1, dilation, t, + BatchNorm)) + input_channel = output_channel + self.features = nn.Sequential(*self.features) + self._initialize_weights() + + if pretrained: + self._load_pretrained_model() + + self.low_level_features = self.features[0:4] + self.high_level_features = self.features[4:] + + def forward(self, x): + low_level_feat = self.low_level_features(x) + x = self.high_level_features(low_level_feat) + return x, low_level_feat + + def _load_pretrained_model(self): + import paddlehub as hub + pretrain_dict = hub.Module(name="mobilenet_v2_imagenet") + model_dict = {} + state_dict = self.state_dict() + for k, v in pretrain_dict.items(): + if k in state_dict: + model_dict[k] = v + state_dict.update(model_dict) + self.set_state_dict(state_dict) + + def _initialize_weights(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + # n = m._kernel_size[0] * m._kernel_size[1] * m._out_channels + # m.weight.normal_(0, math.sqrt(2. / n)) + kaiming_normal_(m.weight) + elif isinstance(m, nn.BatchNorm2D): + from utils.api import fill_ + fill_(m.weight, 1) + from utils.api import zero_ + zero_(m.bias) diff --git a/applications/Ma-Net/networks/backbone/resnet.py b/applications/Ma-Net/networks/backbone/resnet.py new file mode 100644 index 0000000000000000000000000000000000000000..310acbcb7e8aefc287f33365bd2111bac0d7fa1f --- /dev/null +++ b/applications/Ma-Net/networks/backbone/resnet.py @@ -0,0 +1,239 @@ +import math +import paddle.nn as nn +# from reprod_log.utils import paddle2np +import paddle + +from utils.api import normal_, fill_, zero_ + + +class Bottleneck(nn.Layer): + expansion = 4 + + def __init__(self, + inplanes, + planes, + stride=1, + dilation=1, + downsample=None, + BatchNorm=None): + super(Bottleneck, self).__init__() + self.conv1 = nn.Conv2D(inplanes, planes, kernel_size=1, bias_attr=False) + self.bn1 = BatchNorm(planes) + self.conv2 = nn.Conv2D(planes, + planes, + kernel_size=3, + stride=stride, + dilation=dilation, + padding=dilation, + bias_attr=False) + self.bn2 = BatchNorm(planes) + self.conv3 = nn.Conv2D(planes, + planes * 4, + kernel_size=1, + bias_attr=False) + self.bn3 = BatchNorm(planes * 4) + self.relu = nn.ReLU() + self.downsample = downsample + self.stride = stride + self.dilation = dilation + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class ResNet(nn.Layer): + def __init__(self, + block, + layers, + output_stride, + BatchNorm, + pretrained=False): + self.inplanes = 64 + super(ResNet, self).__init__() + blocks = [1, 2, 4] + if output_stride == 16: + strides = [1, 2, 2, 1] + dilations = [1, 1, 1, 2] + elif output_stride == 8: + strides = [1, 2, 1, 1] + dilations = [1, 1, 2, 4] + else: + raise NotImplementedError + + # Modules + self.conv1 = nn.Conv2D(3, + 64, + kernel_size=7, + stride=2, + padding=3, + bias_attr=False) + self.bn1 = BatchNorm(64) + self.relu = nn.ReLU() + self.maxpool = nn.MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.layer1 = self._make_layer(block, + 64, + layers[0], + stride=strides[0], + dilation=dilations[0], + BatchNorm=BatchNorm) + self.layer2 = self._make_layer(block, + 128, + layers[1], + stride=strides[1], + dilation=dilations[1], + BatchNorm=BatchNorm) + self.layer3 = self._make_layer(block, + 256, + layers[2], + stride=strides[2], + dilation=dilations[2], + BatchNorm=BatchNorm) + self.layer4 = self._make_MG_unit(block, + 512, + blocks=blocks, + stride=strides[3], + dilation=dilations[3], + BatchNorm=BatchNorm) + # self.layer4 = self._make_layer(block, 512, layers[3], stride=strides[3], dilation=dilations[3], BatchNorm=BatchNorm) + self._init_weight() + + if pretrained: + self._load_pretrained_model() + + def _make_layer(self, + block, + planes, + blocks, + stride=1, + dilation=1, + BatchNorm=None): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D(self.inplanes, + planes * block.expansion, + kernel_size=1, + stride=stride, + bias_attr=False), + BatchNorm(planes * block.expansion), + ) + + layers = [] + layers.append( + block(self.inplanes, planes, stride, dilation, downsample, + BatchNorm)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append( + block(self.inplanes, + planes, + dilation=dilation, + BatchNorm=BatchNorm)) + + return nn.Sequential(*layers) + + def _make_MG_unit(self, + block, + planes, + blocks, + stride=1, + dilation=1, + BatchNorm=None): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D(self.inplanes, + planes * block.expansion, + kernel_size=1, + stride=stride, + bias_attr=False), + BatchNorm(planes * block.expansion), + ) + + layers = [] + layers.append( + block(self.inplanes, + planes, + stride, + dilation=blocks[0] * dilation, + downsample=downsample, + BatchNorm=BatchNorm)) + self.inplanes = planes * block.expansion + for i in range(1, len(blocks)): + layers.append( + block(self.inplanes, + planes, + stride=1, + dilation=blocks[i] * dilation, + BatchNorm=BatchNorm)) + + return nn.Sequential(*layers) + + def forward(self, input): + # print('input:', input.mean().item()) + x = self.conv1(input) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + + x = self.layer1(x) + low_level_feat = x + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + return x, low_level_feat + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + n = m._kernel_size[0] * m._kernel_size[1] * m._out_channels + fill_(m.weight, 1) + # normal_(m.weight, 0, math.sqrt(2. / n)) + elif isinstance(m, nn.BatchNorm2D): + fill_(m.weight, 1) + zero_(m.bias) + return self.sublayers() + + def _load_pretrained_model(self): + # TODO + pretrain_dict = paddle.load( + '/home/lc/manet/manet_paddle/model_best.pdparams.tar') + model_dict = {} + state_dict = self.state_dict() + for k, v in pretrain_dict.items(): + if k in state_dict: + model_dict[k] = v + state_dict.update(model_dict) + self.set_state_dict(state_dict) + + +def ResNet101(output_stride, BatchNorm, pretrained=False): + """Constructs a ResNet-101 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(Bottleneck, [3, 4, 23, 3], + output_stride, + BatchNorm, + pretrained=pretrained) + return model diff --git a/applications/Ma-Net/networks/backbone/xception.py b/applications/Ma-Net/networks/backbone/xception.py new file mode 100644 index 0000000000000000000000000000000000000000..e5dfb562bbff84532e1760728271a90f41c337c1 --- /dev/null +++ b/applications/Ma-Net/networks/backbone/xception.py @@ -0,0 +1,455 @@ +import math +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + + +def fixed_padding(inputs, kernel_size, dilation): + kernel_size_effective = kernel_size + (kernel_size - 1) * (dilation - 1) + pad_total = kernel_size_effective - 1 + pad_beg = pad_total // 2 + pad_end = pad_total - pad_beg + padded_inputs = F.pad(inputs, (pad_beg, pad_end, pad_beg, pad_end)) + return padded_inputs + + +class SeparableConv2d(nn.Layer): + def __init__(self, + inplanes, + planes, + kernel_size=3, + stride=1, + dilation=1, + bias=False, + BatchNorm=None): + super(SeparableConv2d, self).__init__() + + self.conv1 = nn.Conv2D(inplanes, + inplanes, + kernel_size, + stride, + 0, + dilation, + groups=inplanes, + bias=bias) + self.bn = BatchNorm(inplanes) + self.pointwise = nn.Conv2D(inplanes, planes, 1, 1, 0, 1, 1, bias=bias) + + def forward(self, x): + x = fixed_padding(x, + self.conv1._kernel_size[0], + dilation=self.conv1.dilation[0]) + x = self.conv1(x) + x = self.bn(x) + x = self.pointwise(x) + return x + + +class Block(nn.Layer): + def __init__(self, + inplanes, + planes, + reps, + stride=1, + dilation=1, + BatchNorm=None, + start_with_relu=True, + grow_first=True, + is_last=False): + super(Block, self).__init__() + + if planes != inplanes or stride != 1: + self.skip = nn.Conv2D(inplanes, + planes, + 1, + stride=stride, + bias_attr=False) + self.skipbn = BatchNorm(planes) + else: + self.skip = None + + self.relu = nn.ReLU() + rep = [] + + filters = inplanes + if grow_first: + rep.append(self.relu) + rep.append( + SeparableConv2d(inplanes, + planes, + 3, + 1, + dilation, + BatchNorm=BatchNorm)) + rep.append(BatchNorm(planes)) + filters = planes + + for i in range(reps - 1): + rep.append(self.relu) + rep.append( + SeparableConv2d(filters, + filters, + 3, + 1, + dilation, + BatchNorm=BatchNorm)) + rep.append(BatchNorm(filters)) + + if not grow_first: + rep.append(self.relu) + rep.append( + SeparableConv2d(inplanes, + planes, + 3, + 1, + dilation, + BatchNorm=BatchNorm)) + rep.append(BatchNorm(planes)) + + if stride != 1: + rep.append(self.relu) + rep.append( + SeparableConv2d(planes, planes, 3, 2, BatchNorm=BatchNorm)) + rep.append(BatchNorm(planes)) + + if stride == 1 and is_last: + rep.append(self.relu) + rep.append( + SeparableConv2d(planes, planes, 3, 1, BatchNorm=BatchNorm)) + rep.append(BatchNorm(planes)) + + if not start_with_relu: + rep = rep[1:] + + self.rep = nn.Sequential(*rep) + + def forward(self, inp): + x = self.rep(inp) + + if self.skip is not None: + skip = self.skip(inp) + skip = self.skipbn(skip) + else: + skip = inp + + x = x + skip + + return x + + +class AlignedXception(nn.Layer): + """ + Modified Alighed Xception + """ + def __init__(self, output_stride, BatchNorm, pretrained=True): + super(AlignedXception, self).__init__() + + if output_stride == 16: + entry_block3_stride = 2 + middle_block_dilation = 1 + exit_block_dilations = (1, 2) + elif output_stride == 8: + entry_block3_stride = 1 + middle_block_dilation = 2 + exit_block_dilations = (2, 4) + else: + raise NotImplementedError + + # Entry flow + self.conv1 = nn.Conv2D(3, 32, 3, stride=2, padding=1, bias_attr=False) + self.bn1 = BatchNorm(32) + self.relu = nn.ReLU() + + self.conv2 = nn.Conv2D(32, 64, 3, stride=1, padding=1, bias_attr=False) + self.bn2 = BatchNorm(64) + + self.block1 = Block(64, + 128, + reps=2, + stride=2, + BatchNorm=BatchNorm, + start_with_relu=False) + self.block2 = Block(128, + 256, + reps=2, + stride=2, + BatchNorm=BatchNorm, + start_with_relu=False, + grow_first=True) + self.block3 = Block(256, + 728, + reps=2, + stride=entry_block3_stride, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True, + is_last=True) + + # Middle flow + self.block4 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block5 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block6 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block7 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block8 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block9 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block10 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block11 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block12 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block13 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block14 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block15 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block16 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block17 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block18 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + self.block19 = Block(728, + 728, + reps=3, + stride=1, + dilation=middle_block_dilation, + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=True) + + # Exit flow + self.block20 = Block(728, + 1024, + reps=2, + stride=1, + dilation=exit_block_dilations[0], + BatchNorm=BatchNorm, + start_with_relu=True, + grow_first=False, + is_last=True) + + self.conv3 = SeparableConv2d(1024, + 1536, + 3, + stride=1, + dilation=exit_block_dilations[1], + BatchNorm=BatchNorm) + self.bn3 = BatchNorm(1536) + + self.conv4 = SeparableConv2d(1536, + 1536, + 3, + stride=1, + dilation=exit_block_dilations[1], + BatchNorm=BatchNorm) + self.bn4 = BatchNorm(1536) + + self.conv5 = SeparableConv2d(1536, + 2048, + 3, + stride=1, + dilation=exit_block_dilations[1], + BatchNorm=BatchNorm) + self.bn5 = BatchNorm(2048) + + # Init weights + self._init_weight() + + # Load pretrained model + if pretrained: + self._load_pretrained_model() + + def forward(self, x): + # Entry flow + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + x = self.conv2(x) + x = self.bn2(x) + x = self.relu(x) + + x = self.block1(x) + # add relu here + x = self.relu(x) + low_level_feat = x + x = self.block2(x) + x = self.block3(x) + + # Middle flow + x = self.block4(x) + x = self.block5(x) + x = self.block6(x) + x = self.block7(x) + x = self.block8(x) + x = self.block9(x) + x = self.block10(x) + x = self.block11(x) + x = self.block12(x) + x = self.block13(x) + x = self.block14(x) + x = self.block15(x) + x = self.block16(x) + x = self.block17(x) + x = self.block18(x) + x = self.block19(x) + + # Exit flow + x = self.block20(x) + x = self.relu(x) + x = self.conv3(x) + x = self.bn3(x) + x = self.relu(x) + + x = self.conv4(x) + x = self.bn4(x) + x = self.relu(x) + + x = self.conv5(x) + x = self.bn5(x) + x = self.relu(x) + + return x, low_level_feat + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + n = m._kernel_size[0] * m._kernel_size[1] * m._out_channels + m.weight.normal_(0, math.sqrt(2. / n)) + elif isinstance(m, nn.BatchNorm2D): + from utils.api import fill_ + fill_(m.weight, 1) + from utils.api import zero_ + zero_(m.bias) + + def _load_pretrained_model(self): + import paddlehub as hub + pretrain_dict = hub.Module(name="xception71_imagenet") + model_dict = {} + state_dict = self.state_dict() + + for k, v in pretrain_dict.items(): + if k in model_dict: + if 'pointwise' in k: + v = v.unsqueeze(-1).unsqueeze(-1) + if k.startswith('block11'): + model_dict[k] = v + model_dict[k.replace('block11', 'block12')] = v + model_dict[k.replace('block11', 'block13')] = v + model_dict[k.replace('block11', 'block14')] = v + model_dict[k.replace('block11', 'block15')] = v + model_dict[k.replace('block11', 'block16')] = v + model_dict[k.replace('block11', 'block17')] = v + model_dict[k.replace('block11', 'block18')] = v + model_dict[k.replace('block11', 'block19')] = v + elif k.startswith('block12'): + model_dict[k.replace('block12', 'block20')] = v + elif k.startswith('bn3'): + model_dict[k] = v + model_dict[k.replace('bn3', 'bn4')] = v + elif k.startswith('conv4'): + model_dict[k.replace('conv4', 'conv5')] = v + elif k.startswith('bn4'): + model_dict[k.replace('bn4', 'bn5')] = v + else: + model_dict[k] = v + state_dict.update(model_dict) + self.set_state_dict(state_dict) diff --git a/applications/Ma-Net/networks/decoder.py b/applications/Ma-Net/networks/decoder.py new file mode 100644 index 0000000000000000000000000000000000000000..f4bb19f0e0ab028cf1c85ae001d70daf02e93997 --- /dev/null +++ b/applications/Ma-Net/networks/decoder.py @@ -0,0 +1,66 @@ +import math +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from utils.api import kaiming_normal_ + + +class Decoder(nn.Layer): + def __init__(self, num_classes, backbone, BatchNorm): + super(Decoder, self).__init__() + if backbone == 'resnet' or backbone == 'drn' or backbone == 'resnet_edge': + low_level_inplanes = 256 + elif backbone == 'xception': + low_level_inplanes = 128 + elif backbone == 'mobilenet': + low_level_inplanes = 24 + else: + raise NotImplementedError + + self.conv1 = nn.Conv2D(low_level_inplanes, 48, 1, bias_attr=False) + self.bn1 = BatchNorm(48) + self.relu = nn.ReLU(True) + self.last_conv = nn.Sequential( + nn.Conv2D(304, + 256, + kernel_size=3, + stride=1, + padding=1, + bias_attr=False), BatchNorm(256), nn.ReLU(True), + nn.Sequential(), + nn.Conv2D(256, + 256, + kernel_size=3, + stride=1, + padding=1, + bias_attr=False), BatchNorm(256), nn.ReLU(True), + nn.Sequential()) + self._init_weight() + + def forward(self, x, low_level_feat): + low_level_feat = self.conv1(low_level_feat) + low_level_feat = self.bn1(low_level_feat) + low_level_feat = self.relu(low_level_feat) + + x = F.interpolate(x, + size=low_level_feat.shape[2:], + mode='bilinear', + align_corners=True) + x = paddle.concat((x, low_level_feat), axis=1) + x = self.last_conv(x) + + return x + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + kaiming_normal_(m.weight) + elif isinstance(m, nn.BatchNorm2D): + from utils.api import fill_ + fill_(m.weight, 1) + from utils.api import zero_ + zero_(m.bias) + + +def build_decoder(num_classes, backbone, BatchNorm): + return Decoder(num_classes, backbone, BatchNorm) diff --git a/applications/Ma-Net/networks/deeplab.py b/applications/Ma-Net/networks/deeplab.py new file mode 100644 index 0000000000000000000000000000000000000000..fe801573318fdc7c95e3ad43702b04aaf591756e --- /dev/null +++ b/applications/Ma-Net/networks/deeplab.py @@ -0,0 +1,81 @@ +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from networks.aspp import build_aspp +from networks.decoder import build_decoder +from networks.backbone import build_backbone + + +class FrozenBatchNorm2d(nn.Layer): + def __init__(self, n): + super(FrozenBatchNorm2d, self).__init__() + self.register_buffer("weight", paddle.ones(n)) + self.register_buffer("bias", paddle.zeros(n)) + self.register_buffer("running_mean", paddle.zeros(n)) + self.register_buffer("running_var", paddle.ones(n)) + + def forward(self, x): + if x.dtype == paddle.float16: + self.weight = self.weight.half() + self.bias = self.bias.half() + self.running_mean = self.running_mean.half() + self.running_var = self.running_var.half() + scale = self.weight * self.running_var.rsqrt() + bias = self.bias - self.running_mean * scale + scale = scale.reshape(1, -1, 1, 1) + bias = bias.reshape(1, -1, 1, 1) + return x * scale + bias + + +class DeepLab(nn.Layer): + def __init__(self, + backbone='resnet', + output_stride=16, + num_classes=21, + sync_bn=True, + freeze_bn=False): + super(DeepLab, self).__init__() + if backbone == 'drn': + output_stride = 8 + if freeze_bn == True: + print("Use frozen BN in DeepLab") + BatchNorm = FrozenBatchNorm2d + else: + BatchNorm = nn.BatchNorm2D + + self.backbone = build_backbone(backbone, output_stride, BatchNorm) + self.aspp = build_aspp(backbone, output_stride, BatchNorm) + self.decoder = build_decoder(num_classes, backbone, BatchNorm) + + def forward(self, input): + x, low_level_feat = self.backbone(input) + x = self.aspp(x) + x = self.decoder(x, low_level_feat) + + return x + + def freeze_bn(self): + for m in self.sublayers(): + if isinstance(m, nn.BatchNorm2D): + m.eval() + + def get_1x_lr_params(self): + modules = [self.backbone] + for i in range(len(modules)): + for m in modules[i].named_modules(): + if isinstance(m[1], nn.Conv2D) or isinstance( + m[1], nn.BatchNorm2D): + for p in m[1].parameters(): + if p.requires_grad: + yield p + + def get_10x_lr_params(self): + modules = [self.aspp, self.decoder] + for i in range(len(modules)): + for m in modules[i].named_modules(): + if isinstance(m[1], nn.Conv2D) or isinstance( + m[1], nn.BatchNorm2D): + for p in m[1].parameters(): + if p.requires_grad: + yield p diff --git a/applications/Ma-Net/networks/loss.py b/applications/Ma-Net/networks/loss.py new file mode 100644 index 0000000000000000000000000000000000000000..e818b8c4203ee8f10f2fdac847d8d14443a65c16 --- /dev/null +++ b/applications/Ma-Net/networks/loss.py @@ -0,0 +1,153 @@ +import paddle +import paddle.nn as nn +import os + + +class Added_BCEWithLogitsLoss(nn.Layer): + def __init__(self, + top_k_percent_pixels=None, + hard_example_mining_step=100000): + super(Added_BCEWithLogitsLoss, self).__init__() + self.top_k_percent_pixels = top_k_percent_pixels + if top_k_percent_pixels is not None: + assert (top_k_percent_pixels > 0 and top_k_percent_pixels < 1) + self.hard_example_mining_step = hard_example_mining_step + if self.top_k_percent_pixels == None: + self.bceloss = nn.BCEWithLogitsLoss(reduction='mean') + else: + self.bceloss = nn.BCEWithLogitsLoss(reduction='none') + + def forward(self, dic_tmp, y, step): + final_loss = 0 + for seq_name in dic_tmp.keys(): + pred_logits = dic_tmp[seq_name] + gts = y[seq_name] + if self.top_k_percent_pixels == None: + final_loss += self.bceloss(pred_logits, gts) + else: + # Only compute the loss for top k percent pixels. + # First, compute the loss for all pixels. Note we do not put the loss + # to loss_collection and set reduction = None to keep the shape. + num_pixels = float(pred_logits.shape[2] * pred_logits.shape[3]) + pred_logits = pred_logits.view( + -1, pred_logits.shape[1], + pred_logits.shape[2] * pred_logits.shape[3]) + gts = gts.view(-1, gts.shape[1], gts.shape[2] * gts.shape[3]) + pixel_losses = self.bceloss(pred_logits, gts) + if self.hard_example_mining_step == 0: + top_k_pixels = int(self.top_k_percent_pixels * num_pixels) + else: + ratio = min(1.0, + step / float(self.hard_example_mining_step)) + top_k_pixels = int((ratio * self.top_k_percent_pixels + + (1.0 - ratio)) * num_pixels) + _, top_k_indices = paddle.topk(pixel_losses, + k=top_k_pixels, + axis=2) + + final_loss += nn.BCEWithLogitsLoss(weight=top_k_indices, + reduction='mean')( + pred_logits, gts) + return final_loss + + +class Added_CrossEntropyLoss(nn.Layer): + def __init__(self, + top_k_percent_pixels=None, + hard_example_mining_step=100000): + super(Added_CrossEntropyLoss, self).__init__() + self.top_k_percent_pixels = top_k_percent_pixels + if top_k_percent_pixels is not None: + assert (top_k_percent_pixels > 0 and top_k_percent_pixels < 1) + self.hard_example_mining_step = hard_example_mining_step + if self.top_k_percent_pixels == None: + self.celoss = nn.CrossEntropyLoss(ignore_index=255, + reduction='mean') + else: + self.celoss = nn.CrossEntropyLoss(ignore_index=255, + reduction='none') + + def forward(self, dic_tmp, y, step): + final_loss = 0 + for seq_name in dic_tmp.keys(): + pred_logits = dic_tmp[seq_name] + gts = y[seq_name] + if self.top_k_percent_pixels == None: + final_loss += self.celoss(pred_logits, gts) + else: + # Only compute the loss for top k percent pixels. + # First, compute the loss for all pixels. Note we do not put the loss + # to loss_collection and set reduction = None to keep the shape. + num_pixels = float(pred_logits.shape[2] * pred_logits.shape[3]) + pred_logits = pred_logits.reshape([ + pred_logits.shape[1], + pred_logits.shape[2] * pred_logits.shape[3] + ]).transpose([1, 0]) + gts = gts.reshape([gts.shape[1] * gts.shape[2]]) + pixel_losses = self.celoss(pred_logits, gts).reshape([1, -1]) + if self.hard_example_mining_step == 0: + top_k_pixels = int(self.top_k_percent_pixels * num_pixels) + else: + ratio = min(1.0, + step / float(self.hard_example_mining_step)) + top_k_pixels = int((ratio * self.top_k_percent_pixels + + (1.0 - ratio)) * num_pixels) + top_k_loss, top_k_indices = paddle.topk(pixel_losses, + k=top_k_pixels, + axis=1) + + final_loss += paddle.mean(top_k_loss) + return final_loss + + +class AddedEdge_CrossEntropyLoss(nn.Layer): + def __init__(self, + top_k_percent_pixels=None, + hard_example_mining_step=100000): + super(AddedEdge_CrossEntropyLoss, self).__init__() + self.top_k_percent_pixels = top_k_percent_pixels + if top_k_percent_pixels is not None: + assert (top_k_percent_pixels > 0 and top_k_percent_pixels < 1) + self.hard_example_mining_step = hard_example_mining_step + self.celoss = None + + def forward(self, pred_logits, gts, step): + pos_num = paddle.sum(gts == 1, dtype='float32') + neg_num = paddle.sum(gts == 0, dtype='float32') + + weight_pos = neg_num / (pos_num + neg_num) + weight_neg = pos_num / (pos_num + neg_num) + weights = paddle.to_tensor([weight_neg, weight_pos]) + if self.top_k_percent_pixels == None: + sig_pred_logits = paddle.nn.functional.sigmoid(pred_logits) + self.bceloss = nn.BCEWithLogitsLoss(pos_weight=weight_pos, + reduction='mean') + if paddle.sum(gts) == 0: + dcloss = 0 + else: + dcloss = (paddle.sum(sig_pred_logits * sig_pred_logits) + + paddle.sum(gts * gts)) / ( + paddle.sum(2 * sig_pred_logits * gts) + 1e-5) + final_loss = 0.1 * self.bceloss(pred_logits, gts) + dcloss + else: + self.celoss = nn.CrossEntropyLoss(weight=weights, + ignore_index=255, + reduction='none') + num_pixels = float(pred_logits.shape[2] * pred_logits.shape[3]) + pred_logits = pred_logits.view( + -1, pred_logits.shape[1], + pred_logits.shape[2] * pred_logits.shape[3]) + gts = gts.view(-1, gts.shape[2] * gts.shape[3]) + pixel_losses = self.celoss(pred_logits, gts) + if self.hard_example_mining_step == 0: + top_k_pixels = int(self.top_k_percent_pixels * num_pixels) + else: + ratio = min(1.0, step / float(self.hard_example_mining_step)) + top_k_pixels = int((ratio * self.top_k_percent_pixels + + (1.0 - ratio)) * num_pixels) + top_k_loss, top_k_indices = paddle.topk(pixel_losses, + k=top_k_pixels, + axis=1) + + final_loss = paddle.mean(top_k_loss) + return final_loss diff --git a/applications/Ma-Net/requirements.txt b/applications/Ma-Net/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..a9a6406f84db9b67c96914cec50ad84825d898b4 --- /dev/null +++ b/applications/Ma-Net/requirements.txt @@ -0,0 +1,20 @@ +Cython==0.29.15 +numpy==1.21.4 +astor==0.8.1 +certifi==2021.10.8 +charset-normalizer==2.0.7 +decorator==5.1.0 +idna==3.3 +mkl-fft==1.3.1 +opencv-python==4.2.0.32 +mkl-service==2.4.0 + +# paddlepaddle-gpu==2.2.0 +Pillow==8.4.0 +protobuf==3.19.1 +requests==2.26.0 + +urllib3==1.26.7 + + +davisinteractive==1.0.2 diff --git a/applications/Ma-Net/run.sh b/applications/Ma-Net/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..2d7d1e3e4c699d400a5c3fcf8eec11a756b0766d --- /dev/null +++ b/applications/Ma-Net/run.sh @@ -0,0 +1,15 @@ +PRETRAIN_MODEL='/home/lc/PaddleVideo/applications/Ma-Net/saved_model/DeeplabV3_coco.pdparams' +VOS_SAVE_RESULT_DIR='/home/lc/PaddleVideo/applications/Ma-Net/saved_model/MaNet_davis2017_stage1.pdparams' +#VOS_SAVE_RESULT_DIR='/home/lc/PaddleVideo/applications/Ma-Net/saved_model/stage1' +INT_SAVE_RESULT_DIR='/home/lc/PaddleVideo/applications/Ma-Net/saved_model/MANet_davis2017.pdparams' +#INT_SAVE_RESULT_DIR='/home/lc/PaddleVideo/applications/Ma-Net/saved_model/stage2' +INT_RESULT_DIR='/home/lc/PaddleVideo/applications/Ma-Net/saved_model/result' +RESCALE=416 +RANDOMCROP=416 +DATA_ROOT='/home/lc/PaddleVideo/data/DAVIS' +echo 'Stage1 training' +CUDA_VISIBLE_DEVICE=3 python train_stage1.py --SAVE_RESULT_DIR $VOS_SAVE_RESULT_DIR --PRETRAINED_MODEL $PRETRAIN_MODEL --DATA_ROOT $DATA_ROOT --TRAIN_BATCH_SIZE 2 --DATA_RESCALE $RESCALE --DATA_RANDOMCROP $RANDOMCROP --TRAIN_LR 0.0007 --MODEL_MAX_LOCAL_DISTANCE 12 +echo 'Stage2 training' +python train_stage2.py --SAVE_RESULT_DIR $INT_SAVE_RESULT_DIR --SAVE_VOS_RESULT_DIR $VOS_SAVE_RESULT_DIR --DATA_ROOT $DATA_ROOT --DATA_RESCALE $RESCALE --DATA_RANDOMCROP $RANDOMCROP --PRETRAINED_MODEL $PRETRAIN_MODEL +echo 'Testing' +python test.py --DATA_ROOT $DATA_ROOT --SAVE_RESULT_DIR $INT_SAVE_RESULT_DIR --RESULT_ROOT $INT_RESULT_DIR --MODEL_USEIntSeg False --TEST_MODE True diff --git a/applications/Ma-Net/test.py b/applications/Ma-Net/test.py new file mode 100644 index 0000000000000000000000000000000000000000..77df579219574074154e9998a0e98f4b1e989acf --- /dev/null +++ b/applications/Ma-Net/test.py @@ -0,0 +1,525 @@ +import cv2 +import os +import json + +import paddle +from PIL import Image +import timeit +import numpy as np +from paddle.vision import transforms + +from dataloaders.davis_2017_f import DAVIS2017_Feature_Extract +import dataloaders.custom_transforms_f as tr +from davisinteractive.session import DavisInteractiveSession +from networks.deeplab import DeepLab +from networks.IntVOS import IntVOS +import time +from davisinteractive.utils.scribbles import scribbles2mask, annotated_frames +from config import cfg +from paddle import nn +from paddle.io import DataLoader + +from utils.api import float_, byte_ + + +@paddle.no_grad() +def main(): + paddle.set_device("gpu:0") + total_frame_num_dic = {} + ################# + seqs = [] + with open(os.path.join(cfg.DATA_ROOT, 'ImageSets', '2017', + 'val' + '.txt')) as f: + seqs_tmp = f.readlines() + seqs_tmp = list(map(lambda elem: elem.strip(), seqs_tmp)) + seqs.extend(seqs_tmp) + h_w_dic = {} + for seq_name in seqs: + images = np.sort( + os.listdir( + os.path.join(cfg.DATA_ROOT, 'JPEGImages/480p/', + seq_name.strip()))) + total_frame_num_dic[seq_name] = len(images) + im_ = cv2.imread( + os.path.join(cfg.DATA_ROOT, 'JPEGImages/480p/', seq_name, + '00000.jpg')) + im_ = np.array(im_, dtype=np.float32) + hh_, ww_ = im_.shape[:2] + h_w_dic[seq_name] = (hh_, ww_) + _seq_list_file = os.path.join(cfg.DATA_ROOT, 'ImageSets', '2017', + 'v_a_l' + '_instances.txt') + seq_dict = json.load(open(_seq_list_file, 'r')) + ################## + seq_imgnum_dict_ = {} + seq_imgnum_dict = os.path.join(cfg.DATA_ROOT, 'ImageSets', '2017', + 'val_imgnum.txt') + if os.path.isfile(seq_imgnum_dict): + + seq_imgnum_dict_ = json.load(open(seq_imgnum_dict, 'r')) + else: + for seq in os.listdir(os.path.join(cfg.DATA_ROOT, 'JPEGImages/480p/')): + seq_imgnum_dict_[seq] = len( + os.listdir(os.path.join(cfg.DATA_ROOT, 'JPEGImages/480p/', + seq))) + with open(seq_imgnum_dict, 'w') as f: + json.dump(seq_imgnum_dict_, f) + + ################## + + is_save_image = False # Save the predicted masks + report_save_dir = cfg.RESULT_ROOT + save_res_dir = cfg.SAVE_RESULT_DIR # changed to path + if not os.path.exists(cfg.RESULT_ROOT): + os.makedirs(cfg.RESULT_ROOT) + # Configuration used in the challenges + max_nb_interactions = 8 # Maximum number of interactions + max_time_per_interaction = 30 # Maximum time per interaction per object + # Total time available to interact with a sequence and an initial set of scribbles + max_time = max_nb_interactions * max_time_per_interaction # Maximum time per object + # Interactive parameters + subset = 'val' + host = 'localhost' # 'localhost' for subsets train and val. + + feature_extracter = DeepLab(backbone='resnet', freeze_bn=False) + model = IntVOS(cfg, feature_extracter) + print('model loading...') + + saved_model_dict = save_res_dir + pretrained_dict = paddle.load(saved_model_dict) + load_network(model, pretrained_dict) + + print(f'model loading from {saved_model_dict} finished!') + model.eval() + inter_file = open(os.path.join(cfg.RESULT_ROOT, 'inter_file.txt'), 'w') + resized_h, resized_w = 480, 854 + ############################### + composed_transforms = transforms.Compose( + [tr.Resize((resized_h, resized_w)), + tr.ToTensor()]) + ############################### + + seen_seq = [] + n = 0 + max_n = 1 + with DavisInteractiveSession(host=host, + davis_root=cfg.DATA_ROOT, + subset=subset, + report_save_dir=report_save_dir, + max_nb_interactions=max_nb_interactions, + max_time=max_time, + metric_to_optimize='J') as sess: + while sess.next(): + t_total = timeit.default_timer() + # Get the current iteration scribbles + + sequence, scribbles, first_scribble = sess.get_scribbles( + only_last=True) + h, w = h_w_dic[sequence] + if 'prev_label_storage' not in locals().keys(): + prev_label_storage = paddle.zeros( + [104, h, w]) # because the maximum length of frames is 104. + print(sequence) + h, w = h_w_dic[sequence] + if len( + annotated_frames(scribbles) + ) == 0: # if no scribbles return, keep masks in previous round + + final_masks = prev_label_storage[:seq_imgnum_dict_[sequence]] + sess.submit_masks(final_masks.numpy()) + else: + + start_annotated_frame = annotated_frames(scribbles)[0] + + pred_masks = [] + pred_masks_reverse = [] + + if first_scribble: # If in the first round, initialize memories + n_interaction = 1 + eval_global_map_tmp_dic = {} + local_map_dics = ({}, {}) + total_frame_num = total_frame_num_dic[sequence] + obj_nums = seq_dict[sequence][-1] + + else: + n_interaction += 1 + ## + inter_file.write(sequence + ' ' + 'interaction' + + str(n_interaction) + ' ' + 'frame' + + str(start_annotated_frame) + '\n') + ## + + ##########################Reference image process + + if first_scribble: # if in the first round, extract pixel embbedings. + if sequence not in seen_seq: + inter_turn = 1 + + seen_seq.append(sequence) + embedding_memory = [] + test_dataset = DAVIS2017_Feature_Extract( + root=cfg.DATA_ROOT, + transform=composed_transforms, + seq_name=sequence) + testloader = DataLoader(test_dataset, + batch_size=14, + shuffle=False, + num_workers=cfg.NUM_WORKER) + for ii, sample in enumerate(testloader): + imgs = sample['img1'] + frame_embedding = model.extract_feature(imgs) + embedding_memory.append(frame_embedding) + del frame_embedding + + embedding_memory = paddle.concat(embedding_memory, 0) + _, _, emb_h, emb_w = embedding_memory.shape + ref_frame_embedding = embedding_memory[ + start_annotated_frame] + ref_frame_embedding = ref_frame_embedding.unsqueeze(0) + + else: + inter_turn += 1 + ref_frame_embedding = embedding_memory[ + start_annotated_frame] + ref_frame_embedding = ref_frame_embedding.unsqueeze(0) + + else: + ref_frame_embedding = embedding_memory[ + start_annotated_frame] + ref_frame_embedding = ref_frame_embedding.unsqueeze(0) + ######## + scribble_masks = scribbles2mask(scribbles, (emb_h, emb_w)) + scribble_label = scribble_masks[start_annotated_frame] + scribble_sample = {'scribble_label': scribble_label} + scribble_sample = tr.ToTensor()(scribble_sample) + # print(ref_frame_embedding, ref_frame_embedding.shape) + scribble_label = scribble_sample['scribble_label'] + + scribble_label = scribble_label.unsqueeze(0) + + ###### + if is_save_image: + ref_scribble_to_show = scribble_label.squeeze().numpy() + im_ = Image.fromarray( + ref_scribble_to_show.astype('uint8')).convert('P', ) + im_.putpalette(_palette) + ref_img_name = str(start_annotated_frame) + + if not os.path.exists( + os.path.join(cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn))): + os.makedirs( + os.path.join(cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn))) + im_.save( + os.path.join(cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn), + 'inter_' + ref_img_name + '.png')) + + scribble_label = scribble_label + + ####### + if first_scribble: + + prev_label = None + prev_label_storage = paddle.zeros([104, h, w]) + prev_label_storage = prev_label_storage + else: + prev_label = prev_label_storage[start_annotated_frame] + prev_label = prev_label.unsqueeze(0).unsqueeze(0) + if not first_scribble and paddle.unique( + scribble_label).shape[0] == 1: + final_masks = prev_label_storage[: + seq_imgnum_dict_[sequence]] + sess.submit_masks(final_masks.numpy()) + + else: ###inteaction segmentation head + print('inteaction segmentation head') + tmp_dic, local_map_dics = model.int_seghead( + ref_frame_embedding=ref_frame_embedding, + ref_scribble_label=scribble_label, + prev_round_label=prev_label, + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + frame_num=[start_annotated_frame], + first_inter=first_scribble) + pred_label = tmp_dic[sequence] + pred_label = nn.functional.interpolate(pred_label, + size=(h, w), + mode='bilinear', + align_corners=True) + pred_label = paddle.argmax(pred_label, axis=1) + pred_masks.append(float_(pred_label)) + prev_label_storage[start_annotated_frame] = float_( + pred_label[0]) + + if is_save_image: # save image + pred_label_to_save = pred_label.squeeze(0).numpy() + im = Image.fromarray( + pred_label_to_save.astype('uint8')).convert('P', ) + im.putpalette(_palette) + imgname = str(start_annotated_frame) + while len(imgname) < 5: + imgname = '0' + imgname + if not os.path.exists( + os.path.join(cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn))): + os.makedirs( + os.path.join(cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn))) + im.save( + os.path.join(cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn), + imgname + '.png')) + ####################################### + if first_scribble: + scribble_label = rough_ROI(scribble_label) + + ############################## + ref_prev_label = pred_label.unsqueeze(0) + prev_label = pred_label.unsqueeze(0) + prev_embedding = ref_frame_embedding + #### Propagation -> + for ii in range(start_annotated_frame + 1, total_frame_num): + current_embedding = embedding_memory[ii] + current_embedding = current_embedding.unsqueeze(0) + prev_label = prev_label + tmp_dic, eval_global_map_tmp_dic, local_map_dics = model.prop_seghead( + ref_frame_embedding, + prev_embedding, + current_embedding, + scribble_label, + prev_label, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + k_nearest_neighbors=cfg.KNNS, + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + start_annotated_frame=start_annotated_frame, + frame_num=[ii], + dynamic_seghead=model.dynamic_seghead) + pred_label = tmp_dic[sequence] + + pred_label = nn.functional.interpolate( + pred_label, + size=(h, w), + mode='bilinear', + align_corners=True) + + pred_label = paddle.argmax(pred_label, axis=1) + pred_masks.append(float_(pred_label)) + prev_label = pred_label.unsqueeze(0) + prev_embedding = current_embedding + prev_label_storage[ii] = float_(pred_label[0]) + #### + if is_save_image: + pred_label_to_save = pred_label.squeeze(0).numpy() + im = Image.fromarray( + pred_label_to_save.astype('uint8')).convert( + 'P', ) + im.putpalette(_palette) + imgname = str(ii) + while len(imgname) < 5: + imgname = '0' + imgname + if not os.path.exists( + os.path.join( + cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn))): + os.makedirs( + os.path.join( + cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn))) + im.save( + os.path.join(cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn), + imgname + '.png')) + ####################################### + prev_label = ref_prev_label + prev_embedding = ref_frame_embedding + ####### + # Propagation <- + for ii in range(start_annotated_frame): + current_frame_num = start_annotated_frame - 1 - ii + current_embedding = embedding_memory[current_frame_num] + current_embedding = current_embedding.unsqueeze(0) + prev_label = prev_label + tmp_dic, eval_global_map_tmp_dic, local_map_dics = model.prop_seghead( + ref_frame_embedding, + prev_embedding, + current_embedding, + scribble_label, + prev_label, + normalize_nearest_neighbor_distances=True, + use_local_map=True, + seq_names=[sequence], + gt_ids=paddle.to_tensor([obj_nums]), + k_nearest_neighbors=cfg.KNNS, + global_map_tmp_dic=eval_global_map_tmp_dic, + local_map_dics=local_map_dics, + interaction_num=n_interaction, + start_annotated_frame=start_annotated_frame, + frame_num=[current_frame_num], + dynamic_seghead=model.dynamic_seghead) + pred_label = tmp_dic[sequence] + pred_label = nn.functional.interpolate( + pred_label, + size=(h, w), + mode='bilinear', + align_corners=True) + + pred_label = paddle.argmax(pred_label, axis=1) + pred_masks_reverse.append(float_(pred_label)) + prev_label = pred_label.unsqueeze(0) + prev_embedding = current_embedding + #### + prev_label_storage[current_frame_num] = float_( + pred_label[0]) + ### + if is_save_image: + pred_label_to_save = pred_label.squeeze(0).numpy() + im = Image.fromarray( + pred_label_to_save.astype('uint8')).convert( + 'P', ) + im.putpalette(_palette) + imgname = str(current_frame_num) + while len(imgname) < 5: + imgname = '0' + imgname + if not os.path.exists( + os.path.join( + cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn))): + os.makedirs( + os.path.join( + cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn))) + im.save( + os.path.join(cfg.RESULT_ROOT, sequence, + 'interactive' + str(n_interaction), + 'turn' + str(inter_turn), + imgname + '.png')) + pred_masks_reverse.reverse() + pred_masks_reverse.extend(pred_masks) + final_masks = paddle.concat(pred_masks_reverse, 0) + sess.submit_masks(final_masks.numpy()) + + if inter_turn == 3 and n_interaction == 8: + del eval_global_map_tmp_dic + del local_map_dics + del embedding_memory + del prev_label_storage + t_end = timeit.default_timer() + print('Total time for single interaction: ' + str(t_end - t_total)) + report = sess.get_report() + summary = sess.get_global_summary( + save_file=os.path.join(report_save_dir, 'summary.json')) + inter_file.close() + + +def rough_ROI(ref_scribble_labels): + dist = 20 + b, _, h, w = ref_scribble_labels.shape + filter_ = paddle.zeros_like(ref_scribble_labels) + to_fill = paddle.zeros_like(ref_scribble_labels) + for i in range(b): + no_background = (ref_scribble_labels[i] != -1) + no_background = no_background.squeeze(0) + + no_b = no_background.nonzero() + (h_min, w_min) = paddle.min(no_b, 0) + (h_max, w_max) = paddle.max(no_b, 0) + + filter_[i, 0, + max(h_min - dist, 0):min(h_max + dist, h - 1), + max(w_min - dist, 0):min(w_max + dist, w - 1)] = 1 + + final_scribble_labels = paddle.where(byte_(filter_), ref_scribble_labels, + to_fill) + return final_scribble_labels + + +def load_network(net, pretrained_dict): + model_dict = net.state_dict() + # 1. filter out unnecessary keys + f_pretrained_dict = {} + for k, v in pretrained_dict.items(): + if k in model_dict: + f_pretrained_dict[k] = v + else: + print(k) + + print(len(model_dict.keys()), len(pretrained_dict.keys())) + + # 2. overwrite entries in the existing state dict + model_dict.update(pretrained_dict) + net.set_state_dict(model_dict) + + +_palette = [ + 0, 0, 0, 128, 0, 0, 0, 128, 0, 128, 128, 0, 0, 0, 128, 128, 0, 128, 0, 128, + 128, 128, 128, 128, 64, 0, 0, 191, 0, 0, 64, 128, 0, 191, 128, 0, 64, 0, + 128, 191, 0, 128, 64, 128, 128, 191, 128, 128, 0, 64, 0, 128, 64, 0, 0, 191, + 0, 128, 191, 0, 0, 64, 128, 128, 64, 128, 22, 22, 22, 23, 23, 23, 24, 24, + 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, + 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, + 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, + 43, 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 49, + 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, + 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, 61, 61, 61, 62, 62, + 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 68, 68, 68, + 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, 73, 73, 74, 74, 74, 75, + 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 80, 80, 80, 81, 81, + 81, 82, 82, 82, 83, 83, 83, 84, 84, 84, 85, 85, 85, 86, 86, 86, 87, 87, 87, + 88, 88, 88, 89, 89, 89, 90, 90, 90, 91, 91, 91, 92, 92, 92, 93, 93, 93, 94, + 94, 94, 95, 95, 95, 96, 96, 96, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, + 100, 100, 101, 101, 101, 102, 102, 102, 103, 103, 103, 104, 104, 104, 105, + 105, 105, 106, 106, 106, 107, 107, 107, 108, 108, 108, 109, 109, 109, 110, + 110, 110, 111, 111, 111, 112, 112, 112, 113, 113, 113, 114, 114, 114, 115, + 115, 115, 116, 116, 116, 117, 117, 117, 118, 118, 118, 119, 119, 119, 120, + 120, 120, 121, 121, 121, 122, 122, 122, 123, 123, 123, 124, 124, 124, 125, + 125, 125, 126, 126, 126, 127, 127, 127, 128, 128, 128, 129, 129, 129, 130, + 130, 130, 131, 131, 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, 135, + 135, 135, 136, 136, 136, 137, 137, 137, 138, 138, 138, 139, 139, 139, 140, + 140, 140, 141, 141, 141, 142, 142, 142, 143, 143, 143, 144, 144, 144, 145, + 145, 145, 146, 146, 146, 147, 147, 147, 148, 148, 148, 149, 149, 149, 150, + 150, 150, 151, 151, 151, 152, 152, 152, 153, 153, 153, 154, 154, 154, 155, + 155, 155, 156, 156, 156, 157, 157, 157, 158, 158, 158, 159, 159, 159, 160, + 160, 160, 161, 161, 161, 162, 162, 162, 163, 163, 163, 164, 164, 164, 165, + 165, 165, 166, 166, 166, 167, 167, 167, 168, 168, 168, 169, 169, 169, 170, + 170, 170, 171, 171, 171, 172, 172, 172, 173, 173, 173, 174, 174, 174, 175, + 175, 175, 176, 176, 176, 177, 177, 177, 178, 178, 178, 179, 179, 179, 180, + 180, 180, 181, 181, 181, 182, 182, 182, 183, 183, 183, 184, 184, 184, 185, + 185, 185, 186, 186, 186, 187, 187, 187, 188, 188, 188, 189, 189, 189, 190, + 190, 190, 191, 191, 191, 192, 192, 192, 193, 193, 193, 194, 194, 194, 195, + 195, 195, 196, 196, 196, 197, 197, 197, 198, 198, 198, 199, 199, 199, 200, + 200, 200, 201, 201, 201, 202, 202, 202, 203, 203, 203, 204, 204, 204, 205, + 205, 205, 206, 206, 206, 207, 207, 207, 208, 208, 208, 209, 209, 209, 210, + 210, 210, 211, 211, 211, 212, 212, 212, 213, 213, 213, 214, 214, 214, 215, + 215, 215, 216, 216, 216, 217, 217, 217, 218, 218, 218, 219, 219, 219, 220, + 220, 220, 221, 221, 221, 222, 222, 222, 223, 223, 223, 224, 224, 224, 225, + 225, 225, 226, 226, 226, 227, 227, 227, 228, 228, 228, 229, 229, 229, 230, + 230, 230, 231, 231, 231, 232, 232, 232, 233, 233, 233, 234, 234, 234, 235, + 235, 235, 236, 236, 236, 237, 237, 237, 238, 238, 238, 239, 239, 239, 240, + 240, 240, 241, 241, 241, 242, 242, 242, 243, 243, 243, 244, 244, 244, 245, + 245, 245, 246, 246, 246, 247, 247, 247, 248, 248, 248, 249, 249, 249, 250, + 250, 250, 251, 251, 251, 252, 252, 252, 253, 253, 253, 254, 254, 254, 255, + 255, 255 +] + +if __name__ == '__main__': + main() diff --git a/applications/Ma-Net/train_stage1.py b/applications/Ma-Net/train_stage1.py new file mode 100644 index 0000000000000000000000000000000000000000..fcc10ac3a196454f7692f704bdfddd9d9ebc3fb0 --- /dev/null +++ b/applications/Ma-Net/train_stage1.py @@ -0,0 +1,429 @@ +import cv2 +import paddle +import paddle.nn as nn +import os +import numpy as np +from paddle.io import DataLoader +import paddle.optimizer as optim +from paddle.vision import transforms +from dataloaders.davis_2017_f import DAVIS2017_VOS_Train, DAVIS2017_VOS_Test +import dataloaders.custom_transforms_f as tr +from dataloaders.samplers import RandomIdentitySampler +from networks.deeplab import DeepLab +from networks.IntVOS import IntVOS +from networks.loss import Added_BCEWithLogitsLoss, Added_CrossEntropyLoss +from config import cfg +from utils.api import float_, clip_grad_norm_, int_, long_ +from utils.meters import AverageMeter +from utils.mask_damaging import damage_masks +from utils.utils import label2colormap +from PIL import Image +import scipy.misc as sm +import time +# import logging +paddle.disable_static() + +paddle.device.set_device('gpu:0') + + +class Manager(object): + def __init__(self, + use_gpu=True, + time_budget=None, + save_result_dir=cfg.SAVE_RESULT_DIR, + pretrained=True, + interactive_test=False, + freeze_bn=False): + + self.save_res_dir = save_result_dir + self.time_budget = time_budget + self.feature_extracter = DeepLab(backbone='resnet', freeze_bn=freeze_bn) + if pretrained: + pretrained_dict = paddle.load(cfg.PRETRAINED_MODEL) + # pretrained_dict = np.load(cfg.PRETRAINED_MODEL, allow_pickle=True).item() + pretrained_dict = pretrained_dict['state_dict'] + self.load_network(self.feature_extracter, pretrained_dict) + print('load pretrained model successfully.') + self.model = IntVOS(cfg, self.feature_extracter) + self.use_gpu = use_gpu + if use_gpu: + self.model = self.model + + def train(self, + damage_initial_previous_frame_mask=True, + lossfunc='cross_entropy', + model_resume=False): + ################### + self.model.train() + running_loss = AverageMeter() + running_time = AverageMeter() + + param_list = [{ + 'params': self.model.feature_extracter.parameters() + }, { + 'params': self.model.semantic_embedding.parameters() + }, { + 'params': self.model.dynamic_seghead.parameters() + }] + + ######## + clip = paddle.nn.ClipGradByGlobalNorm( + clip_norm=cfg.TRAIN_CLIP_GRAD_NORM) + # clip = None + optimizer = optim.Momentum(parameters=param_list, + learning_rate=cfg.TRAIN_LR, + momentum=cfg.TRAIN_MOMENTUM, + weight_decay=cfg.TRAIN_WEIGHT_DECAY, + use_nesterov=True, + grad_clip=clip) + + self.param_list = param_list + + ################### + + composed_transforms = transforms.Compose([ + tr.RandomHorizontalFlip(cfg.DATA_RANDOMFLIP), + tr.RandomScale(), + tr.RandomCrop((cfg.DATA_RANDOMCROP, cfg.DATA_RANDOMCROP), 5), + tr.Resize(cfg.DATA_RESCALE), + tr.ToTensor() + ]) + print('dataset processing...') + train_dataset = DAVIS2017_VOS_Train(root=cfg.DATA_ROOT, + transform=composed_transforms) + + trainloader = DataLoader( + train_dataset, + collate_fn=None, + batch_size=cfg.TRAIN_BATCH_SIZE, + shuffle=True, + num_workers=8, + ) + print('dataset processing finished.') + if lossfunc == 'bce': + criterion = Added_BCEWithLogitsLoss(cfg.TRAIN_TOP_K_PERCENT_PIXELS, + cfg.TRAIN_HARD_MINING_STEP) + elif lossfunc == 'cross_entropy': + criterion = Added_CrossEntropyLoss(cfg.TRAIN_TOP_K_PERCENT_PIXELS, + cfg.TRAIN_HARD_MINING_STEP) + else: + print( + 'unsupported loss funciton. Please choose from [cross_entropy,bce]' + ) + + max_itr = cfg.TRAIN_TOTAL_STEPS + + step = 0 + + if model_resume: + saved_model_ = os.path.join(self.save_res_dir, cfg.TRAIN_RESUME_DIR) + + saved_model_ = paddle.load(saved_model_) + self.model = self.load_network(self.model, saved_model_) + step = int(cfg.RESUME_DIR.split('.')[0].split('_')[-1]) + print('resume from step {}'.format(step)) + + while step < cfg.TRAIN_TOTAL_STEPS: + if step > 100001: + break + t1 = time.time() + if step > 0: + running_time.update(time.time() - t1) + print( + f'{time.asctime()}: new epoch starts. last epoch time: {running_time.avg:.3f} s.', + ) + + for ii, sample in enumerate(trainloader): + now_lr = self._adjust_lr(optimizer, step, max_itr) + + if step >= max_itr: + step += 1 + break + + ref_imgs = sample['ref_img'] # batch_size * 3 * h * w + img1s = sample['img1'] + img2s = sample['img2'] + ref_scribble_labels = sample[ + 'ref_scribble_label'] # batch_size * 1 * h * w + label1s = sample['label1'] + label2s = sample['label2'] + seq_names = sample['meta']['seq_name'] + obj_nums = sample['meta']['obj_num'] + + bs, _, h, w = img2s.shape + inputs = paddle.concat((ref_imgs, img1s, img2s), 0) + if damage_initial_previous_frame_mask: + try: + label1s = damage_masks(label1s) + except: + label1s = label1s + print('damage_error') + + ########## + if self.use_gpu: + inputs = inputs + ref_scribble_labels = ref_scribble_labels + label1s = label1s + label2s = label2s + + ########## + + tmp_dic = self.model(inputs, + ref_scribble_labels, + label1s, + use_local_map=True, + seq_names=seq_names, + gt_ids=obj_nums, + k_nearest_neighbors=cfg.KNNS) + + label_and_obj_dic = {} + label_dic = {} + for i, seq_ in enumerate(seq_names): + label_and_obj_dic[seq_] = (label2s[i], obj_nums[i]) + for seq_ in tmp_dic.keys(): + tmp_pred_logits = tmp_dic[seq_] + tmp_pred_logits = nn.functional.interpolate( + tmp_pred_logits, + size=(h, w), + mode='bilinear', + align_corners=True) + tmp_dic[seq_] = tmp_pred_logits + + label_tmp, obj_num = label_and_obj_dic[seq_] + obj_ids = np.arange(1, obj_num + 1) + obj_ids = paddle.to_tensor(obj_ids) + obj_ids = int_(obj_ids) + if lossfunc == 'bce': + label_tmp = label_tmp.transpose([1, 2, 0]) + label = (float_(label_tmp) == float_(obj_ids)) + label = label.unsqueeze(-1).transpose([3, 2, 0, 1]) + label_dic[seq_] = float_(label) + elif lossfunc == 'cross_entropy': + label_dic[seq_] = long_(label_tmp) + + loss = criterion(tmp_dic, label_dic, step) + loss = loss / bs + optimizer.clear_grad() + loss.backward() + + optimizer.step() + + running_loss.update(loss.item(), bs) + ##############Visulization during training + if step % 50 == 0: + print(time.asctime(), end='\t') + log = 'step:{},now_lr:{} ,loss:{:.4f}({:.4f})'.format( + step, now_lr, running_loss.val, running_loss.avg) + print(log) + # logging.info(log) + + show_ref_img = ref_imgs.numpy()[0] + show_img1 = img1s.numpy()[0] + show_img2 = img2s.numpy()[0] + + mean = np.array([[[0.485]], [[0.456]], [[0.406]]]) + sigma = np.array([[[0.229]], [[0.224]], [[0.225]]]) + + show_ref_img = show_ref_img * sigma + mean + show_img1 = show_img1 * sigma + mean + show_img2 = show_img2 * sigma + mean + + show_gt = label2s[0] + + show_gt = show_gt.squeeze(0).numpy() + show_gtf = label2colormap(show_gt).transpose((2, 0, 1)) + + show_preds = tmp_dic[seq_names[0]] + show_preds = nn.functional.interpolate(show_preds, + size=(h, w), + mode='bilinear', + align_corners=True) + show_preds = show_preds.squeeze(0) + if lossfunc == 'bce': + show_preds = (paddle.nn.functional.sigmoid(show_preds) > + 0.5) + show_preds_s = paddle.zeros((h, w)) + for i in range(show_preds.size(0)): + show_preds_s[show_preds[i]] = i + 1 + elif lossfunc == 'cross_entropy': + show_preds_s = paddle.argmax(show_preds, axis=0) + show_preds_s = show_preds_s.numpy() + show_preds_sf = label2colormap(show_preds_s).transpose( + (2, 0, 1)) + + pix_acc = np.sum(show_preds_s == show_gt) / (h * w) + + ###########TODO + if step % 20000 == 0 and step != 0: + self.save_network(self.model, step) + + step += 1 + + def test_VOS(self, use_gpu=True): + seqs = [] + + with open( + os.path.join(cfg.DATA_ROOT, 'ImageSets', '2017', + 'val' + '.txt')) as f: + seqs_tmp = f.readlines() + seqs_tmp = list(map(lambda elem: elem.strip(), seqs_tmp)) + seqs.extend(seqs_tmp) + print('model loading...') + saved_model_dict = os.path.join(self.save_res_dir, cfg.TEST_CHECKPOINT) + pretrained_dict = paddle.load(saved_model_dict) + self.model = self.load_network(self.model, pretrained_dict) + print('model load finished') + + self.model.eval() + with paddle.no_grad(): + for seq_name in seqs: + print('prcessing seq:{}'.format(seq_name)) + test_dataset = DAVIS2017_VOS_Test(root=cfg.DATA_ROOT, + transform=tr.ToTensor(), + result_root=cfg.RESULT_ROOT, + seq_name=seq_name) + test_dataloader = DataLoader(test_dataset, + batch_size=1, + shuffle=False, + num_workers=0) + if not os.path.exists(os.path.join(cfg.RESULT_ROOT, seq_name)): + os.makedirs(os.path.join(cfg.RESULT_ROOT, seq_name)) + time_start = time.time() + for ii, sample in enumerate(test_dataloader): + ref_img = sample['ref_img'] + prev_img = sample['prev_img'] + current_img = sample['current_img'] + ref_label = sample['ref_label'] + prev_label = sample['prev_label'] + obj_num = sample['meta']['obj_num'] + seqnames = sample['meta']['seq_name'] + imgname = sample['meta']['current_name'] + bs, _, h, w = current_img.shape + + inputs = paddle.concat((ref_img, prev_img, current_img), 0) + if use_gpu: + inputs = inputs + ref_label = ref_label + prev_label = prev_label + + ################ + t1 = time.time() + tmp = self.model.extract_feature(inputs) + ref_frame_embedding, previous_frame_embedding, current_frame_embedding = paddle.split( + tmp, num_or_sections=3, axis=0) + t2 = time.time() + print('feature_extracter time:{}'.format(t2 - t1)) + tmp_dic = self.model.prop_seghead( + ref_frame_embedding, previous_frame_embedding, + current_frame_embedding, ref_label, prev_label, True, + seqnames, obj_num, cfg.KNNS, self.model.dynamic_seghead) + t3 = time.time() + print('after time:{}'.format(t3 - t2)) + + ####################### + pred_label = tmp_dic[seq_name] + pred_label = nn.functional.interpolate(pred_label, + size=(h, w), + mode='bilinear', + align_corners=True) + + pred_label = paddle.argmax(pred_label, axis=1) + pred_label = pred_label.squeeze(0) + pred_label = pred_label.numpy() + im = Image.fromarray(pred_label.astype('uint8')).convert( + 'P', ) + im.putpalette(_palette) + im.save( + os.path.join(cfg.RESULT_ROOT, seq_name, + imgname[0].split('.')[0] + '.png')) + one_frametime = time.time() + print('seq name:{} frame:{} time:{}'.format( + seq_name, imgname[0], one_frametime - time_start)) + time_start = time.time() + + def load_network(self, net, pretrained_dict): + + # pretrained_dict = pretrained_dict + model_dict = net.state_dict() + # 1. filter out unnecessary keys + pretrained_dict = { + k: v + for k, v in pretrained_dict.items() if k in model_dict + } + # 2. overwrite entries in the existing state dict + # for k in model_dict: + # if k not in pretrained_dict: + # print(k, 'not in loaded weights.') + + model_dict.update(pretrained_dict) + net.set_state_dict(model_dict) + return net + + def save_network(self, net, step): + save_path = self.save_res_dir + + if not os.path.exists(save_path): + os.makedirs(save_path) + save_file = 'save_step_%s.pth' % (step) + paddle.save(net.state_dict(), os.path.join(save_path, save_file)) + + def _adjust_lr(self, optimizer, itr, max_itr): + now_lr = cfg.TRAIN_LR * (1 - itr / (max_itr + 1))**cfg.TRAIN_POWER + optimizer._param_groups[0]['lr'] = now_lr + return now_lr + + +_palette = [ + 0, 0, 0, 128, 0, 0, 0, 128, 0, 128, 128, 0, 0, 0, 128, 128, 0, 128, 0, 128, + 128, 128, 128, 128, 64, 0, 0, 191, 0, 0, 64, 128, 0, 191, 128, 0, 64, 0, + 128, 191, 0, 128, 64, 128, 128, 191, 128, 128, 0, 64, 0, 128, 64, 0, 0, 191, + 0, 128, 191, 0, 0, 64, 128, 128, 64, 128, 22, 22, 22, 23, 23, 23, 24, 24, + 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, + 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, + 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, + 43, 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 49, + 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, + 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, 61, 61, 61, 62, 62, + 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 68, 68, 68, + 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, 73, 73, 74, 74, 74, 75, + 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 80, 80, 80, 81, 81, + 81, 82, 82, 82, 83, 83, 83, 84, 84, 84, 85, 85, 85, 86, 86, 86, 87, 87, 87, + 88, 88, 88, 89, 89, 89, 90, 90, 90, 91, 91, 91, 92, 92, 92, 93, 93, 93, 94, + 94, 94, 95, 95, 95, 96, 96, 96, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, + 100, 100, 101, 101, 101, 102, 102, 102, 103, 103, 103, 104, 104, 104, 105, + 105, 105, 106, 106, 106, 107, 107, 107, 108, 108, 108, 109, 109, 109, 110, + 110, 110, 111, 111, 111, 112, 112, 112, 113, 113, 113, 114, 114, 114, 115, + 115, 115, 116, 116, 116, 117, 117, 117, 118, 118, 118, 119, 119, 119, 120, + 120, 120, 121, 121, 121, 122, 122, 122, 123, 123, 123, 124, 124, 124, 125, + 125, 125, 126, 126, 126, 127, 127, 127, 128, 128, 128, 129, 129, 129, 130, + 130, 130, 131, 131, 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, 135, + 135, 135, 136, 136, 136, 137, 137, 137, 138, 138, 138, 139, 139, 139, 140, + 140, 140, 141, 141, 141, 142, 142, 142, 143, 143, 143, 144, 144, 144, 145, + 145, 145, 146, 146, 146, 147, 147, 147, 148, 148, 148, 149, 149, 149, 150, + 150, 150, 151, 151, 151, 152, 152, 152, 153, 153, 153, 154, 154, 154, 155, + 155, 155, 156, 156, 156, 157, 157, 157, 158, 158, 158, 159, 159, 159, 160, + 160, 160, 161, 161, 161, 162, 162, 162, 163, 163, 163, 164, 164, 164, 165, + 165, 165, 166, 166, 166, 167, 167, 167, 168, 168, 168, 169, 169, 169, 170, + 170, 170, 171, 171, 171, 172, 172, 172, 173, 173, 173, 174, 174, 174, 175, + 175, 175, 176, 176, 176, 177, 177, 177, 178, 178, 178, 179, 179, 179, 180, + 180, 180, 181, 181, 181, 182, 182, 182, 183, 183, 183, 184, 184, 184, 185, + 185, 185, 186, 186, 186, 187, 187, 187, 188, 188, 188, 189, 189, 189, 190, + 190, 190, 191, 191, 191, 192, 192, 192, 193, 193, 193, 194, 194, 194, 195, + 195, 195, 196, 196, 196, 197, 197, 197, 198, 198, 198, 199, 199, 199, 200, + 200, 200, 201, 201, 201, 202, 202, 202, 203, 203, 203, 204, 204, 204, 205, + 205, 205, 206, 206, 206, 207, 207, 207, 208, 208, 208, 209, 209, 209, 210, + 210, 210, 211, 211, 211, 212, 212, 212, 213, 213, 213, 214, 214, 214, 215, + 215, 215, 216, 216, 216, 217, 217, 217, 218, 218, 218, 219, 219, 219, 220, + 220, 220, 221, 221, 221, 222, 222, 222, 223, 223, 223, 224, 224, 224, 225, + 225, 225, 226, 226, 226, 227, 227, 227, 228, 228, 228, 229, 229, 229, 230, + 230, 230, 231, 231, 231, 232, 232, 232, 233, 233, 233, 234, 234, 234, 235, + 235, 235, 236, 236, 236, 237, 237, 237, 238, 238, 238, 239, 239, 239, 240, + 240, 240, 241, 241, 241, 242, 242, 242, 243, 243, 243, 244, 244, 244, 245, + 245, 245, 246, 246, 246, 247, 247, 247, 248, 248, 248, 249, 249, 249, 250, + 250, 250, 251, 251, 251, 252, 252, 252, 253, 253, 253, 254, 254, 254, 255, + 255, 255 +] + +manager = Manager() + +manager.train() diff --git a/applications/Ma-Net/train_stage2.py b/applications/Ma-Net/train_stage2.py new file mode 100644 index 0000000000000000000000000000000000000000..0f1d0f4916d142f63185037f6d0c2bb1ec1a1273 --- /dev/null +++ b/applications/Ma-Net/train_stage2.py @@ -0,0 +1,612 @@ +import cv2 +import paddle +import paddle.nn as nn +import os +import numpy as np +# from paddle.io import DataLoader +import paddle.optimizer as optim +from paddle.vision import transforms +from dataloaders.davis_2017_f import DAVIS2017_Train +import dataloaders.custom_transforms_f as tr +from dataloaders.samplers import RandomIdentitySampler +from networks.deeplab import DeepLab +from networks.IntVOS import IntVOS +from networks.loss import Added_BCEWithLogitsLoss, Added_CrossEntropyLoss +from config import cfg +from utils.api import float_, long_, byte_ +from utils.meters import AverageMeter +from utils.mask_damaging import damage_masks, mask_damager +from utils.utils import label2colormap +from PIL import Image +import random +import scipy.misc as sm +import time +import davisinteractive.robot.interactive_robot as interactive_robot + +paddle.disable_static() +paddle.device.set_device("gpu:0") + + +class DataLoader(paddle.io.DataLoader): + def __init__(self, + dataset, + batch_size=1, + shuffle=False, + sampler=None, + batch_sampler=None, + num_workers=0, + collate_fn=None, + pin_memory=False, + drop_last=False, + timeout=0, + worker_init_fn=None, + multiprocessing_context=None, + generator=None): + if isinstance(dataset[0], (tuple, list)): + return_list = True + else: + return_list = False + + super().__init__(dataset, + feed_list=None, + places=None, + return_list=return_list, + batch_sampler=batch_sampler, + batch_size=batch_size, + shuffle=shuffle, + drop_last=drop_last, + collate_fn=collate_fn, + num_workers=num_workers, + use_buffer_reader=True, + use_shared_memory=False, + timeout=timeout, + worker_init_fn=worker_init_fn) + if sampler is not None: + self.batch_sampler.sampler = sampler + + +class Manager(object): + def __init__(self, + use_gpu=True, + time_budget=None, + save_result_dir=cfg.SAVE_RESULT_DIR, + pretrained=True, + interactive_test=False): + + self.save_res_dir = save_result_dir + self.time_budget = time_budget + self.feature_extracter = DeepLab(backbone='resnet') + + if pretrained: + pretrained_dict = paddle.load(cfg.PRETRAINED_MODEL) + pretrained_dict = pretrained_dict['state_dict'] + self.load_network(self.feature_extracter, pretrained_dict) + print('load pretrained model successfully.') + self.model = IntVOS(cfg, self.feature_extracter) + model_filename = cfg.SAVE_VOS_RESULT_DIR + pd = paddle.load(model_filename) + + self.load_network(self.model, pd) + + print('load stage 1 model from', model_filename) + self.use_gpu = use_gpu + if use_gpu: + self.model = self.model + + ################################## + def train(self, + damage_initial_previous_frame_mask=True, + lossfunc='cross_entropy', + model_resume=False, + eval_total=False, + init_prev=False): + ################### + interactor = interactive_robot.InteractiveScribblesRobot() + self.model.train() + running_loss = AverageMeter() + optimizer = optim.Momentum(parameters=[{ + 'params': + self.model.inter_seghead.parameters() + }], + learning_rate=cfg.TRAIN_LR, + momentum=cfg.TRAIN_MOMENTUM, + weight_decay=cfg.TRAIN_WEIGHT_DECAY) + + ################### + + composed_transforms = transforms.Compose([ + tr.RandomHorizontalFlip(cfg.DATA_RANDOMFLIP), + tr.RandomScale(), + tr.RandomCrop((cfg.DATA_RANDOMCROP, cfg.DATA_RANDOMCROP), 10), + tr.Resize(cfg.DATA_RESCALE), + tr.ToTensor() + ]) + print('dataset processing...') + train_dataset = DAVIS2017_Train(root=cfg.DATA_ROOT, + transform=composed_transforms) + train_list = train_dataset.seqs + + print('dataset processing finished.') + if lossfunc == 'bce': + criterion = Added_BCEWithLogitsLoss(cfg.TRAIN_TOP_K_PERCENT_PIXELS, + cfg.TRAIN_HARD_MINING_STEP) + elif lossfunc == 'cross_entropy': + criterion = Added_CrossEntropyLoss(cfg.TRAIN_TOP_K_PERCENT_PIXELS, + cfg.TRAIN_HARD_MINING_STEP) + else: + print( + 'unsupported loss funciton. Please choose from [cross_entropy,bce]' + ) + + max_itr = cfg.TRAIN_TOTAL_STEPS + + step = 0 + round_ = 3 + epoch_per_round = 30 + if model_resume: + saved_model_ = os.path.join(self.save_res_dir, + 'save_step_75000.pth') + + saved_model_ = paddle.load(saved_model_) + self.model = self.load_network(self.model, saved_model_) + step = 75000 + print('resume from step {}'.format(step)) + + while step < cfg.TRAIN_TOTAL_STEPS: + + if step > 80001: + break + + for r in range(round_): + if r == 0: #### r==0: Train the interaction branch in the first round + print('start new') + global_map_tmp_dic = {} + train_dataset.transform = transforms.Compose([ + tr.RandomHorizontalFlip(cfg.DATA_RANDOMFLIP), + tr.RandomScale(), + tr.RandomCrop( + (cfg.DATA_RANDOMCROP, cfg.DATA_RANDOMCROP)), + tr.Resize(cfg.DATA_RESCALE), + tr.ToTensor() + ]) + train_dataset.init_ref_frame_dic() + + trainloader = DataLoader(train_dataset, + sampler=RandomIdentitySampler( + train_dataset.sample_list), + shuffle=False, + batch_size=cfg.TRAIN_BATCH_SIZE, + num_workers=0) + print('round:{} start'.format(r)) + print(len(train_dataset)) + print(len(trainloader)) + + for epoch in range(epoch_per_round): + + for ii, sample in enumerate(trainloader): + now_lr = self._adjust_lr(optimizer, step, max_itr) + ref_imgs = sample['ref_img'] # batch_size * 3 * h * w + ref_scribble_labels = sample[ + 'ref_scribble_label'] # batch_size * 1 * h * w + seq_names = sample['meta']['seq_name'] + obj_nums = sample['meta']['obj_num'] + ref_frame_nums = sample['meta']['ref_frame_num'] + ref_frame_gts = sample['ref_frame_gt'] + bs, _, h, w = ref_imgs.shape + ########## + if self.use_gpu: + inputs = ref_imgs + + ref_scribble_labels = ref_scribble_labels + ref_frame_gts = ref_frame_gts + ########## + with paddle.no_grad(): + self.model.feature_extracter.eval() + self.model.semantic_embedding.eval() + ref_frame_embedding = self.model.extract_feature( + inputs) + if r == 0: + first_inter = True + + tmp_dic = self.model.int_seghead( + ref_frame_embedding=ref_frame_embedding, + ref_scribble_label=ref_scribble_labels, + prev_round_label=None, + normalize_nearest_neighbor_distances=True, + global_map_tmp_dic={}, + seq_names=seq_names, + gt_ids=obj_nums, + k_nearest_neighbors=cfg.KNNS, + frame_num=ref_frame_nums, + first_inter=first_inter) + else: + first_inter = False + prev_round_label = sample['prev_round_label'] + prev_round_label = prev_round_label + tmp_dic = self.model.int_seghead( + ref_frame_embedding=ref_frame_embedding, + ref_scribble_label=ref_scribble_labels, + prev_round_label=prev_round_label, + normalize_nearest_neighbor_distances=True, + global_map_tmp_dic={}, + seq_names=seq_names, + gt_ids=obj_nums, + k_nearest_neighbors=cfg.KNNS, + frame_num=ref_frame_nums, + first_inter=first_inter) + label_and_obj_dic = {} + label_dic = {} + for i, seq_ in enumerate(seq_names): + label_and_obj_dic[seq_] = (ref_frame_gts[i], + obj_nums[i]) + for seq_ in tmp_dic.keys(): + tmp_pred_logits = tmp_dic[seq_] + tmp_pred_logits = nn.functional.interpolate( + tmp_pred_logits, + size=(h, w), + mode='bilinear', + align_corners=True) + tmp_dic[seq_] = tmp_pred_logits + + label_tmp, obj_num = label_and_obj_dic[seq_] + obj_ids = np.arange(0, obj_num + 1) + obj_ids = paddle.to_tensor(obj_ids) + obj_ids = paddle.to_tensor(obj_ids, dtype='int64') + if lossfunc == 'bce': + label_tmp = label_tmp.permute(1, 2, 0) + label = (float_(label_tmp) == float_(obj_ids)) + label = label.unsqueeze(-1).permute(3, 2, 0, 1) + label_dic[seq_] = float_(label) + elif lossfunc == 'cross_entropy': + label_dic[seq_] = long_(label_tmp) + + loss = criterion(tmp_dic, label_dic, step) + loss = loss / bs + optimizer.clear_grad() + loss.backward() + optimizer.step() + + running_loss.update(loss.item(), bs) + if step % 50 == 0: + print( + 'step:{},now_lr:{} ,loss:{:.4f}({:.4f})'.format( + step, now_lr, running_loss.val, + running_loss.avg)) + + show_ref_img = ref_imgs.numpy()[0] + + mean = np.array([[[0.485]], [[0.456]], [[0.406]]]) + sigma = np.array([[[0.229]], [[0.224]], [[0.225]]]) + + show_ref_img = show_ref_img * sigma + mean + + show_gt = ref_frame_gts[0].squeeze(0).numpy() + show_gtf = label2colormap(show_gt).transpose( + (2, 0, 1)) + show_scrbble = ref_scribble_labels[0].squeeze( + 0).numpy() + show_scrbble = label2colormap( + show_scrbble).transpose((2, 0, 1)) + if r != 0: + show_prev_round_label = prev_round_label[ + 0].squeeze(0).numpy() + show_prev_round_label = label2colormap( + show_prev_round_label).transpose((2, 0, 1)) + else: + show_prev_round_label = np.zeros_like(show_gt) + + show_prev_round_label = label2colormap( + show_prev_round_label).transpose((2, 0, 1)) + + ########## + show_preds = tmp_dic[seq_names[0]] + show_preds = nn.functional.interpolate( + show_preds, + size=(h, w), + mode='bilinear', + align_corners=True) + show_preds = show_preds.squeeze(0) + if lossfunc == 'bce': + show_preds = show_preds[1:] + + show_preds = ( + paddle.nn.functional.sigmoid(show_preds) > + 0.5) + marker = paddle.argmax(show_preds, axis=0) + show_preds_s = paddle.zeros((h, w)) + for i in range(show_preds.size(0)): + tmp_mask = (marker + == i) & (show_preds[i] > 0.5) + show_preds_s[tmp_mask] = i + 1 + elif lossfunc == 'cross_entropy': + show_preds_s = paddle.argmax(show_preds, axis=0) + show_preds_s = show_preds_s.numpy() + show_preds_sf = label2colormap( + show_preds_s).transpose((2, 0, 1)) + + pix_acc = np.sum(show_preds_s == show_gt) / (h * w) + + ###########TODO + if step % 20000 == 0 and step != 0: + self.save_network(self.model, step) + + step += 1 + + print('trainset evaluating...') + print('*' * 100) + + if cfg.TRAIN_INTER_USE_TRUE_RESULT: + if r != round_ - 1: + if r == 0: + prev_round_label_dic = {} + self.model.eval() + with paddle.no_grad(): + round_scribble = {} + + frame_num_dic = {} + train_dataset.transform = transforms.Compose( + [tr.Resize(cfg.DATA_RESCALE), + tr.ToTensor()]) + trainloader = DataLoader( + train_dataset, + sampler=RandomIdentitySampler( + train_dataset.sample_list), + shuffle=False, + batch_size=1, + num_workers=0) + for ii, sample in enumerate(trainloader): + ref_imgs = sample[ + 'ref_img'] # batch_size * 3 * h * w + img1s = sample['img1'] + img2s = sample['img2'] + ref_scribble_labels = sample[ + 'ref_scribble_label'] # batch_size * 1 * h * w + label1s = sample['label1'] + label2s = sample['label2'] + seq_names = sample['meta']['seq_name'] + obj_nums = sample['meta']['obj_num'] + frame_nums = sample['meta']['frame_num'] + bs, _, h, w = img2s.shape + inputs = paddle.concat((ref_imgs, img1s, img2s), + 0) + if r == 0: + ref_scribble_labels = self.rough_ROI( + ref_scribble_labels) + print(seq_names[0]) + label1s_tocat = None + for i in range(bs): + l = label1s[i] + l = l.unsqueeze(0) + l = mask_damager(l, 0.0) + l = paddle.to_tensor(l) + + l = l.unsqueeze(0).unsqueeze(0) + + if label1s_tocat is None: + label1s_tocat = float_(l) + else: + label1s_tocat = paddle.concat( + (label1s_tocat, float_(l)), 0) + + label1s = label1s_tocat + if self.use_gpu: + inputs = inputs + ref_scribble_labels = ref_scribble_labels + label1s = label1s + + tmp_dic, global_map_tmp_dic = self.model( + inputs, + ref_scribble_labels, + label1s, + seq_names=seq_names, + gt_ids=obj_nums, + k_nearest_neighbors=cfg.KNNS, + global_map_tmp_dic=global_map_tmp_dic, + frame_num=frame_nums) + pred_label = tmp_dic[ + seq_names[0]].detach().cpu() + pred_label = nn.functional.interpolate( + pred_label, + size=(h, w), + mode='bilinear', + align_corners=True) + pred_label = paddle.argmax(pred_label, axis=1) + pred_label = pred_label.unsqueeze(0) + try: + pred_label = damage_masks(pred_label) + except: + pred_label = pred_label + pred_label = pred_label.squeeze(0) + round_scribble[ + seq_names[0]] = interactor.interact( + seq_names[0], pred_label.numpy(), + float_(label2s).squeeze(0).numpy(), + obj_nums) + frame_num_dic[seq_names[0]] = frame_nums[0] + pred_label = pred_label.unsqueeze(0) + img_ww = Image.open( + os.path.join(cfg.DATA_ROOT, + 'JPEGImages/480p/', + seq_names[0], '00000.jpg')) + img_ww = np.array(img_ww) + or_h, or_w = img_ww.shape[:2] + pred_label = paddle.nn.functional.interpolate( + float_(pred_label), (or_h, or_w), + mode='nearest') + prev_round_label_dic[ + seq_names[0]] = pred_label.squeeze(0) + train_dataset.update_ref_frame_and_label( + round_scribble, frame_num_dic, prev_round_label_dic) + + print(f'round {r}', 'trainset evaluating finished!') + print('*' * 100) + self.model.train() + print('updating ref frame and label') + + train_dataset.transform = composed_transforms + print('updating ref frame and label finished!') + + else: + if r != round_ - 1: + round_scribble = {} + + if r == 0: + prev_round_label_dic = {} + frame_num_dic = {} + train_dataset.transform = tr.ToTensor() + trainloader = DataLoader(train_dataset, + sampler=RandomIdentitySampler( + train_dataset.sample_list), + shuffle=False, + batch_size=1, + num_workers=0) + + self.model.eval() + with paddle.no_grad(): + for ii, sample in enumerate(trainloader): + ref_imgs = sample[ + 'ref_img'] # batch_size * 3 * h * w + img1s = sample['img1'] + img2s = sample['img2'] + ref_scribble_labels = sample[ + 'ref_scribble_label'] # batch_size * 1 * h * w + label1s = sample['label1'] + label2s = sample['label2'] + seq_names = sample['meta']['seq_name'] + obj_nums = sample['meta']['obj_num'] + frame_nums = sample['meta']['frame_num'] + bs, _, h, w = img2s.shape + + print(seq_names[0]) + label2s_ = mask_damager(label2s, 0.1) + round_scribble[ + seq_names[0]] = interactor.interact( + seq_names[0], + np.expand_dims(label2s_, axis=0), + float_(label2s).squeeze(0).numpy(), + obj_nums) + label2s__ = paddle.to_tensor(label2s_) + + frame_num_dic[seq_names[0]] = frame_nums[0] + prev_round_label_dic[seq_names[0]] = label2s__ + + print(f'round {r}', 'trainset evaluating finished!') + print('*' * 100) + print('updating ref frame and label') + + train_dataset.update_ref_frame_and_label( + round_scribble, frame_num_dic, prev_round_label_dic) + self.model.train() + train_dataset.transform = composed_transforms + print('updating ref frame and label finished!') + + ############################################# + + def rough_ROI(self, ref_scribble_labels): + #### b*1*h*w + dist = 15 + b, _, h, w = ref_scribble_labels.shape + filter_ = paddle.zeros_like(ref_scribble_labels) + to_fill = paddle.zeros_like(ref_scribble_labels) + for i in range(b): + no_background = (ref_scribble_labels[i] != -1) + no_background = no_background.squeeze(0) + + no_b = no_background.nonzero() + h_min, w_min = paddle.min(no_b, 0) # fixed + h_max, w_max = paddle.max(no_b, 0) # fixed + + filter_[i, 0, + max(h_min - dist, 0):min(h_max + dist, h - 1), + max(w_min - dist, 0):min(w_max + dist, w - 1)] = 1 + + final_scribble_labels = paddle.where(byte_(filter_), + ref_scribble_labels, + to_fill) # uint8_ fixed. + return final_scribble_labels + + def load_network(self, net, pretrained_dict): + + # pretrained_dict = pretrained_dict + model_dict = net.state_dict() + # 1. filter out unnecessary keys + pretrained_dict = { + k: v + for k, v in pretrained_dict.items() if k in model_dict + } + # 2. overwrite entries in the existing state dict + # for k in model_dict: + # if k not in pretrained_dict: + # print(k, 'not in loaded weights.') + + model_dict.update(pretrained_dict) + net.set_state_dict(model_dict) + return net + + def save_network(self, net, step): + save_path = self.save_res_dir + + if not os.path.exists(save_path): + os.makedirs(save_path) + save_file = 'save_step_%s.pth' % (step) + paddle.save(net.state_dict(), os.path.join(save_path, save_file)) + + def _adjust_lr(self, optimizer, itr, max_itr): + now_lr = cfg.TRAIN_LR * (1 - itr / (max_itr + 1))**cfg.TRAIN_POWER + optimizer._param_groups[0]['lr'] = now_lr + return now_lr + + +_palette = [ + 0, 0, 0, 128, 0, 0, 0, 128, 0, 128, 128, 0, 0, 0, 128, 128, 0, 128, 0, 128, + 128, 128, 128, 128, 64, 0, 0, 191, 0, 0, 64, 128, 0, 191, 128, 0, 64, 0, + 128, 191, 0, 128, 64, 128, 128, 191, 128, 128, 0, 64, 0, 128, 64, 0, 0, 191, + 0, 128, 191, 0, 0, 64, 128, 128, 64, 128, 22, 22, 22, 23, 23, 23, 24, 24, + 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, + 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, + 37, 37, 38, 38, 38, 39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, + 43, 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 49, + 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, + 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, 61, 61, 61, 62, 62, + 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, 67, 68, 68, 68, + 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, 73, 73, 74, 74, 74, 75, + 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 80, 80, 80, 81, 81, + 81, 82, 82, 82, 83, 83, 83, 84, 84, 84, 85, 85, 85, 86, 86, 86, 87, 87, 87, + 88, 88, 88, 89, 89, 89, 90, 90, 90, 91, 91, 91, 92, 92, 92, 93, 93, 93, 94, + 94, 94, 95, 95, 95, 96, 96, 96, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, + 100, 100, 101, 101, 101, 102, 102, 102, 103, 103, 103, 104, 104, 104, 105, + 105, 105, 106, 106, 106, 107, 107, 107, 108, 108, 108, 109, 109, 109, 110, + 110, 110, 111, 111, 111, 112, 112, 112, 113, 113, 113, 114, 114, 114, 115, + 115, 115, 116, 116, 116, 117, 117, 117, 118, 118, 118, 119, 119, 119, 120, + 120, 120, 121, 121, 121, 122, 122, 122, 123, 123, 123, 124, 124, 124, 125, + 125, 125, 126, 126, 126, 127, 127, 127, 128, 128, 128, 129, 129, 129, 130, + 130, 130, 131, 131, 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, 135, + 135, 135, 136, 136, 136, 137, 137, 137, 138, 138, 138, 139, 139, 139, 140, + 140, 140, 141, 141, 141, 142, 142, 142, 143, 143, 143, 144, 144, 144, 145, + 145, 145, 146, 146, 146, 147, 147, 147, 148, 148, 148, 149, 149, 149, 150, + 150, 150, 151, 151, 151, 152, 152, 152, 153, 153, 153, 154, 154, 154, 155, + 155, 155, 156, 156, 156, 157, 157, 157, 158, 158, 158, 159, 159, 159, 160, + 160, 160, 161, 161, 161, 162, 162, 162, 163, 163, 163, 164, 164, 164, 165, + 165, 165, 166, 166, 166, 167, 167, 167, 168, 168, 168, 169, 169, 169, 170, + 170, 170, 171, 171, 171, 172, 172, 172, 173, 173, 173, 174, 174, 174, 175, + 175, 175, 176, 176, 176, 177, 177, 177, 178, 178, 178, 179, 179, 179, 180, + 180, 180, 181, 181, 181, 182, 182, 182, 183, 183, 183, 184, 184, 184, 185, + 185, 185, 186, 186, 186, 187, 187, 187, 188, 188, 188, 189, 189, 189, 190, + 190, 190, 191, 191, 191, 192, 192, 192, 193, 193, 193, 194, 194, 194, 195, + 195, 195, 196, 196, 196, 197, 197, 197, 198, 198, 198, 199, 199, 199, 200, + 200, 200, 201, 201, 201, 202, 202, 202, 203, 203, 203, 204, 204, 204, 205, + 205, 205, 206, 206, 206, 207, 207, 207, 208, 208, 208, 209, 209, 209, 210, + 210, 210, 211, 211, 211, 212, 212, 212, 213, 213, 213, 214, 214, 214, 215, + 215, 215, 216, 216, 216, 217, 217, 217, 218, 218, 218, 219, 219, 219, 220, + 220, 220, 221, 221, 221, 222, 222, 222, 223, 223, 223, 224, 224, 224, 225, + 225, 225, 226, 226, 226, 227, 227, 227, 228, 228, 228, 229, 229, 229, 230, + 230, 230, 231, 231, 231, 232, 232, 232, 233, 233, 233, 234, 234, 234, 235, + 235, 235, 236, 236, 236, 237, 237, 237, 238, 238, 238, 239, 239, 239, 240, + 240, 240, 241, 241, 241, 242, 242, 242, 243, 243, 243, 244, 244, 244, 245, + 245, 245, 246, 246, 246, 247, 247, 247, 248, 248, 248, 249, 249, 249, 250, + 250, 250, 251, 251, 251, 252, 252, 252, 253, 253, 253, 254, 254, 254, 255, + 255, 255 +] + +manager = Manager() +manager.train() diff --git a/applications/Ma-Net/utils/__init__.py b/applications/Ma-Net/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/Ma-Net/utils/api.py b/applications/Ma-Net/utils/api.py new file mode 100644 index 0000000000000000000000000000000000000000..bb9d8434926b91054f8822db84197854dc8d8ac5 --- /dev/null +++ b/applications/Ma-Net/utils/api.py @@ -0,0 +1,857 @@ +import math +import warnings + +import numpy +import numpy as np +from numpy import inf +from paddle import Tensor, concat, reshape, nn +import paddle + +from typing import Union, Iterable + +_tensor_or_tensors = Union[paddle.Tensor, Iterable[paddle.Tensor]] + +import paddle +import PIL +import numbers +import numpy as np +from PIL import Image +from paddle.vision.transforms import BaseTransform +from paddle.vision.transforms import functional as F + + +def torch2paddle(data): + try: + import torch + if isinstance(data, dict): + np_data = {} + for k, v in data.items(): + np_data[k] = paddle.to_tensor(v.detach().numpy()) + return np_data + else: + return paddle.to_tensor(data.detach().numpy()) + except: + pass + + +def fill_(tensor: Tensor, value): + return tensor.set_value(paddle.full_like(tensor, value)) + + +def zero_(tensor: Tensor): + return tensor.set_value(paddle.zeros_like(tensor)) + + +def float_(tensor: Tensor): + return paddle.to_tensor(tensor, dtype='float32') + + +def long_(tensor: Tensor): + return paddle.to_tensor(tensor, dtype='int64') + + +def int_(tensor: Tensor): + return paddle.to_tensor(tensor, dtype='int32') + + +def byte_(tensor: Tensor): + return paddle.to_tensor(tensor, dtype='bool') + + +class ToPILImage(BaseTransform): + def __init__(self, mode=None, keys=None): + super(ToPILImage, self).__init__(keys) + + def _apply_image(self, pic): + """ + Args: + pic (Tensor|np.ndarray): Image to be converted to PIL Image. + Returns: + PIL: Converted image. + """ + if not (isinstance(pic, paddle.Tensor) or isinstance(pic, np.ndarray)): + raise TypeError('pic should be Tensor or ndarray. Got {}.'.format( + type(pic))) + + elif isinstance(pic, paddle.Tensor): + if pic.ndimension() not in {2, 3}: + raise ValueError( + 'pic should be 2/3 dimensional. Got {} dimensions.'.format( + pic.ndimension())) + + elif pic.ndimension() == 2: + # if 2D image, add channel dimension (CHW) + pic = pic.unsqueeze(0) + + elif isinstance(pic, np.ndarray): + if pic.ndim not in {2, 3}: + raise ValueError( + 'pic should be 2/3 dimensional. Got {} dimensions.'.format( + pic.ndim)) + + elif pic.ndim == 2: + # if 2D image, add channel dimension (HWC) + pic = np.expand_dims(pic, 2) + + npimg = pic + if isinstance(pic, paddle.Tensor) and "float" in str( + pic.numpy().dtype) and self.mode != 'F': + pic = pic.mul(255).byte() + if isinstance(pic, paddle.Tensor): + npimg = np.transpose(pic.numpy(), (1, 2, 0)) + + if not isinstance(npimg, np.ndarray): + raise TypeError( + 'Input pic must be a paddle.Tensor or NumPy ndarray, ' + + 'not {}'.format(type(npimg))) + + if npimg.shape[2] == 1: + expected_mode = None + npimg = npimg[:, :, 0] + if npimg.dtype == np.uint8: + expected_mode = 'L' + elif npimg.dtype == np.int16: + expected_mode = 'I;16' + elif npimg.dtype == np.int32: + expected_mode = 'I' + elif npimg.dtype == np.float32: + expected_mode = 'F' + if self.mode is not None and self.mode != expected_mode: + raise ValueError( + "Incorrect self.mode ({}) supplied for input type {}. Should be {}" + .format(self.mode, np.dtype, expected_mode)) + self.mode = expected_mode + + elif npimg.shape[2] == 2: + permitted_2_channel_modes = ['LA'] + if self.mode is not None and self.mode not in permitted_2_channel_modes: + raise ValueError( + "Only self.modes {} are supported for 2D inputs".format( + permitted_2_channel_modes)) + + if self.mode is None and npimg.dtype == np.uint8: + self.mode = 'LA' + + elif npimg.shape[2] == 4: + permitted_4_channel_modes = ['RGBA', 'CMYK', 'RGBX'] + if self.mode is not None and self.mode not in permitted_4_channel_modes: + raise ValueError( + "Only self.modes {} are supported for 4D inputs".format( + permitted_4_channel_modes)) + + if self.mode is None and npimg.dtype == np.uint8: + self.mode = 'RGBA' + else: + permitted_3_channel_modes = ['RGB', 'YCbCr', 'HSV'] + if self.mode is not None and self.mode not in permitted_3_channel_modes: + raise ValueError( + "Only self.modes {} are supported for 3D inputs".format( + permitted_3_channel_modes)) + if self.mode is None and npimg.dtype == np.uint8: + self.mode = 'RGB' + + if self.mode is None: + raise TypeError('Input type {} is not supported'.format( + npimg.dtype)) + + return Image.fromarray(npimg, mode=self.mode) + + +class Identity(nn.Layer): + r"""A placeholder identity operator that is argument-insensitive. + + Args: + args: any argument (unused) + kwargs: any keyword argument (unused) + """ + def __init__(self, *args, **kwargs): + super(Identity, self).__init__() + + def forward(self, input): + return input + + +def convert(data: dict, to, dtype=None): + assert isinstance(data, dict) + input = {} + for k, v in data.items(): + + if 'paddle' == to: + if isinstance(v, np.ndarray): + if dtype is not None: + input[k] = paddle.to_tensor(v.astype(dtype)) + else: + input[k] = paddle.to_tensor(v) + else: + input[k] = v + elif 'torch' == to: + try: + import torch + if isinstance(v, np.ndarray): + if dtype is not None: + input[k] = torch.tensor(v.astype(dtype)) + else: + input[k] = torch.tensor(v) + else: + input[k] = v + except: + pass + else: + if isinstance(v, np.ndarray): + input[k] = v.astype(to) + else: + input[k] = v + return input + + +def clip_grad_norm_(parameters: _tensor_or_tensors, + max_norm: float, + norm_type: float = 2.0, + error_if_nonfinite: bool = False) -> paddle.Tensor: + r"""Clips gradient norm of an iterable of parameters. + + The norm is computed over all gradients together, as if they were + concatenated into a single vector. Gradients are modified in-place. + + Args: + parameters (Iterable[Tensor] or Tensor): an iterable of Tensors or a + single Tensor that will have gradients normalized + max_norm (float or int): max norm of the gradients + norm_type (float or int): type of the used p-norm. Can be ``'inf'`` for + infinity norm. + error_if_nonfinite (bool): if True, an error is thrown if the total + norm of the gradients from :attr:``parameters`` is ``nan``, + ``inf``, or ``-inf``. Default: False (will switch to True in the future) + + Returns: + Total norm of the parameters (viewed as a single vector). + """ + import time + if isinstance(parameters, paddle.Tensor): + parameters = [parameters] + parameters = [p for p in parameters if p.grad is not None] + detached_grads = [p.grad.detach() for p in parameters] + + max_norm = float(max_norm) + norm_type = float(norm_type) + if len(parameters) == 0: + return paddle.to_tensor(0.) + if norm_type == inf: + norms = [p.abs().max() for p in parameters] + total_norm = norms[0] if len(norms) == 1 else paddle.max( + paddle.stack(norms)) + else: + total_norm = paddle.norm( + paddle.stack([paddle.norm(g, norm_type) for g in detached_grads]), + norm_type) + if error_if_nonfinite and paddle.logical_or(total_norm.isnan(), + total_norm.isinf()): + raise RuntimeError( + f'The total norm of order {norm_type} for gradients from ' + '`parameters` is non-finite, so it cannot be clipped. To disable ' + 'this error and scale the gradients by the non-finite norm anyway, ' + 'set `error_if_nonfinite=False`') + clip_coef = max_norm / (total_norm + 1e-6) + # Note: multiplying by the clamped coef is redundant when the coef is clamped to 1, but doing so + # avoids a `if clip_coef < 1:` conditional which can require a CPU <=> device synchronization + # when the gradients do not reside in CPU memory. + clip_coef_clamped = paddle.clip(clip_coef, max=1.0) + for i, p in enumerate(parameters): + p.grad.set_value(detached_grads[i] * clip_coef_clamped) # fixed + return total_norm + + +def max(a: paddle.Tensor, axis=0, keepdim=True): + """ndarray=numpy.array([[1, 2, 3, 4], + [4, 3, 2, 1], + [5, 6, 7, 8], + [8, 7, 6, 5]]) + np.where(ndarray == np.max(ndarray)) + (array([2, 3]), array([3, 0])) + ndarray[np.where(ndarray == np.max(ndarray))] + array([8, 8]) + """ + max_ = a.max(axis).unsqueeze(-1) + index = paddle.argmax(a, axis=axis, keepdim=keepdim) + max_ = max_.numpy() + index = index.numpy() + # index = paddle.argmax(a, axis=axis, keepdim=keepdim)[-1].flatten() + return max_, index + + +def gather(tmp: paddle.Tensor, ind: paddle.Tensor): + shape = tmp.shape + tmp = paddle.to_tensor(tmp) + ind = paddle.to_tensor(ind) + if len(shape) == 2: + b = shape[0] + return concat([ + reshape(paddle.gather(tmp[i, :], ind[i, :]), [1, -1]) + for i in range(b) + ], + axis=0) + elif len(shape) == 3: + out = [] + for i in range(tmp.shape[0]): + _ = paddle.index_sample(tmp[i], ind[i]) + out.append(_) + return paddle.to_tensor(out) + elif len(shape) == 4: + b, c, d = shape[:3] + return concat([ + reshape( + concat([ + reshape( + concat([ + reshape( + paddle.gather(tmp[i, j, k, :], ind[i, j, k, :]), + [1, -1]) for k in range(d) + ], + axis=0), [1, d, -1]) for j in range(c) + ], + axis=0), [1, c, d, -1]) for i in range(b) + ], + axis=0) + else: + pass + + +# These no_grad_* functions are necessary as wrappers around the parts of these +# functions that use `with torch.no_grad()`. The JIT doesn't support context +# managers, so these need to be implemented as builtins. Using these wrappers +# lets us keep those builtins small and re-usable. +def _no_grad_uniform_(tensor, a, b): + with paddle.no_grad(): + tensor.set_value(paddle.uniform(tensor.shape, min=a, max=b)) + return tensor + + +def _no_grad_normal_(tensor, mean, std): + with paddle.no_grad(): + tensor.set_value(paddle.normal(shape=tensor.shape, mean=mean, std=std)) + return tensor + + +def _no_grad_trunc_normal_(tensor, mean, std, a, b): + from scipy import special + + # Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf + def norm_cdf(x): + # Computes standard normal cumulative distribution function + return (1. + math.erf(x / math.sqrt(2.))) / 2. + + if (mean < a - 2 * std) or (mean > b + 2 * std): + warnings.warn( + "mean is more than 2 std from [a, b] in nn.init.trunc_normal_. " + "The distribution of values may be incorrect.", + stacklevel=2) + + with paddle.no_grad(): + # Values are generated by using a truncated uniform distribution and + # then using the inverse CDF for the normal distribution. + # Get upper and lower cdf values + l = norm_cdf((a - mean) / std) + u = norm_cdf((b - mean) / std) + + # Uniformly fill tensor with values from [l, u], then translate to + # [2l-1, 2u-1]. + tensor.set_value( + paddle.uniform(tensor.shape, min=2 * l - 1, max=2 * u - 1)) + # tensor.uniform_(2 * l - 1, 2 * u - 1) + + # Use inverse cdf transform for normal distribution to get truncated + # standard normal + tensor.set_value(special.erfinv(tensor)) + + # Transform to proper mean, std + tensor.set_value(tensor.multiply(paddle.to_tensor(std * math.sqrt(2.)))) + tensor.add_(mean) + + # Clamp to ensure it's in the proper range + tensor.clip_(min=a, max=b) + return tensor + + +def _no_grad_fill_(tensor, val): + with paddle.no_grad(): + tensor.set_value(paddle.full_like(tensor, fill_value=val)) + return tensor + + +def _no_grad_zero_(tensor): + with paddle.no_grad(): + tensor.set_value(paddle.zeros_like(tensor)) + return tensor + + +def calculate_gain(nonlinearity, param=None): + r"""Return the recommended gain value for the given nonlinearity function. + The values are as follows: + + ================= ==================================================== + nonlinearity gain + ================= ==================================================== + Linear / Identity :math:`1` + Conv{1,2,3}D :math:`1` + Sigmoid :math:`1` + Tanh :math:`\frac{5}{3}` + ReLU :math:`\sqrt{2}` + Leaky Relu :math:`\sqrt{\frac{2}{1 + \text{negative\_slope}^2}}` + SELU :math:`\frac{3}{4}` + ================= ==================================================== + + Args: + nonlinearity: the non-linear function (`nn.functional` name) + param: optional parameter for the non-linear function + + Examples: + >>> gain = nn.init.calculate_gain('leaky_relu', 0.2) # leaky_relu with negative_slope=0.2 + """ + linear_fns = [ + 'linear', 'conv1d', 'conv2d', 'conv3d', 'conv_transpose1d', + 'conv_transpose2d', 'conv_transpose3d' + ] + if nonlinearity in linear_fns or nonlinearity == 'sigmoid': + return 1 + elif nonlinearity == 'tanh': + return 5.0 / 3 + elif nonlinearity == 'relu': + return math.sqrt(2.0) + elif nonlinearity == 'leaky_relu': + if param is None: + negative_slope = 0.01 + elif not isinstance(param, bool) and isinstance( + param, int) or isinstance(param, float): + # True/False are instances of int, hence check above + negative_slope = param + else: + raise ValueError( + "negative_slope {} not a valid number".format(param)) + return math.sqrt(2.0 / (1 + negative_slope**2)) + elif nonlinearity == 'selu': + return 3.0 / 4 # Value found empirically (https://github.com/pytorch/pytorch/pull/50664) + else: + raise ValueError("Unsupported nonlinearity {}".format(nonlinearity)) + + +def uniform_(tensor: Tensor, a: float = 0., b: float = 1.) -> Tensor: + r"""Fills the input Tensor with values drawn from the uniform + distribution :math:`\mathcal{U}(a, b)`. + + Args: + tensor: an n-dimensional `torch.Tensor` + a: the lower bound of the uniform distribution + b: the upper bound of the uniform distribution + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.uniform_(w) + """ + return _no_grad_uniform_(tensor, a, b) + + +def normal_(tensor: Tensor, mean: float = 0., std: float = 1.) -> Tensor: + r"""Fills the input Tensor with values drawn from the normal + distribution :math:`\mathcal{N}(\text{mean}, \text{std}^2)`. + + Args: + tensor: an n-dimensional `torch.Tensor` + mean: the mean of the normal distribution + std: the standard deviation of the normal distribution + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.normal_(w) + """ + return _no_grad_normal_(tensor, mean, std) + + +def trunc_normal_(tensor: Tensor, + mean: float = 0., + std: float = 1., + a: float = -2., + b: float = 2.) -> Tensor: + r"""Fills the input Tensor with values drawn from a truncated + normal distribution. The values are effectively drawn from the + normal distribution :math:`\mathcal{N}(\text{mean}, \text{std}^2)` + with values outside :math:`[a, b]` redrawn until they are within + the bounds. The method used for generating the random values works + best when :math:`a \leq \text{mean} \leq b`. + + Args: + tensor: an n-dimensional `torch.Tensor` + mean: the mean of the normal distribution + std: the standard deviation of the normal distribution + a: the minimum cutoff value + b: the maximum cutoff value + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.trunc_normal_(w) + """ + return _no_grad_trunc_normal_(tensor, mean, std, a, b) + + +def constant_(tensor: Tensor, val: float) -> Tensor: + r"""Fills the input Tensor with the value :math:`\text{val}`. + + Args: + tensor: an n-dimensional `torch.Tensor` + val: the value to fill the tensor with + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.constant_(w, 0.3) + """ + return _no_grad_fill_(tensor, val) + + +def ones_(tensor: Tensor) -> Tensor: + r"""Fills the input Tensor with the scalar value `1`. + + Args: + tensor: an n-dimensional `torch.Tensor` + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.ones_(w) + """ + return _no_grad_fill_(tensor, 1.) + + +def zeros_(tensor: Tensor) -> Tensor: + r"""Fills the input Tensor with the scalar value `0`. + + Args: + tensor: an n-dimensional `torch.Tensor` + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.zeros_(w) + """ + return _no_grad_zero_(tensor) + + +def eye_(tensor): + r"""Fills the 2-dimensional input `Tensor` with the identity + matrix. Preserves the identity of the inputs in `Linear` layers, where as + many inputs are preserved as possible. + + Args: + tensor: a 2-dimensional `torch.Tensor` + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.eye_(w) + """ + if tensor.ndimension() != 2: + raise ValueError("Only tensors with 2 dimensions are supported") + + with paddle.no_grad(): + tensor.set_value(paddle.eye(*tensor.shape)) + return tensor + + +def dirac_(tensor, groups=1): + r"""Fills the {3, 4, 5}-dimensional input `Tensor` with the Dirac + delta function. Preserves the identity of the inputs in `Convolutional` + layers, where as many input channels are preserved as possible. In case + of groups>1, each group of channels preserves identity + + Args: + tensor: a {3, 4, 5}-dimensional `torch.Tensor` + groups (optional): number of groups in the conv layer (default: 1) + Examples: + >>> w = torch.empty(3, 16, 5, 5) + >>> nn.init.dirac_(w) + >>> w = torch.empty(3, 24, 5, 5) + >>> nn.init.dirac_(w, 3) + """ + dimensions = tensor.ndimension() + if dimensions not in [3, 4, 5]: + raise ValueError( + "Only tensors with 3, 4, or 5 dimensions are supported") + + sizes = tensor.shape + + if sizes[0] % groups != 0: + raise ValueError('dim 0 must be divisible by groups') + + out_chans_per_grp = sizes[0] // groups + min_dim = min(out_chans_per_grp, sizes[1]) + + with paddle.no_grad(): + tensor.zero_() + + for g in range(groups): + for d in range(min_dim): + if dimensions == 3: # Temporal convolution + tensor[g * out_chans_per_grp + d, d, + tensor.shape[2] // 2] = 1 + elif dimensions == 4: # Spatial convolution + tensor[g * out_chans_per_grp + d, d, tensor.shape[2] // 2, + tensor.shape[3] // 2] = 1 + else: # Volumetric convolution + tensor[g * out_chans_per_grp + d, d, tensor.shape[2] // 2, + tensor.shape[3] // 2, tensor.shape[4] // 2] = 1 + return tensor + + +def _calculate_fan_in_and_fan_out(tensor): + dimensions = tensor.dim() + if dimensions < 2: + raise ValueError( + "Fan in and fan out can not be computed for tensor with fewer than 2 dimensions" + ) + + num_input_fmaps = tensor.shape[1] # .size(1) + num_output_fmaps = tensor.shape[0] # .size(0) + receptive_field_size = 1 + if tensor.dim() > 2: + for s in tensor.shape[2:]: + receptive_field_size *= s # fixed + fan_in = num_input_fmaps * receptive_field_size + fan_out = num_output_fmaps * receptive_field_size + + return fan_in, fan_out + + +def LongTensor(x): + return paddle.to_tensor(x, dtype='int64') + + +def IntTensor(x): + return paddle.to_tensor(x, dtype='int32') + + +def xavier_uniform_(tensor: Tensor, gain: float = 1.) -> Tensor: + r"""Fills the input `Tensor` with values according to the method + described in `Understanding the difficulty of training deep feedforward + neural networks` - Glorot, X. & Bengio, Y. (2010), using a uniform + distribution. The resulting tensor will have values sampled from + :math:`\mathcal{U}(-a, a)` where + + .. math:: + a = \text{gain} \times \sqrt{\frac{6}{\text{fan\_in} + \text{fan\_out}}} + + Also known as Glorot initialization. + + Args: + tensor: an n-dimensional `torch.Tensor` + gain: an optional scaling factor + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.xavier_uniform_(w, gain=nn.init.calculate_gain('relu')) + """ + fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor) + std = gain * math.sqrt(2.0 / float(fan_in + fan_out)) + a = math.sqrt(3.0) * std # Calculate uniform bounds from standard deviation + + return _no_grad_uniform_(tensor, -a, a) + + +def xavier_normal_(tensor: Tensor, gain: float = 1.) -> Tensor: + r"""Fills the input `Tensor` with values according to the method + described in `Understanding the difficulty of training deep feedforward + neural networks` - Glorot, X. & Bengio, Y. (2010), using a normal + distribution. The resulting tensor will have values sampled from + :math:`\mathcal{N}(0, \text{std}^2)` where + + .. math:: + \text{std} = \text{gain} \times \sqrt{\frac{2}{\text{fan\_in} + \text{fan\_out}}} + + Also known as Glorot initialization. + + Args: + tensor: an n-dimensional `torch.Tensor` + gain: an optional scaling factor + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.xavier_normal_(w) + """ + fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor) + std = gain * math.sqrt(2.0 / float(fan_in + fan_out)) + + return _no_grad_normal_(tensor, 0., std) + + +def _calculate_correct_fan(tensor, mode): + mode = mode.lower() + valid_modes = ['fan_in', 'fan_out'] + if mode not in valid_modes: + raise ValueError("Mode {} not supported, please use one of {}".format( + mode, valid_modes)) + + fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor) + return fan_in if mode == 'fan_in' else fan_out + + +def kaiming_uniform_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'): + r"""Fills the input `Tensor` with values according to the method + described in `Delving deep into rectifiers: Surpassing human-level + performance on ImageNet classification` - He, K. et al. (2015), using a + uniform distribution. The resulting tensor will have values sampled from + :math:`\mathcal{U}(-\text{bound}, \text{bound})` where + + .. math:: + \text{bound} = \text{gain} \times \sqrt{\frac{3}{\text{fan\_mode}}} + + Also known as He initialization. + + Args: + tensor: an n-dimensional `torch.Tensor` + a: the negative slope of the rectifier used after this layer (only + used with ``'leaky_relu'``) + mode: either ``'fan_in'`` (default) or ``'fan_out'``. Choosing ``'fan_in'`` + preserves the magnitude of the variance of the weights in the + forward pass. Choosing ``'fan_out'`` preserves the magnitudes in the + backwards pass. + nonlinearity: the non-linear function (`nn.functional` name), + recommended to use only with ``'relu'`` or ``'leaky_relu'`` (default). + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.kaiming_uniform_(w, mode='fan_in', nonlinearity='relu') + """ + fan = _calculate_correct_fan(tensor, mode) + gain = calculate_gain(nonlinearity, a) + std = gain / math.sqrt(fan) + bound = math.sqrt( + 3.0) * std # Calculate uniform bounds from standard deviation + with paddle.no_grad(): + tensor.set_value(paddle.uniform(tensor.shape, min=-bound, max=bound)) + return tensor + + +def kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'): + r"""Fills the input `Tensor` with values according to the method + described in `Delving deep into rectifiers: Surpassing human-level + performance on ImageNet classification` - He, K. et al. (2015), using a + normal distribution. The resulting tensor will have values sampled from + :math:`\mathcal{N}(0, \text{std}^2)` where + + .. math:: + \text{std} = \frac{\text{gain}}{\sqrt{\text{fan\_mode}}} + + Also known as He initialization. + + Args: + tensor: an n-dimensional `torch.Tensor` + a: the negative slope of the rectifier used after this layer (only + used with ``'leaky_relu'``) + mode: either ``'fan_in'`` (default) or ``'fan_out'``. Choosing ``'fan_in'`` + preserves the magnitude of the variance of the weights in the + forward pass. Choosing ``'fan_out'`` preserves the magnitudes in the + backwards pass. + nonlinearity: the non-linear function (`nn.functional` name), + recommended to use only with ``'relu'`` or ``'leaky_relu'`` (default). + + Examples: + >>> w = torch.empty(3, 5) + >>> kaiming_normal_(w, mode='fan_out', nonlinearity='relu') + """ + fan = _calculate_correct_fan(tensor, mode) + gain = calculate_gain(nonlinearity, a) + std = gain / math.sqrt(fan) + with paddle.no_grad(): + tensor.set_value(paddle.normal(shape=tensor.shape, mean=0, std=std)) + return tensor + + +def orthogonal_(tensor, gain=1): + r"""Fills the input `Tensor` with a (semi) orthogonal matrix, as + described in `Exact solutions to the nonlinear dynamics of learning in deep + linear neural networks` - Saxe, A. et al. (2013). The input tensor must have + at least 2 dimensions, and for tensors with more than 2 dimensions the + trailing dimensions are flattened. + + Args: + tensor: an n-dimensional `torch.Tensor`, where :math:`n \geq 2` + gain: optional scaling factor + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.orthogonal_(w) + """ + if tensor.ndimension() < 2: + raise ValueError("Only tensors with 2 or more dimensions are supported") + + rows = tensor.shape[0] # .size(0) + cols = tensor.numel() // rows + flattened = tensor.new(rows, cols).normal_(0, 1) + + if rows < cols: + flattened.t_() + + # Compute the qr factorization + q, r = paddle.to_tensor(np.linalg.qr(flattened.numpy())) + # q, r = torch.qr(flattened) + # Make Q uniform according to https://arxiv.org/pdf/math-ph/0609050.pdf + d = paddle.diag(r, 0) + ph = d.sign() + q *= ph + + if rows < cols: + q.t_() + + with paddle.no_grad(): + tensor.view_as(q).copy_(q) + tensor.mul_(gain) + return tensor + + +def sparse_(tensor, sparsity, std=0.01): + r"""Fills the 2D input `Tensor` as a sparse matrix, where the + non-zero elements will be drawn from the normal distribution + :math:`\mathcal{N}(0, 0.01)`, as described in `Deep learning via + Hessian-free optimization` - Martens, J. (2010). + + Args: + tensor: an n-dimensional `torch.Tensor` + sparsity: The fraction of elements in each column to be set to zero + std: the standard deviation of the normal distribution used to generate + the non-zero values + + Examples: + >>> w = torch.empty(3, 5) + >>> nn.init.sparse_(w, sparsity=0.1) + """ + if tensor.ndimension() != 2: + raise ValueError("Only tensors with 2 dimensions are supported") + + rows, cols = tensor.shape + num_zeros = int(math.ceil(sparsity * rows)) + + with paddle.no_grad(): + tensor.normal_(0, std) + for col_idx in range(cols): + row_indices = paddle.randperm(rows) + zero_indices = row_indices[:num_zeros] + tensor[zero_indices, col_idx] = 0 + return tensor + + +# for backward compatibility +def _make_deprecate(meth): + new_name = meth.__name__ + old_name = new_name[:-1] + + def deprecated_init(*args, **kwargs): + warnings.warn( + "nn.init.{} is now deprecated in favor of nn.init.{}.".format( + old_name, new_name), + stacklevel=2) + return meth(*args, **kwargs) + + deprecated_init.__doc__ = r""" + {old_name}(...) + + .. warning:: + This method is now deprecated in favor of :func:`torch.nn.init.{new_name}`. + + See :func:`~torch.nn.init.{new_name}` for details.""".format( + old_name=old_name, new_name=new_name) + deprecated_init.__name__ = old_name + return deprecated_init diff --git a/applications/Ma-Net/utils/mask_damaging.py b/applications/Ma-Net/utils/mask_damaging.py new file mode 100644 index 0000000000000000000000000000000000000000..12480294b62dfbaea6d3aff07803feed0ceb706e --- /dev/null +++ b/applications/Ma-Net/utils/mask_damaging.py @@ -0,0 +1,170 @@ +import numpy as np +from scipy.ndimage import interpolation +try: + from skimage import morphology, transform +except ImportError as e: + print( + f"{e}, [scikit-image] package and it's dependencies is required for MA-Net." + ) +import paddle +import cv2 +import random + + +#### +def mask_damager(labels=None, p_black=0.2): + scales = (0.8, 1.0, 1.2) + kernel_size = random.randint(10, 15) + kernel = np.ones((kernel_size, kernel_size), np.uint8) + if random.random() < p_black: + final_label = paddle.zeros_like(labels) + final_label = final_label.squeeze().numpy() + else: + prot = random.randint(5, 15) + nrot = random.randint(-15, -5) + rots = [prot, nrot, 0] + rot = rots[random.randint(0, 2)] + + sc = scales[random.randint(0, 2)] + _, _, h, w = labels.shape + tmp = labels.squeeze() + + tmp = tmp.unsqueeze(-1) + tmp = tmp.numpy().astype(np.uint8) + morph_p = random.random() + if morph_p < 0.5: + tmp = cv2.morphologyEx(tmp, cv2.MORPH_OPEN, kernel) + else: + tmp = cv2.morphologyEx(tmp, cv2.MORPH_CLOSE, kernel) + + tmp = tmp.astype(np.uint8) + center = (w / 2, h / 2) + M = cv2.getRotationMatrix2D(center, rot, sc) + final_label = cv2.warpAffine(tmp, M, (w, h), cv2.INTER_NEAREST) + + return final_label + + +##### + + +def damage_masks(labels, shift=True, scale=True, rotate=True): + """ + Args: + labels: numpy array (batch_size * 1 * h * w) + """ + bs, _, h, w = labels.shape + labels = labels.transpose([0, 2, 3, 1]) + labels = labels.numpy() + final_label = [] + for i in range(bs): + label = labels[i] + damaged_label = damage_masks_np(label, shift, scale, rotate) + final_label.append(damaged_label) + final_label = np.array(final_label) + final_label = paddle.to_tensor(final_label) + final_label = final_label.transpose([0, 3, 1, 2]) + return final_label + + +def damage_masks_np(labels, shift=True, scale=True, rotate=True): + """Performs the actual mask damaging in numpy. + Args: + labels: Int32 numpy array of shape (height, width, 1). + shift: Boolean, whether to damage the masks by shifting. + scale: Boolean, whether to damage the masks by scaling. + rotate: Boolean, whether to damage the masks by rotation. + dilate: Boolean, whether to damage the masks by dilation. + Returns: + The damaged version of labels. + """ + unique_labels = np.unique(labels) + unique_labels = np.setdiff1d(unique_labels, [0]) + # Shuffle to get random depth ordering when combining together. + np.random.shuffle(unique_labels) + damaged_labels = np.zeros_like(labels) + for l in unique_labels: + obj_mask = (labels == l) + damaged_obj_mask = _damage_single_object_mask(obj_mask, shift, scale, + rotate) + damaged_labels[damaged_obj_mask] = l + return damaged_labels + + +def _damage_single_object_mask(mask, shift, scale, rotate): + """Performs mask damaging in numpy for a single object. + Args: + mask: Boolean numpy array of shape(height, width, 1). + shift: Boolean, whether to damage the masks by shifting. + scale: Boolean, whether to damage the masks by scaling. + rotate: Boolean, whether to damage the masks by rotation. + dilate: Boolean, whether to damage the masks by dilation. + Returns: + The damaged version of mask. + """ + if shift: + mask = _shift_mask(mask) + if scale: + mask = _scale_mask(mask) + if rotate: + mask = _rotate_mask(mask) + return mask + + +def _shift_mask(mask, max_shift_factor=0.05): + """Damages a mask for a single object by randomly shifting it in numpy. + Args: + mask: Boolean numpy array of shape(height, width, 1). + max_shift_factor: Float scalar, the maximum factor for random shifting. + Returns: + The shifted version of mask. + """ + nzy, nzx, _ = mask.nonzero() + h = nzy.max() - nzy.min() + w = nzx.max() - nzx.min() + size = np.sqrt(h * w) + offset = np.random.uniform(-size * max_shift_factor, + size * max_shift_factor, 2) + shifted_mask = interpolation.shift(np.squeeze(mask, axis=2), + offset, + order=0).astype('bool')[..., np.newaxis] + return shifted_mask + + +def _scale_mask(mask, scale_amount=0.025): + """Damages a mask for a single object by randomly scaling it in numpy. + Args: + mask: Boolean numpy array of shape(height, width, 1). + scale_amount: Float scalar, the maximum factor for random scaling. + Returns: + The scaled version of mask. + """ + nzy, nzx, _ = mask.nonzero() + cy = 0.5 * (nzy.max() - nzy.min()) + cx = 0.5 * (nzx.max() - nzx.min()) + scale_factor = np.random.uniform(1.0 - scale_amount, 1.0 + scale_amount) + shift = transform.SimilarityTransform(translation=[-cx, -cy]) + inv_shift = transform.SimilarityTransform(translation=[cx, cy]) + s = transform.SimilarityTransform(scale=[scale_factor, scale_factor]) + m = (shift + (s + inv_shift)).inverse + scaled_mask = transform.warp(mask, m) > 0.5 + return scaled_mask + + +def _rotate_mask(mask, max_rot_degrees=3.0): + """Damages a mask for a single object by randomly rotating it in numpy. + Args: + mask: Boolean numpy array of shape(height, width, 1). + max_rot_degrees: Float scalar, the maximum number of degrees to rotate. + Returns: + The scaled version of mask. + """ + cy = 0.5 * mask.shape[0] + cx = 0.5 * mask.shape[1] + rot_degrees = np.random.uniform(-max_rot_degrees, max_rot_degrees) + shift = transform.SimilarityTransform(translation=[-cx, -cy]) + inv_shift = transform.SimilarityTransform(translation=[cx, cy]) + r = transform.SimilarityTransform(rotation=np.deg2rad(rot_degrees)) + m = (shift + (r + inv_shift)).inverse + scaled_mask = transform.warp(mask, m) > 0.5 + return scaled_mask diff --git a/applications/Ma-Net/utils/meters.py b/applications/Ma-Net/utils/meters.py new file mode 100644 index 0000000000000000000000000000000000000000..c5cca45e16d85421de5a66507d80c196f71bc26d --- /dev/null +++ b/applications/Ma-Net/utils/meters.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import + + +class AverageMeter(object): + """Computes and stores the average and current value""" + def __init__(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count diff --git a/applications/Ma-Net/utils/utils.py b/applications/Ma-Net/utils/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a52f6cfa7c50a1fef9237bb69e5b62e6b91115e1 --- /dev/null +++ b/applications/Ma-Net/utils/utils.py @@ -0,0 +1,12 @@ +import numpy as np + + +def label2colormap(label): + + m = label.astype(np.uint8) + r, c = m.shape + cmap = np.zeros((r, c, 3), dtype=np.uint8) + cmap[:, :, 0] = (m & 1) << 7 | (m & 8) << 3 | (m & 64) >> 1 + cmap[:, :, 1] = (m & 2) << 6 | (m & 16) << 2 | (m & 128) >> 2 + cmap[:, :, 2] = (m & 4) << 5 | (m & 32) << 1 + return cmap diff --git a/applications/MultimodalVideoTag/README.md b/applications/MultimodalVideoTag/README.md new file mode 100644 index 0000000000000000000000000000000000000000..eef56f1e2a6e0be0428b57f7c5c1288014720d71 --- /dev/null +++ b/applications/MultimodalVideoTag/README.md @@ -0,0 +1,77 @@ +# MutimodalVideoTag 多模态视频分类模型 +--- +## 内容 +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型评估](#模型评估) +- [模型推理](#模型推理) +- [模型优化](#模型优化) +- [模型部署](#模型部署) +- [参考论文](#参考论文) + + +## 模型简介 + +该代码库用于多模态场景下视频分类任务,基于paddle2.0版本开发,模型基于真实短视频业务数据,融合文本、视频图像、音频三种模态进行视频多模标签分类,相比纯视频图像特征,显著提升高层语义标签效果。其原理示意如下图所示。 + +

+
+MutimodalVideoTag 多模态视频分类模型示意图 +

+ +- 数据处理:分别对视频三个模态的数据进行处理,对视频进行抽帧,获得图像序列;抽取视频的音频pcm 文件;收集视频标题,简单进行文本长度截断,一般取50个字。 +- 特征抽取:使用预训练的 ResNet 对图像抽取高层语义特征;使用预训练的VGGish网络抽取音频特征;文本方面使用[ERNIE 1.0](https://github.com/PaddlePaddle/ERNIE)抽取文本特征,无需预先抽取,支持视频分类模型finetune +- 序列学习:分别使用独立的LSTM 对图像特征和音频特征进行序列学习,文本方面预训练模型对字符序列进行建模,在ernie 后接入一个textcnn 网络做下游任务的迁移学习。 +- 多模融合:文本具有显式的高层语义信息,将文本特征引入到LSTM pooling 过程指导图像和音频时序权重分配,进行交叉融合,最后将文本、音频、视频特征拼接。 +- 预测结果:分类器选用sigmoid 多标签分类器,支持视频多标签输出。 + +## 数据准备 +数据方面提供已经抽取好图像、音频特征的特征文件,以及标题和标签信息,模型方面提供训练好checkpoint 文件,可进行finetune、模型评估、预测。 +``` +sh download.sh +``` +数据文件包括抽取好特征的文件夹 `feature_files`,以及记录划分的txt 文件,格式如下 +``` +文件名 \t 标题 \t 标签 +18e9bf08a2fc7eaa4ee9215ab42ea827.mp4 叮叮来自肖宇梁肖宇梁rainco的特别起床铃声 拍人-帅哥,拍人-秀特效,明星周边-其他明星周边 +``` + +## 模型训练 +模型训练过程有如下可调模式,可在根据数据集情况进行调整,在`conf/conf.txt` 文件中 +- ernie_freeze: 用于控制文本提特征的ernie 网络是否进行finetune,因为ernie 复杂度远大于图像、视频序列学习网络,因此在某些数据集上不好训练。 +- lstm_pool_mode: 用于控制lstm 序列池化的方式,默认是"text_guide"表示利用文本加强池化注意力权重,如果设置为空,则默认为自注意力的权重。 + +``` +sh train.sh +``` +## 模型评估 +模型对测试集进行评估,同时支持将checkpoint 模型转为inference 模型, 可用参数'save_only' 选项控制,设置即只用于做模型转换,得到inference 模型 +``` +sh eval_and_save_model.sh +``` +## 模型推理 +通过上一步得到的inference 模型进行预测,结果默认阈值为0.5,存储到json 文件中,在`conf/conf.txt` 文件 `threshold` 参数进行控制多标签输出的阈值。 +``` +sh inference.sh +``` +## 模型优化 +模型方面,主要在文本分支进行了实验,实验结果显示ERNIE 在多分支下不微调,而是使用后置网络进行微调,训练速度快,且稳定,同时attention 方面使用文本信息增强图像、音频的attention 学习能一定程度提升模型效果。 + +| 模型 | Hit@1 | Hit@2 | +| ------------------------------------------------------------ | ----- | ----- | +| 文本分支ERNIE 不finetune +self-attention | 71.07 | 83.72 | +| 文本分支ERNIE 不finetune +textcnn finetune + self-attention | 72.66 | 85.01 | +| 文本分支ERNIE 不finetune +extcnn finetune + text-guide-attention | 73.29 | 85.59 | + +## 模型部署 + +
+
+
+ + +## 参考论文 +- [Attention Clusters: Purely Attention Based Local Feature Integration for Video Classification](https://arxiv.org/abs/1711.09550), Xiang Long, Chuang Gan, Gerard de Melo, Jiajun Wu, Xiao Liu, Shilei Wen +- [YouTube-8M: A Large-Scale Video Classification Benchmark](https://arxiv.org/abs/1609.08675), Sami Abu-El-Haija, Nisarg Kothari, Joonseok Lee, Paul Natsev, George Toderici, Balakrishnan Varadarajan, Sudheendra Vijayanarasimhan +- [Ernie: Enhanced representation through knowledge integration](https://arxiv.org/abs/1904.09223), Sun, Yu and Wang, Shuohuan and Li, Yukun and Feng, Shikun and Chen, Xuyi and Zhang, Han and Tian, Xin and Zhu, Danxiang and Tian, Hao and Wu, Hua diff --git a/applications/MultimodalVideoTag/conf/conf.txt b/applications/MultimodalVideoTag/conf/conf.txt new file mode 100755 index 0000000000000000000000000000000000000000..cec68ba74894fc9377e6202a1598168e50232070 --- /dev/null +++ b/applications/MultimodalVideoTag/conf/conf.txt @@ -0,0 +1,56 @@ +[MODEL] +name = "" +dataset = "Baidudata" +bone_nework = None +drop_rate = 0.5 +feature_num = 3 +feature_names = ['rgb', 'audio', 'text'] +text_max_len = 50 +feature_dims = [[2048], [128], [5, 50, 1]] +feature_dtypes = ['float32', 'float32', 'int64'] +feature_lod_level = [1, 1, 0] +embedding_size = 512 +lstm_size_img = 512 +lstm_size_audio = 128 +num_classes = 110 +top_n = [1,2,3,5] +num_first_classes = 20 +class_name_file = "./datasets/class.txt" +ernie_freeze = True +lstm_pool_mode = 'text_guide' + +[TRAIN] +epoch = 30 +learning_rate = 0.0007 +decay_gamma = 0.2 +l2_weight_decay = 8e-4 +decay_epochs = [5, 10, 15, 20] +num_samples = 70000 +batch_size = 64 +use_gpu = True +warmup_epoch = 5 +loss_type = "sigmoid" +modal_drop_rate = 0.0 +ernie_pretrain_dict_path = "model_pretrained/ernie_v1_params/" +filelist = "./datasets/feature_files/" +url_title_label_file = "./datasets/train.txt" +warmup_proportion = 0.0 +early_stop = 15 + +[VALID] +batch_size = 16 +num_samples = 464 +filelist = "./datasets/feature_files/" +url_title_label_file = "./datasets/val.txt" + +[TEST] +batch_size = 16 +num_samples = 464 +filelist = "./datasets/feature_files/" +url_title_label_file = "./datasets/val.txt" + +[INFER] +batch_size = 64 +threshold = 0.5 +filelist = "./datasets/feature_files/" +url_title_label_file = "./datasets/val.txt" diff --git a/applications/MultimodalVideoTag/download.sh b/applications/MultimodalVideoTag/download.sh new file mode 100644 index 0000000000000000000000000000000000000000..c5ac0aaddad72c3ecae1cfbed526f486a489e041 --- /dev/null +++ b/applications/MultimodalVideoTag/download.sh @@ -0,0 +1,11 @@ +# download ernie 1.0 model +wget https://bj.bcebos.com/acg-algo/PaddleVideo_application/MultimodalVideoTag/model_pretrained_ernie.tar.gz +tar -xzvf model_pretrained_ernie.tar.gz + +# download pretrain model +wget https://bj.bcebos.com/acg-algo/PaddleVideo_application/MultimodalVideoTag/checkpoints_save.tar.gz +tar -xzvf checkpoints_save.tar.gz + +# download test dataset +wget https://bj.bcebos.com/acg-algo/PaddleVideo_application/MultimodalVideoTag/datasets.tar.gz +tar -xzvf datasets.tar.gz diff --git a/applications/MultimodalVideoTag/eval_and_save_model.sh b/applications/MultimodalVideoTag/eval_and_save_model.sh new file mode 100755 index 0000000000000000000000000000000000000000..6ecd57e531f0848438ea90162b2f5a35c0a836c6 --- /dev/null +++ b/applications/MultimodalVideoTag/eval_and_save_model.sh @@ -0,0 +1,13 @@ +# eval sh +export CUDA_VISIBLE_DEVICES=0 +export FLAGS_eager_delete_tensor_gb=0.0 +export FLAGS_sync_nccl_allreduce=1 +export FLAGS_fast_eager_deletion_mode=1 +export FLAGS_fraction_of_gpu_memory_to_use=0.5 +export FLAGS_reallocate_gpu_memory_in_mb=0 +export FLAGS_memory_fraction_of_eager_deletion=1 +python scenario_lib/eval_and_save_model.py --model_name=AttentionLstmErnie \ +--config=./conf/conf.txt \ +--save_model_param_dir=checkpoints_save \ +--save_inference_model=inference_models_save \ +# --save_only diff --git a/applications/MultimodalVideoTag/images/model.png b/applications/MultimodalVideoTag/images/model.png new file mode 100644 index 0000000000000000000000000000000000000000..933edd832dfd2c403379aaa91a4cbf4cca126f34 Binary files /dev/null and b/applications/MultimodalVideoTag/images/model.png differ diff --git a/applications/MultimodalVideoTag/images/show.gif b/applications/MultimodalVideoTag/images/show.gif new file mode 100644 index 0000000000000000000000000000000000000000..b1b0bc8483152e8f893fb8bdd9822b80ba164f6d Binary files /dev/null and b/applications/MultimodalVideoTag/images/show.gif differ diff --git a/applications/MultimodalVideoTag/inference.sh b/applications/MultimodalVideoTag/inference.sh new file mode 100755 index 0000000000000000000000000000000000000000..8d490cf54a26fecadb9deb0a3b91e0ce8c16513e --- /dev/null +++ b/applications/MultimodalVideoTag/inference.sh @@ -0,0 +1,12 @@ +# inference sh +export CUDA_VISIBLE_DEVICES=0 +export FLAGS_eager_delete_tensor_gb=0.0 +export FLAGS_sync_nccl_allreduce=1 +export FLAGS_fast_eager_deletion_mode=1 +export FLAGS_fraction_of_gpu_memory_to_use=0.5 +export FLAGS_reallocate_gpu_memory_in_mb=0 +export FLAGS_memory_fraction_of_eager_deletion=1 +python scenario_lib/inference.py --model_name=AttentionLstmErnie \ +--config=./conf/conf.txt \ +--save_inference_model=inference_models_save \ +--output='output.json' diff --git a/applications/MultimodalVideoTag/scenario_lib/accuracy_metrics.py b/applications/MultimodalVideoTag/scenario_lib/accuracy_metrics.py new file mode 100755 index 0000000000000000000000000000000000000000..21539ed9c93a759618ac13843be1e6f7baa9ccf8 --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/accuracy_metrics.py @@ -0,0 +1,160 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division + +import numpy as np +import logging + +logger = logging.getLogger(__name__) + + +class MetricsCalculator(): + """ + MetricsCalculator + """ + def __init__(self, name, mode, metrics_args): + """ + init + """ + self.name = name + self.mode = mode # 'train', 'val', 'test' + self.acc_dict = {} + self.top_n_list = metrics_args.MODEL.top_n + self.num_classes = metrics_args.MODEL.num_classes + self.reset() + + def reset(self): + """ + reset + """ + logger.info('Resetting {} metrics...'.format(self.mode)) + for topk in self.top_n_list: + self.acc_dict['avg_acc%d' % (topk)] = 0.0 + self.aggr_loss = 0.0 + self.aggr_batch_size = 0 + + def finalize_metrics(self): + """finalize_metrics + """ + for key, value in self.acc_dict.items(): + self.acc_dict[key] = value / self.aggr_batch_size + self.aggr_loss = self.aggr_loss / self.aggr_batch_size + + def get_computed_metrics(self): + """get_computed_metrics + """ + acc_dict = {} + for key, value in self.acc_dict.items(): + acc_dict[key] = value / self.aggr_batch_size + aggr_loss = self.aggr_loss / self.aggr_batch_size + + return acc_dict, aggr_loss + + def accumulate(self, loss, softmax, labels): + """accumulate + """ + cur_batch_size = softmax.shape[0] + # if returned loss is None for e.g. test, just set loss to be 0. + if loss is None: + cur_loss = 0. + else: + cur_loss = np.mean(np.array(loss)) # + self.aggr_batch_size += cur_batch_size + self.aggr_loss += cur_loss * cur_batch_size + + for top_k in self.top_n_list: + self.acc_dict['avg_acc%d' % + (top_k)] += cur_batch_size * compute_topk_accuracy( + softmax, labels, top_k=top_k) * 100. + return + + def finalize_and_log_out(self, info=''): + """finalize_and_log_out + """ + metrics_dict, loss = self.get_computed_metrics() + acc_str = [] + for name, value in metrics_dict.items(): + acc_str.append('{}:{},'.format('%s' % name, '%.2f' % value)) + acc_str = '\t'.join(acc_str) + logger.info(info + + '\tLoss: {},\t{}'.format('%.6f' % loss, '%s' % acc_str)) + return + + +def compute_topk_correct_hits_multilabel(top_k, preds, labels): + '''Compute the number of corret hits''' + batch_size = preds.shape[0] + top_k_preds = np.zeros((batch_size, 10), dtype=np.float32) + for i in range(batch_size): + top_k_preds[i, :] = np.argsort(-preds[i, :])[:10] + correctness = np.zeros(batch_size, dtype=np.float32) + for i in range(batch_size): + correc_sum = 0 + for label_id in range(len(labels[i])): + label_hit = labels[i][label_id] + if label_hit == 0 or label_hit < 0.1: + continue + if label_id in top_k_preds[i, :top_k].astype(np.int32).tolist(): + # correc_sum += 1 + correc_sum = 1 + break + correctness[i] = correc_sum + correct_hits = sum(correctness) + return correct_hits + + +def compute_topk_correct_hits(top_k, preds, labels): + '''Compute the number of corret hits''' + batch_size = preds.shape[0] + + top_k_preds = np.zeros((batch_size, top_k), dtype=np.float32) + for i in range(batch_size): + top_k_preds[i, :] = np.argsort(-preds[i, :])[:top_k] + + correctness = np.zeros(batch_size, dtype=np.int32) + for i in range(batch_size): + if labels[i] in top_k_preds[i, :].astype(np.int32).tolist(): + correctness[i] = 1 + correct_hits = sum(correctness) + + return correct_hits + + +def compute_topk_accuracy(softmax, labels, top_k): + """compute_topk_accuracy + """ + computed_metrics = {} + assert labels.shape[0] == softmax.shape[0], "Batch size mismatch." + aggr_batch_size = labels.shape[0] + # aggr_top_k_correct_hits = compute_topk_correct_hits(top_k, softmax, labels) + aggr_top_k_correct_hits = compute_topk_correct_hits_multilabel( + top_k, softmax, labels) + # normalize results + computed_metrics = \ + float(aggr_top_k_correct_hits) / aggr_batch_size + + return computed_metrics + + +if __name__ == "__main__": + pred = np.array([[0.5, 0.2, 0.3, 0, 0]]) + label = np.array([[0.5, 0.5, 0, 0, 0]]) + print('pred: ', pred) + print('label: ', label) + print('Top 1 hits', compute_topk_correct_hits_multilabel(1, pred, label)) + print('Top 5 hits', compute_topk_correct_hits_multilabel(5, pred, label)) diff --git a/applications/MultimodalVideoTag/scenario_lib/config.py b/applications/MultimodalVideoTag/scenario_lib/config.py new file mode 100755 index 0000000000000000000000000000000000000000..751895c6ebffab52e551ce54f424478efa106332 --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/config.py @@ -0,0 +1,71 @@ +""" +config parser +""" + +try: + from configparser import ConfigParser +except BaseException: + from ConfigParser import ConfigParser + +from utils import AttrDict + +import logging +logger = logging.getLogger(__name__) + +CONFIG_SECS = [ + 'train', + 'valid', + 'test', + 'infer', +] + + +def parse_config(cfg_file): + """parse_config + """ + parser = ConfigParser() + cfg = AttrDict() + parser.read(cfg_file) + for sec in parser.sections(): + sec_dict = AttrDict() + for k, v in parser.items(sec): + try: + v = eval(v) + except BaseException: + pass + setattr(sec_dict, k, v) + setattr(cfg, sec.upper(), sec_dict) + + return cfg + + +def merge_configs(cfg, sec, args_dict): + """merge_configs + """ + assert sec in CONFIG_SECS, "invalid config section {}".format(sec) + sec_dict = getattr(cfg, sec.upper()) + for k, v in args_dict.items(): + if v is None: + continue + # try: + # if hasattr(sec_dict, k): + # setattr(sec_dict, k, v) + # except BaseException: + # pass + if k in sec_dict: + setattr(sec_dict, k, v) + return cfg + +def print_configs(cfg, mode): + """print_configs + """ + logger.info("---------------- {:>5} Arguments ----------------".format(mode)) + for sec, sec_items in cfg.items(): + if isinstance(sec_items, dict) is True: + logger.info("{}:".format(sec)) + for k, v in sec_items.items(): + logger.info(" {}:{}".format(k, v)) + else: + logger.info("{}:{}".format(sec, sec_items)) + + logger.info("-------------------------------------------------") diff --git a/applications/MultimodalVideoTag/scenario_lib/datareader/__init__.py b/applications/MultimodalVideoTag/scenario_lib/datareader/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..270d628e37b3f2a8ba49374dfa82b8300ecb6058 --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/datareader/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +Copyright 2021 Baidu.com, Inc. All Rights Reserved +Description: +Authors: wanghewei(wanghewei@baidu.com) +LastEditors: wanghewei(wanghewei@baidu.com) +Date: 2021-11-26 16:31:59 +""" +from .reader_utils import regist_reader, get_reader +from .feature_reader import FeatureReader +# regist reader, sort by alphabet +regist_reader("ATTENTIONLSTMERNIE", FeatureReader) diff --git a/applications/MultimodalVideoTag/scenario_lib/datareader/ernie_task_reader.py b/applications/MultimodalVideoTag/scenario_lib/datareader/ernie_task_reader.py new file mode 100755 index 0000000000000000000000000000000000000000..e8d49a29b5347c3970b25382a40f21315666b011 --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/datareader/ernie_task_reader.py @@ -0,0 +1,334 @@ +""" +ernie reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import absolute_import + +import sys +import os +import json +import random +import logging +import numpy as np +import six +from io import open +from collections import namedtuple + +from .tokenization import FullTokenizer, convert_to_unicode + +log = logging.getLogger(__name__) + +if six.PY3: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def csv_reader(fd, delimiter='\t'): + """csv_reader + """ + def gen(): + """gen + """ + for i in fd: + yield i.rstrip('\n').split(delimiter) + + return gen() + + +class BaseReader(object): + """BaseReader + """ + def __init__(self, + vocab_path, + label_map_config=None, + max_seq_len=512, + do_lower_case=True, + in_tokens=False, + is_inference=False, + random_seed=None, + tokenizer="FullTokenizer", + is_classify=True, + is_regression=False, + for_cn=True, + task_id=0): + self.max_seq_len = max_seq_len + self.tokenizer = FullTokenizer(vocab_file=vocab_path, + do_lower_case=do_lower_case) + self.vocab = self.tokenizer.vocab + self.pad_id = self.vocab["[PAD]"] + self.cls_id = self.vocab["[CLS]"] + self.sep_id = self.vocab["[SEP]"] + self.in_tokens = in_tokens + self.is_inference = is_inference + self.for_cn = for_cn + self.task_id = task_id + + np.random.seed(random_seed) + + self.is_classify = is_classify + self.is_regression = is_regression + self.current_example = 0 + self.current_epoch = 0 + self.num_examples = 0 + + if label_map_config: + with open(label_map_config, encoding='utf8') as f: + self.label_map = json.load(f) + else: + self.label_map = None + + def _truncate_seq_pair(self, tokens_a, tokens_b, max_length): + """Truncates a sequence pair in place to the maximum length.""" + + # This is a simple heuristic which will always truncate the longer sequence + # one token at a time. This makes more sense than truncating an equal percent + # of tokens from each, since if one sequence is very short then each token + # that's truncated likely contains more information than a longer sequence. + while True: + total_length = len(tokens_a) + len(tokens_b) + if total_length <= max_length: + break + if len(tokens_a) > len(tokens_b): + tokens_a.pop() + else: + tokens_b.pop() + + def _convert_example_to_record(self, example, max_seq_length, tokenizer): + """Converts a single `Example` into a single `Record`.""" + + text_a = convert_to_unicode(example.text_a) + tokens_a = tokenizer.tokenize(text_a) + tokens_b = None + + has_text_b = False + if isinstance(example, dict): + has_text_b = "text_b" in example.keys() + else: + has_text_b = "text_b" in example._fields + + if has_text_b: + text_b = convert_to_unicode(example.text_b) + tokens_b = tokenizer.tokenize(text_b) + + if tokens_b: + # Modifies `tokens_a` and `tokens_b` in place so that the total + # length is less than the specified length. + # Account for [CLS], [SEP], [SEP] with "- 3" + self._truncate_seq_pair(tokens_a, tokens_b, max_seq_length - 3) + else: + # Account for [CLS] and [SEP] with "- 2" + if len(tokens_a) > max_seq_length - 2: + tokens_a = tokens_a[0:(max_seq_length - 2)] + + # The convention in BERT/ERNIE is: + # (a) For sequence pairs: + # tokens: [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP] + # type_ids: 0 0 0 0 0 0 0 0 1 1 1 1 1 1 + # (b) For single sequences: + # tokens: [CLS] the dog is hairy . [SEP] + # type_ids: 0 0 0 0 0 0 0 + # + # Where "type_ids" are used to indicate whether this is the first + # sequence or the second sequence. The embedding vectors for `type=0` and + # `type=1` were learned during pre-training and are added to the wordpiece + # embedding vector (and position vector). This is not *strictly* necessary + # since the [SEP] token unambiguously separates the sequences, but it makes + # it easier for the model to learn the concept of sequences. + # + # For classification tasks, the first vector (corresponding to [CLS]) is + # used as as the "sentence vector". Note that this only makes sense because + # the entire model is fine-tuned. + tokens = [] + text_type_ids = [] + tokens.append("[CLS]") + text_type_ids.append(0) + for token in tokens_a: + tokens.append(token) + text_type_ids.append(0) + tokens.append("[SEP]") + text_type_ids.append(0) + + if tokens_b: + for token in tokens_b: + tokens.append(token) + text_type_ids.append(1) + tokens.append("[SEP]") + text_type_ids.append(1) + + token_ids = tokenizer.convert_tokens_to_ids(tokens) + position_ids = list(range(len(token_ids))) + + if self.is_inference: + Record = namedtuple('Record', + ['token_ids', 'text_type_ids', 'position_ids']) + record = Record(token_ids=token_ids, + text_type_ids=text_type_ids, + position_ids=position_ids) + else: + if self.label_map: + label_id = self.label_map[example.label] + else: + label_id = example.label + + Record = namedtuple('Record', [ + 'token_ids', 'text_type_ids', 'position_ids', 'label_id', 'qid' + ]) + + qid = None + if "qid" in example._fields: + qid = example.qid + + record = Record(token_ids=token_ids, + text_type_ids=text_type_ids, + position_ids=position_ids, + label_id=label_id, + qid=qid) + return record + + def _prepare_batch_data(self, examples, batch_size, phase=None): + """generate batch records""" + batch_records, max_len = [], 0 + for index, example in enumerate(examples): + if phase == "train": + self.current_example = index + record = self._convert_example_to_record(example, self.max_seq_len, + self.tokenizer) + max_len = max(max_len, len(record.token_ids)) + if self.in_tokens: + to_append = (len(batch_records) + 1) * max_len <= batch_size + else: + to_append = len(batch_records) < batch_size + if to_append: + batch_records.append(record) + else: + yield self._pad_batch_records(batch_records) + batch_records, max_len = [record], len(record.token_ids) + + if batch_records: + yield self._pad_batch_records(batch_records) + + +class ExtractEmbeddingReader(BaseReader): + """ + data prepare for getting erine embedding + """ + def _pad_batch_records(self, batch_records): + """ + 对字标号,位置标号特征进行固定长度补全 + batch_records 包含多条文本的标号 + return [字标号列表,文本类型列表,位置特征列表,任务标号列表,掩码列表] + """ + batch_token_ids = [record.token_ids for record in batch_records] + batch_text_type_ids = [ + record.text_type_ids for record in batch_records + ] + batch_position_ids = [record.position_ids for record in batch_records] + + # padding + padded_token_ids, input_mask, seq_lens = pad_batch_data( + batch_token_ids, + pad_idx=self.pad_id, + return_input_mask=True, + return_seq_lens=True, + max_len=self.max_seq_len) + padded_text_type_ids = pad_batch_data(batch_text_type_ids, + pad_idx=self.pad_id, + max_len=self.max_seq_len) + padded_position_ids = pad_batch_data(batch_position_ids, + pad_idx=self.pad_id, + max_len=self.max_seq_len) + padded_task_ids = np.ones_like(padded_token_ids, + dtype="int64") * self.task_id + + return_list = [ + padded_token_ids, padded_text_type_ids, padded_position_ids, + padded_task_ids, input_mask + ] + return return_list + + def data_generate_from_text(self, text): + """ + trans text to idx + input single text + return 5*maxlen*1 + """ + Example = namedtuple('Example', ['text_a', 'label']) + example = Example(text, 0) + records = [ + self._convert_example_to_record(example, self.max_seq_len, + self.tokenizer) + ] + pad_records = self._pad_batch_records(records) + text_one_hot = np.concatenate(pad_records, axis=0).astype('int64') + return text_one_hot + + +def pad_batch_data(insts, + pad_idx=0, + max_len=None, + return_pos=False, + return_input_mask=False, + return_max_len=False, + return_num_token=False, + return_seq_lens=False): + """ + Pad the instances to the max sequence length in batch, and generate the + corresponding position data and attention bias. + """ + return_list = [] + if max_len is None: + max_len = max(len(inst) for inst in insts) + # Any token included in dict can be used to pad, since the paddings' loss + # will be masked out by weights and make no effect on parameter gradients. + + inst_data = np.array( + [inst + list([pad_idx] * (max_len - len(inst))) for inst in insts]) + return_list += [inst_data.astype("int64").reshape([-1, max_len, 1])] + + # position data + if return_pos: + inst_pos = np.array([ + list(range(0, len(inst))) + [pad_idx] * (max_len - len(inst)) + for inst in insts + ]) + + return_list += [inst_pos.astype("int64").reshape([-1, max_len, 1])] + + if return_input_mask: + # This is used to avoid attention on paddings. + input_mask_data = np.array( + [[1] * len(inst) + [0] * (max_len - len(inst)) for inst in insts]) + input_mask_data = np.expand_dims(input_mask_data, axis=-1) + return_list += [input_mask_data.astype("float32")] + + if return_max_len: + return_list += [max_len] + + if return_num_token: + num_token = 0 + for inst in insts: + num_token += len(inst) + return_list += [num_token] + + if return_seq_lens: + seq_lens = np.array([len(inst) for inst in insts]) + return_list += [seq_lens.astype("int64").reshape([-1])] + + return return_list if len(return_list) > 1 else return_list[0] diff --git a/applications/MultimodalVideoTag/scenario_lib/datareader/feature_reader.py b/applications/MultimodalVideoTag/scenario_lib/datareader/feature_reader.py new file mode 100755 index 0000000000000000000000000000000000000000..f97ea7874d045d8e2236fb5934f3d0a9c152dc7e --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/datareader/feature_reader.py @@ -0,0 +1,274 @@ +""" +feature reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle + from io import BytesIO +import numpy as np +import random +import os +import traceback +import pickle +python_ver = sys.version_info +from collections import defaultdict + +import pandas as pd + +from .ernie_task_reader import ExtractEmbeddingReader +from .reader_utils import DataReader + + +class FeatureReader(DataReader): + """ + Data reader for youtube-8M dataset, which was stored as features extracted by prior networks + This is for the three models: lstm, attention cluster, nextvlad + + dataset cfg: num_classes + batch_size + list + NextVlad only: eigen_file + """ + def __init__(self, name, mode, cfg): + """ + init + """ + self.name = name + self.mode = mode + self.num_classes = cfg.MODEL.num_classes + + # set batch size and file list + self.batch_size = cfg[mode.upper()]['batch_size'] + self.filelist = cfg[mode.upper()]['filelist'] + self.eigen_file = cfg.MODEL.get('eigen_file', None) + self.num_seg = cfg.MODEL.get('num_seg', None) + self.loss_type = cfg.TRAIN['loss_type'] + vocab_file = os.path.join(cfg.TRAIN.ernie_pretrain_dict_path, + 'vocab.txt') + self.ernie_reader = ExtractEmbeddingReader( + vocab_path=vocab_file, + max_seq_len=cfg.MODEL.text_max_len, + do_lower_case=True) + url_title_label_file = cfg[mode.upper()]['url_title_label_file'] + self.class_dict = load_class_file(cfg.MODEL.class_name_file) + self.url_title_info = load_video_file(url_title_label_file, + self.class_dict, mode) + + def create_reader(self): + """ + create reader + """ + url_list = list(self.url_title_info.keys()) + if self.mode == 'train': + random.shuffle(url_list) + + def reader(): + """reader + """ + batch_out = [] + for url in url_list: + try: + filepath = os.path.join( + self.filelist, + url.split('/')[-1].split('.')[0] + '.pkl') + if os.path.exists(filepath) is False: + continue + if python_ver < (3, 0): + record = pickle.load(open(filepath, 'rb')) + else: + record = pickle.load(open(filepath, 'rb'), + encoding='iso-8859-1') + text_raw = self.url_title_info[url]['title'] + rgb = record['feature']['image_pkl'].astype(float) + if record['feature']['audio_pkl'].shape[0] == 0: + audio_pkl = np.zeros((10, 128)) + audio = audio_pkl.astype(float) + else: + audio = record['feature']['audio_pkl'].astype(float) + text_one_hot = self.ernie_reader.data_generate_from_text( + str(text_raw)) + video = record['video'] + if self.mode != 'infer': + label = self.url_title_info[url]['label'] + label = [int(w) for w in label] + if self.loss_type == 'sigmoid': + label = make_one_hot(label, self.num_classes) + elif self.loss_type == 'softmax': + label = make_one_soft_hot(label, self.num_classes, + False) + batch_out.append((rgb, audio, text_one_hot, label)) + else: + batch_out.append((rgb, audio, text_one_hot, video)) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + except Exception as e: + print("warning: load data {} failed, {}".format( + filepath, str(e))) + traceback.print_exc() + continue + + +# if self.mode == 'infer' and len(batch_out) > 0: + if len(batch_out) > 0: + yield batch_out + + return reader + + def get_config_from_sec(self, sec, item, default=None): + """get_config_from_sec + """ + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + +def load_video_file(label_file, class_dict, mode='train'): + """ + labelfile formate: URL \t title \t label1,label2 + return dict + """ + data = pd.read_csv(label_file, sep='\t', header=None) + url_info_dict = defaultdict(dict) + for index, row in data.iterrows(): + url = row[0] + if url in url_info_dict: + continue + if pd.isna(row[1]): + title = "" + else: + title = str(row[1]) + if mode == 'infer': + url_info_dict[url] = {'title': title} + else: + if pd.isna(row[2]): + continue + labels = row[2].split(',') + labels_idx = [class_dict[w] for w in labels if w in class_dict] + if len(labels_idx) < 1: + continue + if url not in url_info_dict: + url_info_dict[url] = {'label': labels_idx, 'title': title} + print('load video %d' % (len(url_info_dict))) + return url_info_dict + + +def dequantize(feat_vector, max_quantized_value=2., min_quantized_value=-2.): + """ + Dequantize the feature from the byte format to the float format + """ + + assert max_quantized_value > min_quantized_value + quantized_range = max_quantized_value - min_quantized_value + scalar = quantized_range / 255.0 + bias = (quantized_range / 512.0) + min_quantized_value + + return feat_vector * scalar + bias + + +epsilon = 0.1 +smmoth_score = (1.0 / float(210)) * epsilon + + +def label_smmoth(label_one_hot_vector): + """ + label_smmoth + """ + global smmoth_score + for i in range(len(label_one_hot_vector)): + if label_one_hot_vector[i] == 0: + label_one_hot_vector[i] = smmoth_score + return label_one_hot_vector + + +def make_one_soft_hot(label, dim=15, label_smmoth=False): + """ + make_one_soft_hot + """ + one_hot_soft_label = np.zeros(dim) + one_hot_soft_label = one_hot_soft_label.astype(float) + # multi-labelis + # label smmoth + if label_smmoth: + one_hot_soft_label = label_smmoth(one_hot_soft_label) + label_len = len(label) + prob = (1 - np.sum(one_hot_soft_label)) / float(label_len) + for ind in label: + one_hot_soft_label[ind] += prob + #one_hot_soft_label = label_smmoth(one_hot_soft_label) + return one_hot_soft_label + + +def make_one_hot(label, dim=15): + """ + make_one_hot + """ + one_hot_soft_label = np.zeros(dim) + one_hot_soft_label = one_hot_soft_label.astype(float) + for ind in label: + one_hot_soft_label[ind] = 1 + return one_hot_soft_label + + +def generate_random_idx(feature_len, num_seg): + """ + generate_random_idx + """ + idxs = [] + stride = float(feature_len) / num_seg + for i in range(num_seg): + pos = (i + np.random.random()) * stride + idxs.append(min(feature_len - 1, int(pos))) + return idxs + + +def get_batch_ernie_input_feature(reader, texts): + """ + get_batch_ernie_input_feature + """ + result_list = reader.data_generate_from_texts(texts) + result_trans = [] + for i in range(len(texts)): + result_trans.append([result_list[0][i],\ + result_list[1][i], + result_list[2][i], + result_list[3][i], + result_list[4][i]]) + return np.array(result_trans) + + +def load_class_file(class_file): + """ + load_class_file + """ + class_lines = open(class_file, 'r', encoding='utf8').readlines() + class_dict = {} + for i, line in enumerate(class_lines): + tmp = line.strip().split('\t') + word = tmp[0] + index = str(i) + if len(tmp) == 2: + index = tmp[1] + class_dict[word] = index + return class_dict + + +if __name__ == '__main__': + pass diff --git a/applications/MultimodalVideoTag/scenario_lib/datareader/reader_utils.py b/applications/MultimodalVideoTag/scenario_lib/datareader/reader_utils.py new file mode 100755 index 0000000000000000000000000000000000000000..1532950747dc6b843cd1d33ae29f63aa5cc76ca0 --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/datareader/reader_utils.py @@ -0,0 +1,91 @@ +""" +reader utils +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class ReaderNotFoundError(Exception): + "Error: reader not found" + + def __init__(self, reader_name, avail_readers): + super(ReaderNotFoundError, self).__init__() + self.reader_name = reader_name + self.avail_readers = avail_readers + + def __str__(self): + msg = "Reader {} Not Found.\nAvailiable readers:\n".format( + self.reader_name) + for reader in self.avail_readers: + msg += " {}\n".format(reader) + return msg + + +class DataReader(object): + """data reader for video input""" + + def __init__(self, model_name, mode, cfg): + self.name = model_name + self.mode = mode + self.cfg = cfg + + def create_reader(self): + """Not implemented""" + pass + + def get_config_from_sec(self, sec, item, default=None): + """get_config_from_sec + """ + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + +class ReaderZoo(object): + """ReaderZoo + """ + def __init__(self): + self.reader_zoo = {} + + def regist(self, name, reader): + """regist + """ + assert reader.__base__ == DataReader, "Unknow model type {}".format( + type(reader)) + self.reader_zoo[name] = reader + + def get(self, name, mode, cfg): + """get + """ + for k, v in self.reader_zoo.items(): + if k == name: + return v(name, mode, cfg) + raise ReaderNotFoundError(name, self.reader_zoo.keys()) + + +# singleton reader_zoo +reader_zoo = ReaderZoo() + + +def regist_reader(name, reader): + """regist_reader + """ + reader_zoo.regist(name, reader) + + +def get_reader(name, mode, cfg): + """get_reader + """ + reader_model = reader_zoo.get(name, mode, cfg) + return reader_model.create_reader() diff --git a/applications/MultimodalVideoTag/scenario_lib/datareader/tokenization.py b/applications/MultimodalVideoTag/scenario_lib/datareader/tokenization.py new file mode 100755 index 0000000000000000000000000000000000000000..52c8024ba0f6998b4f12c5bd5c8cb34af3b7288c --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/datareader/tokenization.py @@ -0,0 +1,441 @@ +# coding=utf-8 +# Copyright 2018 The Google AI Language Team Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tokenization classes.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import absolute_import + +from io import open + +import collections +import unicodedata +import six + + +def convert_to_unicode(text): + """Converts `text` to Unicode (if it's not already), assuming utf-8 input.""" + if six.PY3: + if isinstance(text, str): + return text + elif isinstance(text, bytes): + return text.decode("utf-8", "ignore") + else: + raise ValueError("Unsupported string type: %s" % (type(text))) + elif six.PY2: + if isinstance(text, str): + return text.decode("utf-8", "ignore") + elif isinstance(text, unicode): + return text + else: + raise ValueError("Unsupported string type: %s" % (type(text))) + else: + raise ValueError("Not running on Python2 or Python 3?") + + +def printable_text(text): + """Returns text encoded in a way suitable for print or `tf.logging`.""" + + # These functions want `str` for both Python2 and Python3, but in one case + # it's a Unicode string and in the other it's a byte string. + if six.PY3: + if isinstance(text, str): + return text + elif isinstance(text, bytes): + return text.decode("utf-8", "ignore") + else: + raise ValueError("Unsupported string type: %s" % (type(text))) + elif six.PY2: + if isinstance(text, str): + return text + elif isinstance(text, unicode): + return text.encode("utf-8") + else: + raise ValueError("Unsupported string type: %s" % (type(text))) + else: + raise ValueError("Not running on Python2 or Python 3?") + + +def load_vocab(vocab_file): + """Loads a vocabulary file into a dictionary.""" + vocab = collections.OrderedDict() + with open(vocab_file, encoding='utf8') as fin: + for num, line in enumerate(fin): + items = convert_to_unicode(line.strip()).split("\t") + if len(items) > 2: + break + token = items[0] + index = items[1] if len(items) == 2 else num + token = token.strip() + vocab[token] = int(index) + return vocab + + +def convert_by_vocab(vocab, items): + """Converts a sequence of [tokens|ids] using the vocab.""" + output = [] + for item in items: + output.append(vocab[item]) + return output + + +def convert_tokens_to_ids(vocab, tokens): + """convert_tokens_to_ids + """ + return convert_by_vocab(vocab, tokens) + + +def convert_ids_to_tokens(inv_vocab, ids): + """convert_ids_to_tokens + """ + return convert_by_vocab(inv_vocab, ids) + + +def whitespace_tokenize(text): + """Runs basic whitespace cleaning and splitting on a peice of text.""" + text = text.strip() + if not text: + return [] + tokens = text.split() + return tokens + + +class FullTokenizer(object): + """Runs end-to-end tokenziation.""" + + def __init__(self, vocab_file, do_lower_case=True): + """init + """ + self.vocab = load_vocab(vocab_file) + self.inv_vocab = {v: k for k, v in self.vocab.items()} + self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case) + self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab) + + def tokenize(self, text): + """tokenize + """ + split_tokens = [] + for token in self.basic_tokenizer.tokenize(text): + for sub_token in self.wordpiece_tokenizer.tokenize(token): + split_tokens.append(sub_token) + return split_tokens + + def convert_tokens_to_ids(self, tokens): + """convert_tokens_to_ids + """ + return convert_by_vocab(self.vocab, tokens) + + def convert_ids_to_tokens(self, ids): + """convert_ids_to_tokens + """ + return convert_by_vocab(self.inv_vocab, ids) + + +class CharTokenizer(object): + """Runs end-to-end tokenziation.""" + + def __init__(self, vocab_file, do_lower_case=True): + self.vocab = load_vocab(vocab_file) + self.inv_vocab = {v: k for k, v in self.vocab.items()} + self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab) + + def tokenize(self, text): + """tokenize + """ + split_tokens = [] + for token in text.lower().split(" "): + for sub_token in self.wordpiece_tokenizer.tokenize(token): + split_tokens.append(sub_token) + + return split_tokens + + def convert_tokens_to_ids(self, tokens): + """convert_tokens_to_ids + """ + return convert_by_vocab(self.vocab, tokens) + + def convert_ids_to_tokens(self, ids): + """convert_ids_to_tokens + """ + return convert_by_vocab(self.inv_vocab, ids) + + +class BasicTokenizer(object): + """Runs basic tokenization (punctuation splitting, lower casing, etc.).""" + + def __init__(self, do_lower_case=True): + """Constructs a BasicTokenizer. + + Args: + do_lower_case: Whether to lower case the input. + """ + self.do_lower_case = do_lower_case + + def tokenize(self, text): + """Tokenizes a piece of text.""" + text = convert_to_unicode(text) + text = self._clean_text(text) + + # This was added on November 1st, 2018 for the multilingual and Chinese + # models. This is also applied to the English models now, but it doesn't + # matter since the English models were not trained on any Chinese data + # and generally don't have any Chinese data in them (there are Chinese + # characters in the vocabulary because Wikipedia does have some Chinese + # words in the English Wikipedia.). + text = self._tokenize_chinese_chars(text) + + orig_tokens = whitespace_tokenize(text) + split_tokens = [] + for token in orig_tokens: + if self.do_lower_case: + token = token.lower() + token = self._run_strip_accents(token) + split_tokens.extend(self._run_split_on_punc(token)) + + output_tokens = whitespace_tokenize(" ".join(split_tokens)) + return output_tokens + + def _run_strip_accents(self, text): + """Strips accents from a piece of text.""" + text = unicodedata.normalize("NFD", text) + output = [] + for char in text: + cat = unicodedata.category(char) + if cat == "Mn": + continue + output.append(char) + return "".join(output) + + def _run_split_on_punc(self, text): + """Splits punctuation on a piece of text.""" + chars = list(text) + i = 0 + start_new_word = True + output = [] + while i < len(chars): + char = chars[i] + if _is_punctuation(char): + output.append([char]) + start_new_word = True + else: + if start_new_word: + output.append([]) + start_new_word = False + output[-1].append(char) + i += 1 + + return ["".join(x) for x in output] + + def _tokenize_chinese_chars(self, text): + """Adds whitespace around any CJK character.""" + output = [] + for char in text: + cp = ord(char) + if self._is_chinese_char(cp): + output.append(" ") + output.append(char) + output.append(" ") + else: + output.append(char) + return "".join(output) + + def _is_chinese_char(self, cp): + """Checks whether CP is the codepoint of a CJK character.""" + # This defines a "chinese character" as anything in the CJK Unicode block: + # https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) + # + # Note that the CJK Unicode block is NOT all Japanese and Korean characters, + # despite its name. The modern Korean Hangul alphabet is a different block, + # as is Japanese Hiragana and Katakana. Those alphabets are used to write + # space-separated words, so they are not treated specially and handled + # like the all of the other languages. + if ((cp >= 0x4E00 and cp <= 0x9FFF) or # + (cp >= 0x3400 and cp <= 0x4DBF) or # + (cp >= 0x20000 and cp <= 0x2A6DF) or # + (cp >= 0x2A700 and cp <= 0x2B73F) or # + (cp >= 0x2B740 and cp <= 0x2B81F) or # + (cp >= 0x2B820 and cp <= 0x2CEAF) or + (cp >= 0xF900 and cp <= 0xFAFF) or # + (cp >= 0x2F800 and cp <= 0x2FA1F)): # + return True + + return False + + def _clean_text(self, text): + """Performs invalid character removal and whitespace cleanup on text.""" + output = [] + for char in text: + cp = ord(char) + if cp == 0 or cp == 0xfffd or _is_control(char): + continue + if _is_whitespace(char): + output.append(" ") + else: + output.append(char) + return "".join(output) + + +class WordpieceTokenizer(object): + """Runs WordPiece tokenziation.""" + + def __init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=100): + self.vocab = vocab + self.unk_token = unk_token + self.max_input_chars_per_word = max_input_chars_per_word + + def tokenize(self, text): + """Tokenizes a piece of text into its word pieces. + + This uses a greedy longest-match-first algorithm to perform tokenization + using the given vocabulary. + + For example: + input = "unaffable" + output = ["un", "##aff", "##able"] + + Args: + text: A single token or whitespace separated tokens. This should have + already been passed through `BasicTokenizer. + + Returns: + A list of wordpiece tokens. + """ + + text = convert_to_unicode(text) + + output_tokens = [] + for token in whitespace_tokenize(text): + chars = list(token) + if len(chars) > self.max_input_chars_per_word: + output_tokens.append(self.unk_token) + continue + + is_bad = False + start = 0 + sub_tokens = [] + while start < len(chars): + end = len(chars) + cur_substr = None + while start < end: + substr = "".join(chars[start:end]) + if start > 0: + substr = "##" + substr + if substr in self.vocab: + cur_substr = substr + break + end -= 1 + if cur_substr is None: + is_bad = True + break + sub_tokens.append(cur_substr) + start = end + + if is_bad: + output_tokens.append(self.unk_token) + else: + output_tokens.extend(sub_tokens) + return output_tokens + + +def _is_whitespace(char): + """Checks whether `chars` is a whitespace character.""" + # \t, \n, and \r are technically contorl characters but we treat them + # as whitespace since they are generally considered as such. + if char == " " or char == "\t" or char == "\n" or char == "\r": + return True + cat = unicodedata.category(char) + if cat == "Zs": + return True + return False + + +def _is_control(char): + """Checks whether `chars` is a control character.""" + # These are technically control characters but we count them as whitespace + # characters. + if char == "\t" or char == "\n" or char == "\r": + return False + cat = unicodedata.category(char) + if cat.startswith("C"): + return True + return False + + +def _is_punctuation(char): + """Checks whether `chars` is a punctuation character.""" + cp = ord(char) + # We treat all non-letter/number ASCII as punctuation. + # Characters such as "^", "$", and "`" are not in the Unicode + # Punctuation class but we treat them as punctuation anyways, for + # consistency. + if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or + (cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)): + return True + cat = unicodedata.category(char) + if cat.startswith("P"): + return True + return False + + +def tokenize_chinese_chars(text): + """Adds whitespace around any CJK character.""" + + def _is_chinese_char(cp): + """Checks whether CP is the codepoint of a CJK character.""" + # This defines a "chinese character" as anything in the CJK Unicode block: + # https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) + # + # Note that the CJK Unicode block is NOT all Japanese and Korean characters, + # despite its name. The modern Korean Hangul alphabet is a different block, + # as is Japanese Hiragana and Katakana. Those alphabets are used to write + # space-separated words, so they are not treated specially and handled + # like the all of the other languages. + if ((cp >= 0x4E00 and cp <= 0x9FFF) or # + (cp >= 0x3400 and cp <= 0x4DBF) or # + (cp >= 0x20000 and cp <= 0x2A6DF) or # + (cp >= 0x2A700 and cp <= 0x2B73F) or # + (cp >= 0x2B740 and cp <= 0x2B81F) or # + (cp >= 0x2B820 and cp <= 0x2CEAF) or + (cp >= 0xF900 and cp <= 0xFAFF) or # + (cp >= 0x2F800 and cp <= 0x2FA1F)): # + return True + + return False + + def _is_whitespace(c): + """_is_whitespace + """ + if c == " " or c == "\t" or c == "\r" or c == "\n" or ord(c) == 0x202F: + return True + return False + + output = [] + buff = "" + for char in text: + cp = ord(char) + if _is_chinese_char(cp) or _is_whitespace(char): + if buff != "": + output.append(buff) + buff = "" + output.append(char) + else: + buff += char + + if buff != "": + output.append(buff) + + return output diff --git a/applications/MultimodalVideoTag/scenario_lib/eval_and_save_model.py b/applications/MultimodalVideoTag/scenario_lib/eval_and_save_model.py new file mode 100755 index 0000000000000000000000000000000000000000..dc63b738e64e4abc4dd23d1992488ae41f3528ac --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/eval_and_save_model.py @@ -0,0 +1,158 @@ +""" +eval main +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import time +import argparse +import logging +import pickle + +import numpy as np +import paddle.fluid as fluid +import paddle +paddle.enable_static() + +from accuracy_metrics import MetricsCalculator +from datareader import get_reader +from config import parse_config, merge_configs, print_configs +from models.attention_lstm_ernie import AttentionLstmErnie +from utils import test_with_pyreader + + +def parse_args(): + """parse_args + """ + parser = argparse.ArgumentParser("Paddle Video evaluate script") + parser.add_argument('--model_name', + type=str, + default='BaiduNet', + help='name of model to train.') + parser.add_argument('--config', + type=str, + default='configs/conf.txt', + help='path to config file of model') + parser.add_argument( + '--pretrain', + type=str, + default=None, + help= + 'path to pretrain weights. None to use default weights path in ~/.paddle/weights.' + ) + parser.add_argument('--output', type=str, default=None, help='output path') + parser.add_argument('--use_gpu', + type=bool, + default=True, + help='default use gpu.') + parser.add_argument('--save_model_param_dir', + type=str, + default=None, + help='checkpoint path') + parser.add_argument('--save_inference_model', + type=str, + default=None, + help='save inference path') + parser.add_argument('--save_only', + action='store_true', + default=False, + help='only save model, do not evaluate model') + args = parser.parse_args() + return args + + +def evaluate(args): + """evaluate + """ + # parse config + config = parse_config(args.config) + valid_config = merge_configs(config, 'valid', vars(args)) + print_configs(valid_config, 'Valid') + + # build model + valid_model = AttentionLstmErnie(args.model_name, + valid_config, + mode='valid') + startup = fluid.Program() + valid_prog = fluid.default_main_program().clone(for_test=True) + with fluid.program_guard(valid_prog, startup): + with fluid.unique_name.guard(): + valid_model.build_input(True) + valid_model.build_model() + valid_feeds = valid_model.feeds() + valid_outputs = valid_model.outputs() + valid_loss = valid_model.loss() + valid_pyreader = valid_model.pyreader() + + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + exe.run(startup) + compiled_valid_prog = fluid.compiler.CompiledProgram(valid_prog) + + # load weights + assert os.path.exists(args.save_model_param_dir), \ + "Given save weight dir {} not exist.".format(args.save_model_param_dir) + valid_model.load_test_weights_file(exe, args.save_model_param_dir, + valid_prog, place) + + if args.save_inference_model: + save_model_params(exe, valid_prog, valid_model, + args.save_inference_model) + + if args.save_only is True: + print('save model only, exit') + return + + # get reader + bs_denominator = 1 + valid_config.VALID.batch_size = int(valid_config.VALID.batch_size / + bs_denominator) + valid_reader = get_reader(args.model_name.upper(), 'valid', valid_config) + + # get metrics + valid_metrics = MetricsCalculator(args.model_name.upper(), 'valid', + valid_config) + valid_fetch_list = [valid_loss.name] + [x.name for x in valid_outputs + ] + [valid_feeds[-1].name] + # get reader + exe_places = fluid.cuda_places() if args.use_gpu else fluid.cpu_places() + valid_pyreader.decorate_sample_list_generator(valid_reader, + places=exe_places) + + test_loss, metrics_dict_test = test_with_pyreader(exe, compiled_valid_prog, + valid_pyreader, + valid_fetch_list, + valid_metrics) + test_acc1 = metrics_dict_test['avg_acc1'] + print(test_loss) + print(test_acc1) + + +def save_model_params(exe, program, model_object, save_dir): + """save_model_params + """ + feeded_var_names = [var.name for var in model_object.feeds()][:-1] + fluid.io.save_inference_model(dirname=save_dir, + feeded_var_names=feeded_var_names, + main_program=program, + target_vars=model_object.outputs(), + executor=exe, + model_filename='model', + params_filename='params') + +if __name__ == "__main__": + args = parse_args() + evaluate(args) diff --git a/applications/MultimodalVideoTag/scenario_lib/inference.py b/applications/MultimodalVideoTag/scenario_lib/inference.py new file mode 100644 index 0000000000000000000000000000000000000000..e3abfbc5d07c273482e5840ae44dba35df4a3045 --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/inference.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +infer model +""" +import sys +import os +import numpy as np +import json +import pickle +import argparse +import time + +import numpy as np +import paddle.fluid as fluid +from paddle.inference import Config +from paddle.inference import create_predictor +from paddle.inference import Predictor + +from datareader import get_reader +from config import merge_configs, parse_config, print_configs + + +def parse_args(): + """parse_args + """ + parser = argparse.ArgumentParser("Paddle Video infer script") + parser.add_argument('--model_name', + type=str, + default='BaiduNet', + help='name of model to train.') + parser.add_argument('--config', + type=str, + default='configs/conf.txt', + help='path to config file of model') + parser.add_argument('--output', type=str, default=None, help='output path') + parser.add_argument('--use_gpu', + type=bool, + default=True, + help='default use gpu.') + parser.add_argument('--save_inference_model', + type=str, + default=None, + help='save inference path') + args = parser.parse_args() + return args + +class InferModel(object): + """lstm infer""" + def __init__(self, cfg, name='ACTION'): + name = name.upper() + self.name = name + self.threshold = cfg.INFER.threshold + self.cfg = cfg + self.label_map = load_class_file(cfg.MODEL.class_name_file) + + + def load_inference_model(self, model_dir, use_gpu=True): + """model_init + """ + model_file = os.path.join(model_dir, "model") + params_file = os.path.join(model_dir, "params") + config = Config(model_file, params_file) + if use_gpu: + config.enable_use_gpu(1024) + else: + config.disable_gpu() + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + # build input tensor and output tensor + self.build_input_output() + + def build_input_output(self): + """build_input_output + """ + input_names = self.predictor.get_input_names() + # input + self.input_rgb_tensor = self.predictor.get_input_handle(input_names[0]) + self.input_audio_tensor = self.predictor.get_input_handle(input_names[1]) + self.input_text_tensor = self.predictor.get_input_handle(input_names[2]) + + # output + output_names = self.predictor.get_output_names() + self.output_tensor = self.predictor.get_output_handle(output_names[0]) + + def preprocess_for_lod_data(self, input): + """pre process""" + input_arr = [] + input_lod = [0] + start_lod = 0 + end_lod = 0 + for sub_item in input: + end_lod = start_lod + len(sub_item) + input_lod.append(end_lod) + input_arr.extend(sub_item) + start_lod = end_lod + input_arr = np.array(input_arr) + return input_arr, [input_lod] + + def predict(self): + """predict""" + infer_reader = get_reader(self.name, 'infer', self.cfg) + probs = [] + video_ids = [] + label_map_inverse = {value: key for key, value in self.label_map.items()} + for infer_iter, data in enumerate(infer_reader()): + # video_id = [[items[-2], items[-1]] for items in data] + rgb = [items[0] for items in data] + audio = [items[1] for items in data] + text = np.array([items[2] for items in data]) + videos = np.array([items[3] for items in data]) + + rgb_arr, rgb_lod = self.preprocess_for_lod_data(rgb) + audio_arr, audio_lod = self.preprocess_for_lod_data(audio) + + self.input_rgb_tensor.copy_from_cpu(rgb_arr.astype('float32')) + self.input_rgb_tensor.set_lod(rgb_lod) + + self.input_audio_tensor.copy_from_cpu(audio_arr.astype('float32')) + self.input_audio_tensor.set_lod(audio_lod) + + self.input_text_tensor.copy_from_cpu(text.astype('int64')) + + self.predictor.run() + output = self.output_tensor.copy_to_cpu() + probs.extend(list(output)) + video_ids.extend(videos) + assert len(video_ids) == len(probs) + result = [] + for video_id, prob in zip(video_ids, probs): + label_idx = list(np.where(prob >= self.threshold)[0]) + result.append({ + "video_id": video_id, + "labels": [ + (label_map_inverse[str(idx)], float(prob[idx])) for idx in label_idx + ] + }) + return result + + +def load_class_file(class_file): + """ + load_class_file + """ + class_lines = open(class_file, 'r', encoding='utf8').readlines() + class_dict = {} + for i, line in enumerate(class_lines): + tmp = line.strip().split('\t') + word = tmp[0] + index = str(i) + if len(tmp) == 2: + index = tmp[1] + class_dict[word] = index + return class_dict + + +def infer(args): + """ + infer main + """ + config = parse_config(args.config) + infer_config = merge_configs(config, 'infer', vars(args)) + print_configs(infer_config, 'infer') + infer_obj = InferModel(infer_config, name=args.model_name) + infer_obj.load_inference_model(args.save_inference_model, use_gpu=args.use_gpu) + rt = infer_obj.predict() + if args.output: + with open(args.output, 'w') as f: + json.dump(rt, f, ensure_ascii=False, indent=4) + +if __name__ == "__main__": + args = parse_args() + infer(args) diff --git a/applications/MultimodalVideoTag/scenario_lib/models/attention_lstm_ernie.py b/applications/MultimodalVideoTag/scenario_lib/models/attention_lstm_ernie.py new file mode 100755 index 0000000000000000000000000000000000000000..f37d27058b486378765bbfc9685de68a434f485b --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/models/attention_lstm_ernie.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +attention lstm add ernie model +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import paddle.fluid as fluid +from paddle.fluid import ParamAttr +from .ernie import ErnieConfig, ErnieModel + + +class AttentionLstmErnie(object): + """ + Base on scenario-classify (image + audio), add text information + use ERNIE to extract text feature + """ + def __init__(self, name, cfg, mode='train'): + self.cfg = cfg + self.name = name + self.mode = mode + self.py_reader = None + self.get_config() + + def get_config(self): + """get_config + """ + # get model configs + self.feature_num = self.cfg.MODEL.feature_num + self.feature_names = self.cfg.MODEL.feature_names + self.feature_dims = self.cfg.MODEL.feature_dims + self.feature_dtypes = self.cfg.MODEL.feature_dtypes + self.feature_lod_level = self.cfg.MODEL.feature_lod_level + self.num_classes = self.cfg.MODEL.num_classes + self.embedding_size = self.cfg.MODEL.embedding_size + self.lstm_size_img = self.cfg.MODEL.lstm_size_img + self.lstm_size_audio = self.cfg.MODEL.lstm_size_audio + self.ernie_freeze = self.cfg.MODEL.ernie_freeze + self.lstm_pool_mode = self.cfg.MODEL.lstm_pool_mode + self.drop_rate = self.cfg.MODEL.drop_rate + self.loss_type = self.cfg.TRAIN.loss_type + self.ernie_pretrain_dict_path = self.cfg.TRAIN.ernie_pretrain_dict_path + + # get mode configs + self.batch_size = self.get_config_from_sec(self.mode, 'batch_size', 1) + self.num_gpus = self.get_config_from_sec(self.mode, 'num_gpus', 1) + + if self.mode == 'train': + self.learning_rate = self.get_config_from_sec( + 'train', 'learning_rate', 1e-3) + self.weight_decay = self.get_config_from_sec( + 'train', 'weight_decay', 8e-4) + self.num_samples = self.get_config_from_sec( + 'train', 'num_samples', 5000000) + self.decay_epochs = self.get_config_from_sec( + 'train', 'decay_epochs', [5]) + self.decay_gamma = self.get_config_from_sec( + 'train', 'decay_gamma', 0.1) + + def get_config_from_sec(self, sec, item, default=None): + """get_config_from_sec""" + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + def build_input(self, use_pyreader): + """ + build input + """ + self.feature_input = [] + for name, dim, dtype, lod_level in zip(self.feature_names, + self.feature_dims, + self.feature_dtypes, + self.feature_lod_level): + self.feature_input.append( + fluid.layers.data(shape=dim, + lod_level=lod_level, + dtype=dtype, + name=name)) + self.label_input = fluid.layers.data(shape=[self.num_classes], + dtype='float32', + name='label') + + self.py_reader = fluid.io.PyReader(feed_list=self.feature_input + + [self.label_input], + capacity=1024, + iterable=True) + + def ernie_encoder(self): + """ + text feature extractor + """ + ernie_config = ErnieConfig( + os.path.join(self.ernie_pretrain_dict_path, 'ernie_config.json')) + if self.mode != 'train': + ernie_config['attention_probs_dropout_prob'] = 0.0 + ernie_config['hidden_dropout_prob'] = 0.0 + + src_ids = self.feature_input[2][:, 0] + sent_ids = self.feature_input[2][:, 1] + position_ids = self.feature_input[2][:, 2] + task_ids = self.feature_input[2][:, 3] + input_mask = self.feature_input[2][:, 4].astype('float32') + ernie = ErnieModel(src_ids=src_ids, + position_ids=position_ids, + sentence_ids=sent_ids, + task_ids=task_ids, + input_mask=input_mask, + config=ernie_config) + enc_out = ernie.get_sequence_output() + # to Freeze ERNIE param + if self.ernie_freeze is True: + enc_out.stop_gradient = True + # ernie cnn + enc_out_cnn = ernie.get_sequence_textcnn_output(enc_out, input_mask) + + enc_out_cnn_drop = fluid.layers.dropout( + x=enc_out_cnn, + dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + return enc_out_cnn_drop + + def build_model(self): + """build_model + """ + # ---------------- transfer from old paddle --------------- + # get image,audio,text feature + video_input_tensor = self.feature_input[0] + audio_input_tensor = self.feature_input[1] + self.ernie_feature = self.ernie_encoder() + + # ------image------ + lstm_forward_fc = fluid.layers.fc(input=video_input_tensor, + size=self.lstm_size_img * 4, + act=None, + bias_attr=False) + lstm_forward, _ = fluid.layers.dynamic_lstm(input=lstm_forward_fc, + size=self.lstm_size_img * + 4, + is_reverse=False, + use_peepholes=True) + + lsmt_backward_fc = fluid.layers.fc(input=video_input_tensor, + size=self.lstm_size_img * 4, + act=None, + bias_attr=None) + lstm_backward, _ = fluid.layers.dynamic_lstm(input=lsmt_backward_fc, + size=self.lstm_size_img * + 4, + is_reverse=True, + use_peepholes=True) + + lstm_forward_img = fluid.layers.concat( + input=[lstm_forward, lstm_backward], axis=1) + + lstm_dropout = fluid.layers.dropout(x=lstm_forward_img, + dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + if self.lstm_pool_mode == 'text_guide': + lstm_weight = self.attention_weight_by_feature_seq2seq_attention( + self.ernie_feature, lstm_dropout, self.lstm_size_img * 2) + else: + lstm_weight = fluid.layers.fc(input=lstm_dropout, + size=1, + act='sequence_softmax', + bias_attr=None) + scaled = fluid.layers.elementwise_mul(x=lstm_dropout, + y=lstm_weight, + axis=0) + self.lstm_pool = fluid.layers.sequence_pool(input=scaled, + pool_type='sum') + # ------audio------ + lstm_forward_fc_audio = fluid.layers.fc( + input=audio_input_tensor, + size=self.lstm_size_audio * 4, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + lstm_forward_audio, _ = fluid.layers.dynamic_lstm( + input=lstm_forward_fc_audio, + size=self.lstm_size_audio * 4, + is_reverse=False, + use_peepholes=True) + + lsmt_backward_fc_audio = fluid.layers.fc(input=audio_input_tensor, + size=self.lstm_size_audio * 4, + act=None, + bias_attr=False) + lstm_backward_audio, _ = fluid.layers.dynamic_lstm( + input=lsmt_backward_fc_audio, + size=self.lstm_size_audio * 4, + is_reverse=True, + use_peepholes=True) + + lstm_forward_audio = fluid.layers.concat( + input=[lstm_forward_audio, lstm_backward_audio], axis=1) + + lstm_dropout_audio = fluid.layers.dropout( + x=lstm_forward_audio, + dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + if self.lstm_pool_mode == 'text_guide': + lstm_weight_audio = self.attention_weight_by_feature_seq2seq_attention( + self.ernie_feature, lstm_dropout_audio, + self.lstm_size_audio * 2) + else: + lstm_weight_audio = fluid.layers.fc(input=lstm_dropout_audio, + size=1, + act='sequence_softmax', + bias_attr=None) + scaled_audio = fluid.layers.elementwise_mul(x=lstm_dropout_audio, + y=lstm_weight_audio, + axis=0) + self.lstm_pool_audio = fluid.layers.sequence_pool(input=scaled_audio, + pool_type='sum') + + lstm_concat = fluid.layers.concat( + input=[self.lstm_pool, self.lstm_pool_audio, self.ernie_feature], + axis=1, + name='final_concat') + + # lstm_concat = self.add_bn(lstm_concat) + if self.loss_type == 'softmax': + self.fc = fluid.layers.fc(input=lstm_concat, + size=self.num_classes, + act='softmax') + elif self.loss_type == 'sigmoid': + self.fc = fluid.layers.fc(input=lstm_concat, + size=self.num_classes, + act=None) + self.logit = self.fc + self.fc = fluid.layers.sigmoid(self.fc) + + self.network_outputs = [self.fc] + + def attention_weight_by_feature_seq2seq_attention( + self, + text_feature, + sequence_feature, + sequence_feature_dim, + name_prefix="seq2seq_attention"): + """ + caculate weight by feature + Neural Machine Translation by Jointly Learning to Align and Translate + """ + text_feature_expand = fluid.layers.sequence_expand(text_feature, + sequence_feature, + ref_level=0) + sequence_text_concat = fluid.layers.concat( + [sequence_feature, text_feature_expand], + axis=-1, + name='video_text_concat') + energy = fluid.layers.fc(input=sequence_text_concat, + size=sequence_feature_dim, + act='tanh', + name=name_prefix + "_tanh_fc") + weight_vector = fluid.layers.fc(input=energy, + size=1, + act='sequence_softmax', + bias_attr=None, + name=name_prefix + "_softmax_fc") + return weight_vector + + def add_bn(self, lstm_concat): + """ + v2.5 add drop out and batch norm + """ + input_fc_proj = fluid.layers.fc( + input=lstm_concat, + size=8192, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + input_fc_proj_bn = fluid.layers.batch_norm( + input=input_fc_proj, + act="relu", + is_test=(not self.mode == 'train')) + input_fc_proj_dropout = fluid.layers.dropout( + x=input_fc_proj_bn, + dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + input_fc_hidden = fluid.layers.fc( + input=input_fc_proj_dropout, + size=4096, + act=None, + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0))) + input_fc_hidden_bn = fluid.layers.batch_norm( + input=input_fc_hidden, + act="relu", + is_test=(not self.mode == 'train')) + input_fc_hidden_dropout = fluid.layers.dropout( + x=input_fc_hidden_bn, + dropout_prob=self.drop_rate, + is_test=(not self.mode == 'train')) + return input_fc_hidden_dropout + + def optimizer(self): + """ + optimizer + """ + assert self.mode == 'train', "optimizer only can be get in train mode" + values = [ + self.learning_rate * (self.decay_gamma ** i) + for i in range(len(self.decay_epochs) + 1) + ] + iter_per_epoch = self.num_samples / self.batch_size + boundaries = [e * iter_per_epoch for e in self.decay_epochs] + return fluid.optimizer.RMSProp( + learning_rate=fluid.layers.piecewise_decay(values=values, + boundaries=boundaries), + centered=True, + regularization=fluid.regularizer.L2Decay( + regularization_coeff=self.weight_decay)) + + def softlabel_cross_entropy_loss(self): + """ + softlabel_cross_entropy_loss + """ + assert self.mode != 'infer', "invalid loss calculationg in infer mode" + ''' + cost = fluid.layers.cross_entropy(input=self.network_outputs[0], \ + label=self.label_input) + ''' + cost = fluid.layers.cross_entropy(input=self.network_outputs[0], \ + label=self.label_input, + soft_label=True) + + cost = fluid.layers.reduce_sum(cost, dim=-1) + sum_cost = fluid.layers.reduce_sum(cost) + self.loss_ = fluid.layers.scale(sum_cost, + scale=self.num_gpus, + bias_after_scale=False) + + return self.loss_ + + def sigmoid_cross_entropy_loss(self): + """ + sigmoid_cross_entropy_loss + """ + assert self.mode != 'infer', "invalid loss calculationg in infer mode" + cost = fluid.layers.sigmoid_cross_entropy_with_logits(x=self.logit,\ + label=self.label_input) + + cost = fluid.layers.reduce_sum(cost, dim=-1) + sum_cost = fluid.layers.reduce_sum(cost) + self.loss_ = fluid.layers.scale(sum_cost, + scale=self.num_gpus, + bias_after_scale=False) + + return self.loss_ + + def loss(self): + """ + loss + """ + if self.loss_type == 'sigmoid': + return self.sigmoid_cross_entropy_loss() + else: + return self.softlabel_cross_entropy_loss() + + def outputs(self): + """ + get outputs + """ + return self.network_outputs + + def feeds(self): + """ + get feeds + """ + return self.feature_input if self.mode == 'infer' else self.feature_input + [ + self.label_input + ] + + def pyreader(self): + """pyreader""" + return self.py_reader + + def epoch_num(self): + """get train epoch num""" + return self.cfg.TRAIN.epoch + + def load_test_weights_file(self, exe, weights, prog, place): + """ + load_test_weights_file + """ + load_vars = [x for x in prog.list_vars() \ + if isinstance(x, fluid.framework.Parameter)] + fluid.io.load_vars(exe, + dirname=weights, + vars=load_vars, + filename="param") diff --git a/applications/MultimodalVideoTag/scenario_lib/models/ernie.py b/applications/MultimodalVideoTag/scenario_lib/models/ernie.py new file mode 100755 index 0000000000000000000000000000000000000000..f64326c3b8c22ecab08c7413aad1d90415a428d9 --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/models/ernie.py @@ -0,0 +1,250 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Ernie model.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import absolute_import + +import json +import six +import logging +import paddle.fluid as fluid +from io import open +from paddle.fluid.layers import core + +from .transformer_encoder import encoder, pre_process_layer + +log = logging.getLogger(__name__) + +class ErnieConfig(object): + """ + Erine model config + """ + def __init__(self, config_path): + """ + init + """ + self._config_dict = self._parse(config_path) + + def _parse(self, config_path): + """ + parse config + """ + try: + with open(config_path, 'r', encoding='utf8') as json_file: + config_dict = json.load(json_file) + except Exception: + raise IOError("Error in parsing Ernie model config file '%s'" % + config_path) + else: + return config_dict + + def __getitem__(self, key): + """ + get item + """ + return self._config_dict.get(key, None) + def __setitem__(self, key, value): + """ + set item + """ + self._config_dict[key] = value + + def print_config(self): + """ + print config + """ + for arg, value in sorted(six.iteritems(self._config_dict)): + log.info('%s: %s' % (arg, value)) + log.info('------------------------------------------------') + + +class ErnieModel(object): + """ + ERINE Model + """ + def __init__(self, + src_ids, + position_ids, + sentence_ids, + task_ids, + input_mask, + config, + weight_sharing=True, + use_fp16=False): + """ + init model + """ + self._emb_size = config['hidden_size'] + self._n_layer = config['num_hidden_layers'] + self._n_head = config['num_attention_heads'] + self._voc_size = config['vocab_size'] + self._max_position_seq_len = config['max_position_embeddings'] + if config['sent_type_vocab_size']: + self._sent_types = config['sent_type_vocab_size'] + else: + self._sent_types = config['type_vocab_size'] + + self._use_task_id = config['use_task_id'] + if self._use_task_id: + self._task_types = config['task_type_vocab_size'] + self._hidden_act = config['hidden_act'] + self._prepostprocess_dropout = config['hidden_dropout_prob'] + self._attention_dropout = config['attention_probs_dropout_prob'] + self._weight_sharing = weight_sharing + + self._word_emb_name = "word_embedding" + self._pos_emb_name = "pos_embedding" + self._sent_emb_name = "sent_embedding" + self._task_emb_name = "task_embedding" + self._dtype = core.VarDesc.VarType.FP16 if use_fp16 else core.VarDesc.VarType.FP32 + self._emb_dtype = core.VarDesc.VarType.FP32 + + # Initialize all weigths by truncated normal initializer, and all biases + # will be initialized by constant zero by default. + self._param_initializer = fluid.initializer.TruncatedNormal( + scale=config['initializer_range']) + + self._build_model(src_ids, position_ids, sentence_ids, task_ids, + input_mask) + + def _build_model(self, src_ids, position_ids, sentence_ids, task_ids, + input_mask): + """ + build model + """ + # padding id in vocabulary must be set to 0 + emb_out = fluid.layers.embedding( + input=src_ids, + size=[self._voc_size, self._emb_size], + dtype=self._emb_dtype, + param_attr=fluid.ParamAttr( + name=self._word_emb_name, initializer=self._param_initializer), + is_sparse=False) + + position_emb_out = fluid.layers.embedding( + input=position_ids, + size=[self._max_position_seq_len, self._emb_size], + dtype=self._emb_dtype, + param_attr=fluid.ParamAttr( + name=self._pos_emb_name, initializer=self._param_initializer)) + + sent_emb_out = fluid.layers.embedding( + sentence_ids, + size=[self._sent_types, self._emb_size], + dtype=self._emb_dtype, + param_attr=fluid.ParamAttr( + name=self._sent_emb_name, initializer=self._param_initializer)) + + # emb_out = emb_out + position_emb_out + # emb_out = emb_out + sent_emb_out + emb_out = fluid.layers.elementwise_add(emb_out, position_emb_out, axis=0) + emb_out = fluid.layers.elementwise_add(emb_out, sent_emb_out, axis=0) + + if self._use_task_id: + task_emb_out = fluid.layers.embedding( + task_ids, + size=[self._task_types, self._emb_size], + dtype=self._emb_dtype, + param_attr=fluid.ParamAttr( + name=self._task_emb_name, + initializer=self._param_initializer)) + + emb_out = emb_out + task_emb_out + + emb_out = pre_process_layer( + emb_out, 'nd', self._prepostprocess_dropout, name='pre_encoder') + + if self._dtype == core.VarDesc.VarType.FP16: + emb_out = fluid.layers.cast(x=emb_out, dtype=self._dtype) + input_mask = fluid.layers.cast(x=input_mask, dtype=self._dtype) + self_attn_mask = fluid.layers.matmul( + x=input_mask, y=input_mask, transpose_y=True) + + self_attn_mask = fluid.layers.scale( + x=self_attn_mask, scale=10000.0, bias=-1.0, bias_after_scale=False) + n_head_self_attn_mask = fluid.layers.stack( + x=[self_attn_mask] * self._n_head, axis=1) + n_head_self_attn_mask.stop_gradient = True + + self._enc_out = encoder( + enc_input=emb_out, + attn_bias=n_head_self_attn_mask, + n_layer=self._n_layer, + n_head=self._n_head, + d_key=self._emb_size // self._n_head, + d_value=self._emb_size // self._n_head, + d_model=self._emb_size, + d_inner_hid=self._emb_size * 4, + prepostprocess_dropout=self._prepostprocess_dropout, + attention_dropout=self._attention_dropout, + relu_dropout=0, + hidden_act=self._hidden_act, + preprocess_cmd="", + postprocess_cmd="dan", + param_initializer=self._param_initializer, + name='encoder') + if self._dtype == core.VarDesc.VarType.FP16: + self._enc_out = fluid.layers.cast( + x=self._enc_out, dtype=self._emb_dtype) + + + def get_sequence_output(self): + """ + get sequence output + """ + return self._enc_out + + def get_sequence_textcnn_output(self, sequence_feature, input_mask): + """ + get sequence output + """ + seq_len = fluid.layers.reduce_sum(input_mask, dim=[1, 2]) + seq_len = fluid.layers.cast(seq_len, 'int64') + sequence_feature = fluid.layers.sequence_unpad(sequence_feature, seq_len) + + return self.textcnn(sequence_feature) + + def get_pooled_output(self): + """Get the first feature of each sequence for classification""" + next_sent_feat = fluid.layers.slice( + input=self._enc_out, axes=[1], starts=[0], ends=[1]) + next_sent_feat = fluid.layers.fc( + input=next_sent_feat, + size=self._emb_size, + act="tanh", + param_attr=fluid.ParamAttr( + name="pooled_fc.w_0", initializer=self._param_initializer), + bias_attr="pooled_fc.b_0") + return next_sent_feat + + def textcnn(self, feature, name='text_cnn'): + """ + TextCNN sequence feature extraction + """ + win_sizes = [2, 3, 4] + hid_dim = 256 + convs = [] + for win_size in win_sizes: + conv_h = fluid.nets.sequence_conv_pool(input=feature, + num_filters=hid_dim, + filter_size=win_size, + act="tanh", + pool_type="max") + convs.append(conv_h) + convs_out = fluid.layers.concat(input=convs, axis=1) + return convs_out diff --git a/applications/MultimodalVideoTag/scenario_lib/models/transformer_encoder.py b/applications/MultimodalVideoTag/scenario_lib/models/transformer_encoder.py new file mode 100755 index 0000000000000000000000000000000000000000..dbab84a140ca5fbdadb0fb7cae95f05b031ce49c --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/models/transformer_encoder.py @@ -0,0 +1,343 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Transformer encoder.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from functools import partial + +import paddle.fluid as fluid +import paddle.fluid.layers as layers + + +def multi_head_attention(queries, + keys, + values, + attn_bias, + d_key, + d_value, + d_model, + n_head=1, + dropout_rate=0., + cache=None, + param_initializer=None, + name='multi_head_att'): + """ + Multi-Head Attention. Note that attn_bias is added to the logit before + computing softmax activiation to mask certain selected positions so that + they will not considered in attention weights. + """ + keys = queries if keys is None else keys + values = keys if values is None else values + + if not (len(queries.shape) == len(keys.shape) == len(values.shape) == 3): + raise ValueError( + "Inputs: quries, keys and values should all be 3-D tensors.") + + def __compute_qkv(queries, keys, values, n_head, d_key, d_value): + """ + Add linear projection to queries, keys, and values. + """ + q = layers.fc(input=queries, + size=d_key * n_head, + num_flatten_dims=2, + param_attr=fluid.ParamAttr( + name=name + '_query_fc.w_0', + initializer=param_initializer), + bias_attr=name + '_query_fc.b_0') + k = layers.fc(input=keys, + size=d_key * n_head, + num_flatten_dims=2, + param_attr=fluid.ParamAttr( + name=name + '_key_fc.w_0', + initializer=param_initializer), + bias_attr=name + '_key_fc.b_0') + v = layers.fc(input=values, + size=d_value * n_head, + num_flatten_dims=2, + param_attr=fluid.ParamAttr( + name=name + '_value_fc.w_0', + initializer=param_initializer), + bias_attr=name + '_value_fc.b_0') + return q, k, v + + def __split_heads(x, n_head): + """ + Reshape the last dimension of inpunt tensor x so that it becomes two + dimensions and then transpose. Specifically, input a tensor with shape + [bs, max_sequence_length, n_head * hidden_dim] then output a tensor + with shape [bs, n_head, max_sequence_length, hidden_dim]. + """ + hidden_size = x.shape[-1] + # The value 0 in shape attr means copying the corresponding dimension + # size of the input as the output dimension size. + reshaped = layers.reshape( + x=x, shape=[0, 0, n_head, hidden_size // n_head], inplace=True) + + # permuate the dimensions into: + # [batch_size, n_head, max_sequence_len, hidden_size_per_head] + return layers.transpose(x=reshaped, perm=[0, 2, 1, 3]) + + def __combine_heads(x): + """ + Transpose and then reshape the last two dimensions of inpunt tensor x + so that it becomes one dimension, which is reverse to __split_heads. + """ + if len(x.shape) == 3: return x + if len(x.shape) != 4: + raise ValueError("Input(x) should be a 4-D Tensor.") + + trans_x = layers.transpose(x, perm=[0, 2, 1, 3]) + # The value 0 in shape attr means copying the corresponding dimension + # size of the input as the output dimension size. + return layers.reshape( + x=trans_x, + shape=[0, 0, trans_x.shape[2] * trans_x.shape[3]], + inplace=True) + + def scaled_dot_product_attention(q, k, v, attn_bias, d_key, dropout_rate): + """ + Scaled Dot-Product Attention + """ + scaled_q = layers.scale(x=q, scale=d_key**-0.5) + product = layers.matmul(x=scaled_q, y=k, transpose_y=True) + if attn_bias: + # product += attn_bias + product = fluid.layers.elementwise_add(product, attn_bias, axis=0) + weights = layers.softmax(product) + if dropout_rate: + weights = layers.dropout( + weights, + dropout_prob=dropout_rate, + dropout_implementation="upscale_in_train", + is_test=False) + out = layers.matmul(weights, v) + return out + + q, k, v = __compute_qkv(queries, keys, values, n_head, d_key, d_value) + + if cache is not None: # use cache and concat time steps + # Since the inplace reshape in __split_heads changes the shape of k and + # v, which is the cache input for next time step, reshape the cache + # input from the previous time step first. + k = cache["k"] = layers.concat( + [layers.reshape( + cache["k"], shape=[0, 0, d_model]), k], axis=1) + v = cache["v"] = layers.concat( + [layers.reshape( + cache["v"], shape=[0, 0, d_model]), v], axis=1) + + q = __split_heads(q, n_head) + k = __split_heads(k, n_head) + v = __split_heads(v, n_head) + + ctx_multiheads = scaled_dot_product_attention(q, k, v, attn_bias, d_key, + dropout_rate) + + out = __combine_heads(ctx_multiheads) + + # Project back to the model size. + proj_out = layers.fc(input=out, + size=d_model, + num_flatten_dims=2, + param_attr=fluid.ParamAttr( + name=name + '_output_fc.w_0', + initializer=param_initializer), + bias_attr=name + '_output_fc.b_0') + return proj_out + + +def positionwise_feed_forward(x, + d_inner_hid, + d_hid, + dropout_rate, + hidden_act, + param_initializer=None, + name='ffn'): + """ + Position-wise Feed-Forward Networks. + This module consists of two linear transformations with a ReLU activation + in between, which is applied to each position separately and identically. + """ + hidden = layers.fc(input=x, + size=d_inner_hid, + num_flatten_dims=2, + act=hidden_act, + param_attr=fluid.ParamAttr( + name=name + '_fc_0.w_0', + initializer=param_initializer), + bias_attr=name + '_fc_0.b_0') + if dropout_rate: + hidden = layers.dropout( + hidden, + dropout_prob=dropout_rate, + dropout_implementation="upscale_in_train", + is_test=False) + out = layers.fc(input=hidden, + size=d_hid, + num_flatten_dims=2, + param_attr=fluid.ParamAttr( + name=name + '_fc_1.w_0', initializer=param_initializer), + bias_attr=name + '_fc_1.b_0') + return out + + +def pre_post_process_layer(prev_out, out, process_cmd, dropout_rate=0., + name=''): + """ + Add residual connection, layer normalization and droput to the out tensor + optionally according to the value of process_cmd. + This will be used before or after multi-head attention and position-wise + feed-forward networks. + """ + for cmd in process_cmd: + if cmd == "a": # add residual connection + # out = out + prev_out if prev_out else out + out = fluid.layers.elementwise_add(out, prev_out, axis=0) if prev_out else out + elif cmd == "n": # add layer normalization + out_dtype = out.dtype + if out_dtype == fluid.core.VarDesc.VarType.FP16: + out = layers.cast(x=out, dtype="float32") + out = layers.layer_norm( + out, + begin_norm_axis=len(out.shape) - 1, + param_attr=fluid.ParamAttr( + name=name + '_layer_norm_scale', + initializer=fluid.initializer.Constant(1.)), + bias_attr=fluid.ParamAttr( + name=name + '_layer_norm_bias', + initializer=fluid.initializer.Constant(0.))) + if out_dtype == fluid.core.VarDesc.VarType.FP16: + out = layers.cast(x=out, dtype="float16") + elif cmd == "d": # add dropout + if dropout_rate: + out = layers.dropout( + out, + dropout_prob=dropout_rate, + dropout_implementation="upscale_in_train", + is_test=False) + return out + + +pre_process_layer = partial(pre_post_process_layer, None) +post_process_layer = pre_post_process_layer + + +def encoder_layer(enc_input, + attn_bias, + n_head, + d_key, + d_value, + d_model, + d_inner_hid, + prepostprocess_dropout, + attention_dropout, + relu_dropout, + hidden_act, + preprocess_cmd="n", + postprocess_cmd="da", + param_initializer=None, + name=''): + """The encoder layers that can be stacked to form a deep encoder. + This module consits of a multi-head (self) attention followed by + position-wise feed-forward networks and both the two components companied + with the post_process_layer to add residual connection, layer normalization + and droput. + """ + attn_output = multi_head_attention( + pre_process_layer( + enc_input, + preprocess_cmd, + prepostprocess_dropout, + name=name + '_pre_att'), + None, + None, + attn_bias, + d_key, + d_value, + d_model, + n_head, + attention_dropout, + param_initializer=param_initializer, + name=name + '_multi_head_att') + attn_output = post_process_layer( + enc_input, + attn_output, + postprocess_cmd, + prepostprocess_dropout, + name=name + '_post_att') + ffd_output = positionwise_feed_forward( + pre_process_layer( + attn_output, + preprocess_cmd, + prepostprocess_dropout, + name=name + '_pre_ffn'), + d_inner_hid, + d_model, + relu_dropout, + hidden_act, + param_initializer=param_initializer, + name=name + '_ffn') + return post_process_layer( + attn_output, + ffd_output, + postprocess_cmd, + prepostprocess_dropout, + name=name + '_post_ffn') + + +def encoder(enc_input, + attn_bias, + n_layer, + n_head, + d_key, + d_value, + d_model, + d_inner_hid, + prepostprocess_dropout, + attention_dropout, + relu_dropout, + hidden_act, + preprocess_cmd="n", + postprocess_cmd="da", + param_initializer=None, + name=''): + """ + The encoder is composed of a stack of identical layers returned by calling + encoder_layer. + """ + for i in range(n_layer): + enc_output = encoder_layer( + enc_input, + attn_bias, + n_head, + d_key, + d_value, + d_model, + d_inner_hid, + prepostprocess_dropout, + attention_dropout, + relu_dropout, + hidden_act, + preprocess_cmd, + postprocess_cmd, + param_initializer=param_initializer, + name=name + '_layer_' + str(i)) + enc_input = enc_output + enc_output = pre_process_layer( + enc_output, preprocess_cmd, prepostprocess_dropout, name="post_encoder") + + return enc_output diff --git a/applications/MultimodalVideoTag/scenario_lib/train.py b/applications/MultimodalVideoTag/scenario_lib/train.py new file mode 100755 index 0000000000000000000000000000000000000000..7d366eb959f4e219e6d51d9ced425c420ee45604 --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/train.py @@ -0,0 +1,261 @@ +""" +train main +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import time +import argparse +import logging + + +import numpy as np +import paddle.fluid as fluid +import paddle +paddle.enable_static() + +from accuracy_metrics import MetricsCalculator +from datareader import get_reader +from config import print_configs, merge_configs, parse_config +from models.attention_lstm_ernie import AttentionLstmErnie +from utils import init_pretraining_params, train_with_pyreader + + +logging.root.handlers = [] +FORMAT = '[%(levelname)s: %(filename)s: %(lineno)4d]: %(message)s' +logging.basicConfig(level=logging.INFO, format=FORMAT, stream=sys.stdout) +logger = logging.getLogger(__name__) + + +def parse_args(): + """parse_args + """ + parser = argparse.ArgumentParser("Paddle Video train script") + parser.add_argument( + '--model_name', + type=str, + default='BaiduNet', + help='name of model to train.') + parser.add_argument( + '--config', + type=str, + default='configs/conf.txt', + help='path to config file of model') + parser.add_argument( + '--batch_size', + type=int, + default=None, + help='training batch size. None to use config file setting.') + parser.add_argument( + '--learning_rate', + type=float, + default=None, + help='learning rate use for training. None to use config file setting.') + parser.add_argument( + '--pretrain', + type=str, + default=None, + help='path to pretrain weights. None to use default weights path in ~/.paddle/weights.' + ) + parser.add_argument( + '--resume', + type=str, + default=None, + help='path to resume training based on previous checkpoints. ' + 'None for not resuming any checkpoints.') + parser.add_argument( + '--use_gpu', type=bool, default=True, help='default use gpu.') + parser.add_argument( + '--no_use_pyreader', + action='store_true', + default=False, + help='whether to use pyreader') + parser.add_argument( + '--no_memory_optimize', + action='store_true', + default=False, + help='whether to use memory optimize in train') + parser.add_argument( + '--epoch_num', + type=int, + default=0, + help='epoch number, 0 for read from config file') + parser.add_argument( + '--valid_interval', + type=int, + default=1, + help='validation epoch interval, 0 for no validation.') + parser.add_argument( + '--save_dir', + type=str, + default='checkpoints', + help='directory name to save train snapshoot') + parser.add_argument( + '--log_interval', + type=int, + default=10, + help='mini-batch interval to log.') + parser.add_argument( + '--save_log_name', + type=str, + default='train_val', + help='save to tensorboard filename recommand model name.') + args = parser.parse_args() + return args + + +def train(args): + """train main + """ + # parse config + config = parse_config(args.config) + train_config = merge_configs(config, 'train', vars(args)) + valid_config = merge_configs(config, 'valid', vars(args)) + print_configs(train_config, 'Train') + train_model = AttentionLstmErnie(args.model_name, train_config, mode='train') + valid_model = AttentionLstmErnie(args.model_name, valid_config, mode='valid') + + max_train_steps = train_config.TRAIN.epoch * train_config.TRAIN.num_samples // train_config.TRAIN.batch_size + print('max train steps %d' % (max_train_steps)) + # build model + startup = fluid.Program() + train_prog = fluid.Program() + with fluid.program_guard(train_prog, startup): + with fluid.unique_name.guard(): + train_model.build_input(use_pyreader=True) + train_model.build_model() + # for the input, has the form [data1, data2,..., label], so train_feeds[-1] is label + train_feeds = train_model.feeds() + train_feeds[-1].persistable = True + # for the output of classification model, has the form [pred] + train_outputs = train_model.outputs() + for output in train_outputs: + output.persistable = True + train_loss = train_model.loss() + train_loss.persistable = True + # outputs, loss, label should be fetched, so set persistable to be true + optimizer = train_model.optimizer() + optimizer.minimize(train_loss) + train_pyreader = train_model.pyreader() + + if not args.no_memory_optimize: + fluid.memory_optimize(train_prog) + + valid_prog = fluid.Program() + with fluid.program_guard(valid_prog, startup): + with fluid.unique_name.guard(): + valid_model.build_input(True) + valid_model.build_model() + valid_feeds = valid_model.feeds() + valid_outputs = valid_model.outputs() + valid_loss = valid_model.loss() + valid_pyreader = valid_model.pyreader() + + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + exe.run(startup) + + if args.resume: + # if resume weights is given, load resume weights directly + assert os.path.exists(args.resume), \ + "Given resume weight dir {} not exist.".format(args.resume) + + def if_exist(var): + """if_exist + """ + return os.path.exists(os.path.join(args.resume, var.name)) + + print('resuming ,,,,,,,,,,,,,,') + fluid.io.load_persistables( + exe, '', main_program=train_prog, filename=args.resume) + + else: + # load ernie pretrain model + init_pretraining_params(exe, + train_config.TRAIN.ernie_pretrain_dict_path, + main_program=train_prog) + # if not in resume mode, load pretrain weights + # this pretrain may be only audio or video + if args.pretrain: + assert os.path.exists(args.pretrain), \ + "Given pretrain weight dir {} not exist.".format(args.pretrain) + if args.pretrain: + train_model.load_test_weights_file(exe, args.pretrain, train_prog, place) + + build_strategy = fluid.BuildStrategy() + build_strategy.enable_inplace = True + + compiled_train_prog = fluid.compiler.CompiledProgram( + train_prog).with_data_parallel(loss_name=train_loss.name, + build_strategy=build_strategy) + compiled_valid_prog = fluid.compiler.CompiledProgram( + valid_prog).with_data_parallel(share_vars_from=compiled_train_prog, + build_strategy=build_strategy) + + # get reader + bs_denominator = 1 + if (not args.no_use_pyreader) and args.use_gpu: + dev_list = fluid.cuda_places() + bs_denominator = len(dev_list) + train_config.TRAIN.batch_size = int(train_config.TRAIN.batch_size / + bs_denominator) + valid_config.VALID.batch_size = int(valid_config.VALID.batch_size / + bs_denominator) + train_reader = get_reader(args.model_name.upper(), 'train', train_config) + valid_reader = get_reader(args.model_name.upper(), 'valid', valid_config) + + exe_places = fluid.cuda_places() if args.use_gpu else fluid.cpu_places() + train_pyreader.decorate_sample_list_generator(train_reader, + places=exe_places) + valid_pyreader.decorate_sample_list_generator(valid_reader, + places=exe_places) + + # get metrics + train_metrics = MetricsCalculator(args.model_name.upper(), 'train', train_config) + valid_metrics = MetricsCalculator(args.model_name.upper(), 'valid', valid_config) + # print("****************************valid_metrics", valid_metrics.get()) + train_fetch_list = [train_loss.name] + [x.name for x in train_outputs + ] + [train_feeds[-1].name] + valid_fetch_list = [valid_loss.name] + [x.name for x in valid_outputs + ] + [valid_feeds[-1].name] + + epochs = args.epoch_num or train_model.epoch_num() + + train_with_pyreader( + exe, + train_prog, + compiled_train_prog, + train_pyreader, + train_fetch_list, + train_metrics, + epochs=epochs, + log_interval=args.log_interval, + valid_interval=args.valid_interval, + save_dir=args.save_dir, + save_model_name=args.model_name, + test_exe=compiled_valid_prog, + test_pyreader=valid_pyreader, + test_fetch_list=valid_fetch_list, + test_metrics=valid_metrics) + +if __name__ == "__main__": + args = parse_args() + logger.info(args) + + if not os.path.exists(args.save_dir): + os.makedirs(args.save_dir) + + train(args) diff --git a/applications/MultimodalVideoTag/scenario_lib/utils.py b/applications/MultimodalVideoTag/scenario_lib/utils.py new file mode 100755 index 0000000000000000000000000000000000000000..9b47d990e99cf97a12807599575499f0a0e95bff --- /dev/null +++ b/applications/MultimodalVideoTag/scenario_lib/utils.py @@ -0,0 +1,215 @@ +""" +utils +""" +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import time +import traceback +import logging +import shutil + +import numpy as np +import paddle.fluid as fluid + +logger = logging.getLogger(__name__) + + +def test_with_pyreader(exe, + compiled_test_prog, + test_pyreader, + test_fetch_list, + test_metrics, + log_interval=0): + """test_with_pyreader + """ + if not test_pyreader: + logger.error("[TEST] get pyreader failed.") + test_metrics.reset() + test_iter = 0 + label_all = [] + pred_all = [] + try: + for data in test_pyreader(): + test_outs = exe.run(compiled_test_prog, + fetch_list=test_fetch_list, + feed=data) + loss = np.array(test_outs[0]) + pred = np.array(test_outs[1]) + label = np.array(test_outs[-1]) + pred_all.extend(pred) + label_all.extend(label) + test_metrics.accumulate(loss, pred, label) + test_iter += 1 + test_metrics.finalize_and_log_out("[TEST] Finish") + except Exception as e: + logger.warn( + "[TEST] fail to execute test or calculate metrics: {}".format(e)) + traceback.print_exc() + metrics_dict, test_loss = test_metrics.get_computed_metrics() + metrics_dict['label_all'] = label_all + metrics_dict['pred_all'] = pred_all + return test_loss, metrics_dict + + +def train_with_pyreader(exe, train_prog, compiled_train_prog, train_pyreader, + train_fetch_list, train_metrics, epochs=10, + log_interval=0, valid_interval=0, + save_dir='./', save_model_name='model', + test_exe=None, test_pyreader=None, + test_fetch_list=None, test_metrics=None): + """train_with_pyreader + """ + if not train_pyreader: + logger.error("[TRAIN] get pyreader failed.") + EARLY_STOP_NUM = 20 + early_stop = EARLY_STOP_NUM + global_iter = 0 + train_iter = 0 + iter_all = 0 + best_test_acc1 = 0 + + for epoch in range(epochs): + lr = fluid.global_scope().find_var("learning_rate").get_tensor() + logger.info( + "------- learning rate {}, learning rate counter -----".format( + np.array(lr))) + if early_stop < 0: + logger.info('Earyly Stop !!!') + break + train_metrics.reset() + global_iter += train_iter + epoch_periods = [] + for data in train_pyreader(): + try: + cur_time = time.time() + train_outs = exe.run(compiled_train_prog, + fetch_list=train_fetch_list, + feed=data) + iter_all += 1 + period = time.time() - cur_time + epoch_periods.append(period) + loss = np.array(train_outs[0]) + pred = np.array(train_outs[1]) + label = np.array(train_outs[-1]) + train_metrics.accumulate(loss, pred, label) + if log_interval > 0 and (train_iter % log_interval == 0): + # eval here + train_metrics.finalize_and_log_out( + info='[TRAIN] Epoch {} iter {} everage: '.format(epoch, train_iter)) + train_iter += 1 + except Exception as e: + logger.info( + "[TRAIN] Epoch {}, iter {} data training failed: {}". + format(epoch, train_iter, str(e))) + if len(epoch_periods) < 1: + logger.info( + 'No iteration was executed, please check the data reader') + sys.exit(1) + + logger.info( + '[TRAIN] Epoch {} training finished, average time: {}'.format( + epoch, np.mean(epoch_periods))) + train_metrics.finalize_and_log_out( \ + info='[TRAIN] Finished ... Epoch {} all iters average: '.format(epoch)) + + # save models of min loss in best acc epochs + if test_exe and valid_interval > 0 and (epoch + + 1) % valid_interval == 0: + # metrics_dict,loss = train_metrics.calculator.get_computed_metrics() + loss, metrics_dict_test = test_with_pyreader( + exe, test_exe, test_pyreader, test_fetch_list, test_metrics, + log_interval) + test_acc1 = metrics_dict_test['avg_acc1'] + if test_acc1 > best_test_acc1: + best_test_acc1 = test_acc1 + save_model(exe, train_prog, save_dir, save_model_name, + "_epoch{}_acc{}".format(epoch, best_test_acc1)) + early_stop = EARLY_STOP_NUM + else: + early_stop -= 1 + + +def save_model(exe, program, save_dir, model_name, postfix=None): + """save_model + """ + model_path = os.path.join(save_dir, model_name + postfix) + if os.path.isdir(model_path): + shutil.rmtree(model_path) + # fluid.io.save_persistables(exe, model_path, main_program=program) + save_vars = [x for x in program.list_vars() \ + if isinstance(x, fluid.framework.Parameter)] + + fluid.io.save_vars(exe, + dirname=model_path, + main_program=program, + vars=save_vars, + filename="param") + + +def save_model_persist(exe, program, save_dir, model_name, postfix=None): + """save_model""" + model_path = os.path.join(save_dir, model_name + postfix) + if os.path.isdir(model_path): + shutil.rmtree(model_path) + fluid.io.save_persistables(exe, + save_dir, + main_program=program, + filename=model_path) + + +def init_pretraining_params(exe, + pretraining_params_path, + main_program, + use_fp16=False): + """ + init pretrain_params + """ + assert os.path.exists(pretraining_params_path + ), "[%s] cann't be found." % pretraining_params_path + + def existed_params(var): + """ + Load existed params + """ + if not isinstance(var, fluid.framework.Parameter): + return False + flag = os.path.exists(os.path.join(pretraining_params_path, var.name)) + return flag + + fluid.io.load_vars(exe, + pretraining_params_path, + main_program=main_program, + predicate=existed_params) + logger.info( + "Load pretraining parameters from {}.".format(pretraining_params_path)) + + +class AttrDict(dict): + """AttrDict + """ + def __getattr__(self, key): + """getter + """ + return self[key] + + def __setattr__(self, key, value): + """setter + """ + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value diff --git a/applications/MultimodalVideoTag/train.sh b/applications/MultimodalVideoTag/train.sh new file mode 100755 index 0000000000000000000000000000000000000000..05db2585e2fa1239512a70ed0df4a33db0dcec6f --- /dev/null +++ b/applications/MultimodalVideoTag/train.sh @@ -0,0 +1,13 @@ +export CUDA_VISIBLE_DEVICES=0,1 +export FLAGS_eager_delete_tensor_gb=0.0 +export FLAGS_sync_nccl_allreduce=1 +export FLAGS_fast_eager_deletion_mode=1 +export FLAGS_fraction_of_gpu_memory_to_use=0.5 +export FLAGS_reallocate_gpu_memory_in_mb=0 +export FLAGS_memory_fraction_of_eager_deletion=1 +python scenario_lib/train.py --model_name=AttentionLstmErnie \ +--config=./conf/conf.txt \ +--log_interval=20 \ +--valid_interval=1 \ +--save_dir=checkpoints_save_new/ \ +--pretrain=checkpoints_save/ diff --git a/applications/PP-Care/Readme.md b/applications/PP-Care/Readme.md new file mode 100644 index 0000000000000000000000000000000000000000..61498fe9674358b9ef8dafa6e229371f39e26f3e --- /dev/null +++ b/applications/PP-Care/Readme.md @@ -0,0 +1,110 @@ +# Video models for 3DMRI + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [实现细节](#实现细节) +- [参考论文](#参考论文) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install SimpleITK +``` + +## 模型简介 + +目前对于医学3D数据如MRI,并无太好的处理手段,大多数2D模型无法获得3D空间层面的特征,而常用的3D模型又需要较大的计算成本。而同时,3D医学数据与常见的视频数据有一定相似之处,我们尝试了通过PaddleVideo中的常见模型解决医学3DMRI数据的分类问题,获得了较好的结果。目前支持PP-TSN、PP-TSM、Slowfast和Timesformer对3DMRI的直接训练。 + +## 数据准备 + +数据集包括帕金森患者(PD)与正常(Con)两种类型共378个case,训练集:测试集=300:78,使用数据均为公开数据集,包括*neurocon*, *taowu*, *PPMI*和*OASIS-1*(经过选取),并经过一定格式转换,数据最后的格式均为*name.nii*或*name.nii.gz*,路径与label信息通过txt文件保存,数据集可以通过百度网盘下载:[下载链接](https://pan.baidu.com/s/1eIsHHqnkKNG5x9CGjRONEA?pwd=avug) +- 数据集label格式 +``` +{ + "0": "Con", + "1": "PD" +} +``` +- 数据集信息文件格式 +``` +{ + path1 label1 + path2 label2 + ... +} +``` +- 数据保存格式 +``` +{ + |-- datasets + |-- neurocon + |-- taowu + |-- PPMI + |-- OASIS-1 +} +``` + +## 模型训练 + +#### 下载并添加预训练模型 + +1. 对于PP-TSN与PP-TSM,除了可以使用ImageNet1000上训练好的预训练模型(见[PP-TSN预训练模型](../../../docs/zh-CN/model_zoo/recognition/pp-tsn.md)与[PP-TSM预训练模型](../../../docs/zh-CN/model_zoo/recognition/pp-tsm.md)),也可以使用在MRI数据集上预训练的ResNet50权重座位Backbone初始化参数,通过百度网盘下载: [下载链接](https://pan.baidu.com/s/1eIsHHqnkKNG5x9CGjRONEA?pwd=avug)。对于Slowfast与TimeSformer,目前只支持是使用自然数据集的预训练模型,见[Slowfast预训练模型](../../../docs/zh-CN/model_zoo/recognition/slowfast.md)与[Timesformer预训练模型](../../../docs/zh-CN/model_zoo/recognition/timesformer.md) + + +2. 打开`PaddleVideo/applications/PP-Care/configs/XXX.yaml`,将下载好的权重路径填写到下方`pretrained:`之后,以pptsn_MRI为例 + + ```yaml + MODEL: + framework: "RecognizerMRI" + backbone: + name: "ResNetTSN_MRI" + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- 训练使用显卡数量与输出路径等信息均可以选择,以PP-TSN_MRI的4卡训练为例,训练启动命令如下 + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_pptsn_MRI main.py --validate -c applications/PP-Care/configs/pptsn_MRI.yaml + ``` + +## 模型测试 + +由于各模型均存在随机采样部分,且采样方式存在不同,所以训练日志中记录的验证指标`topk Acc`不代表最终的测试分数,因此在训练完成之后可以用测试模式对最好的模型进行测试获取最终的指标,以PP-TSN_MRI为例,命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_pptsn_MRI main.py --test -c applications/PP-Care/configs/pptsn_MRI.yaml -w "output/ppTSN_MRI/ppTSN_MRI_best.pdparams" +``` + +当测试配置采用.yaml中参数时,在3DMRI数据的validation数据集上的测试指标如下: + +| backbone | head | Acc | +| :----------------: | :----------: | :---: | +| ResNet50 | PP-TSN | 91.07 | +| ResNet50 | PP-TSM | 90.83 | +| 3DResNet50 | Slowfast | 91.07 | +| Vision Transformer | Timesformer | 88.33 | + +训练好的模型可以通过百度网盘下载:[下载链接](https://pan.baidu.com/s/1eIsHHqnkKNG5x9CGjRONEA?pwd=avug) + + +## 模型优化 +在实际使用中,可以尝试模型优化策略 +- 可以根据MRI数据分布,调整采样率 +- 本模型目前未加入过多的数据预处理策略,针对不同数据特性,在本模型基础上加入一定的预处理手段可能会使结果继续提升 +- 由于数据量与任务难度限制,本模型目前在准确率上的表现与3DResNet并无显著区别,但对于时间与空间的需求均远小于3D模型 + + +## 参考论文 + +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/pdf/1608.00859.pdf), Limin Wang, Yuanjun Xiong, Zhe Wang +- [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf), Ji Lin, Chuang Gan, Song Han +- [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531), Geoffrey Hinton, Oriol Vinyals, Jeff Dean +- [SlowFast Networks for Video Recognition](https://arxiv.org/abs/1812.03982), Feichtenhofer C, Fan H, Malik J, et al. +- [A Multigrid Method for Efficiently Training Video Models](https://arxiv.org/abs/1912.00998), Chao-Yuan Wu, Ross Girshick, et al. +- [Is Space-Time Attention All You Need for Video Understanding?](https://arxiv.org/pdf/2102.05095.pdf), Gedas Bertasius, Heng Wang, Lorenzo Torresani diff --git a/applications/PP-Care/configs/pptsm_MRI.yaml b/applications/PP-Care/configs/pptsm_MRI.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e2774fb599a9bbdb4beb70917f15117c278bec2b --- /dev/null +++ b/applications/PP-Care/configs/pptsm_MRI.yaml @@ -0,0 +1,98 @@ +MODEL: + framework: "RecognizerMRI" + backbone: + name: "ResNetTSM_MRI" + pretrained: "" + depth: 50 + head: + name: "ppTSMHead" + num_classes: 2 + in_channels: 2048 + drop_ratio: 0.5 + std: 0.01 + ls_eps: 0.1 + + +DATASET: + batch_size: 16 + num_workers: 4 + train: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/train.txt" + + valid: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/test.txt" + + test: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/test.txt" + + +PIPELINE: + train: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + select_left: True + + valid: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + + test: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 16 + seg_len: 1 + valid_mode: True + select_left: True + +OPTIMIZER: + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 50 + warmup_epochs: 5 + warmup_start_lr: 0.005 + cosine_base_lr: 0.05 + weight_decay: + name: 'L2' + value: 0.00005 + use_nesterov: True + + +MIX: + name: "Mixup" + alpha: 0.2 + +PRECISEBN: + preciseBN_interval: 5 + num_iters_preciseBN: 200 + + +METRIC: + name: 'CenterCropMetric_MRI' + + +model_name: "ppTSM_MRI" +log_interval: 20 +epochs: 50 +log_level: "INFO" diff --git a/applications/PP-Care/configs/pptsn_MRI.yaml b/applications/PP-Care/configs/pptsn_MRI.yaml new file mode 100644 index 0000000000000000000000000000000000000000..87f4abb56525775b422e6f6030d2cd83a270d004 --- /dev/null +++ b/applications/PP-Care/configs/pptsn_MRI.yaml @@ -0,0 +1,97 @@ +MODEL: + framework: "RecognizerMRI" + backbone: + name: "ResNetTSN_MRI" + pretrained: "" + layers: 50 + head: + name: "ppTSNHead" + num_classes: 2 + in_channels: 2048 + drop_ratio: 0.4 + std: 0.01 + ls_eps: 0.1 + + +DATASET: + batch_size: 16 + valid_batch_size: 16 + test_batch_size: 16 + num_workers: 4 + train: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/train.txt" + + valid: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/test.txt" + + test: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/test.txt" + + +PIPELINE: + train: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + select_left: True + + valid: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + + test: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 16 + seg_len: 1 + valid_mode: True + select_left: True + +OPTIMIZER: + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 50 + warmup_epochs: 5 + warmup_start_lr: 0.005 + cosine_base_lr: 0.01 + weight_decay: + name: 'L2' + value: 0.00005 + use_nesterov: True + + +MIX: + name: "Mixup" + alpha: 0.2 + + +METRIC: + name: 'CenterCropMetric_MRI' + + +model_name: "ppTSN_MRI" +log_interval: 20 +save_interval: 20 +epochs: 50 +log_level: "INFO" diff --git a/applications/PP-Care/configs/slowfast_MRI.yaml b/applications/PP-Care/configs/slowfast_MRI.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a4f6771d91435058a00e73200b1d5543f11c9be6 --- /dev/null +++ b/applications/PP-Care/configs/slowfast_MRI.yaml @@ -0,0 +1,93 @@ + +MODEL: + framework: "Recognizer3DMRI" + backbone: + name: "ResNetSlowFast_MRI" + depth: 50 + alpha: 8 + beta: 8 + width_per_group: 64 + fusion_kernel_sz: 5 + head: + name: "SlowFastHead" + width_per_group: 64 + alpha: 8 + beta: 8 + num_classes: 2 + num_frames: 32 + crop_size: 224 + dropout_rate: 0.5 + + +DATASET: + batch_size: 16 + test_batch_size: 16 + num_workers: 0 + train: + format: "SFMRIDataset" + data_prefix: "" + file_path: "/home/aistudio/train.txt" + + valid: + format: "SFMRIDataset" + data_prefix: "" + file_path: "/home/aistudio/test.txt" + + test: + format: "SFMRIDataset" + data_prefix: "" + file_path: "/home/aistudio/test.txt" + + +PIPELINE: + train: + decode_sampler_MRI: + name: "SFMRI_DecodeSampler" + num_seg: [4,32] + seg_len: 1 + valid_mode: False + select_left: True + + valid: + decode_sampler_MRI: + name: "SFMRI_DecodeSampler" + num_seg: [4,32] + seg_len: 1 + valid_mode: True + select_left: True + test: + decode_sampler_MRI: + name: "SFMRI_DecodeSampler" + num_seg: [4,32] + seg_len: 1 + valid_mode: True + select_left: True + +OPTIMIZER: + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 196 + warmup_epochs: 34 + warmup_start_lr: 0.01 + cosine_base_lr: 0.1 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + +METRIC: + name: 'CenterCropMetric_MRI' + if_slowfast: 1 + +PRECISEBN: + preciseBN_interval: 10 + num_iters_preciseBN: 200 + +model_name: SlowFast_MRI +save_interval: 10 +val_interval: 10 +epochs: 50 #Mandatory, total epoch +log_level: "INFO" diff --git a/applications/PP-Care/configs/timesformer_MRI.yaml b/applications/PP-Care/configs/timesformer_MRI.yaml new file mode 100644 index 0000000000000000000000000000000000000000..03b411e5ec5659a77f55510726f2e6a94e96d51f --- /dev/null +++ b/applications/PP-Care/configs/timesformer_MRI.yaml @@ -0,0 +1,93 @@ +MODEL: + framework: "RecognizerTransformer_MRI" + backbone: + name: "VisionTransformer" + pretrained: "" + img_size: 224 + patch_size: 16 + in_channels: 1 + embed_dim: 768 + depth: 12 + num_heads: 12 + mlp_ratio: 4 + qkv_bias: True + epsilon: 1e-6 + seg_num: 8 + attention_type: 'divided_space_time' + head: + name: "TimeSformerHead" + num_classes: 2 + in_channels: 768 + std: 0.02 + +DATASET: + batch_size: 8 + num_workers: 4 + test_batch_size: 8 + train: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/train.txt" + valid: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/test.txt" + test: + format: "MRIDataset" + data_prefix: "" + file_path: "/home/aistudio/test.txt" + +PIPELINE: + train: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + linspace_sample: True + + valid: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + linspace_sample: True + + test: + decode: + name: "MRIDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + linspace_sample: True + +OPTIMIZER: + name: 'Adam' + learning_rate: + learning_rate: 0.005 + name: 'MultiStepDecay' + milestones: [20, 29] + gamma: 0.9 + weight_decay: + name: 'L2' + value: 0.00005 + + +GRADIENT_ACCUMULATION: + global_batch_size: 64 + +METRIC: + name: 'CenterCropMetric_MRI' + +model_name: "TimeSformer_MRI" +log_interval: 20 +save_interval: 5 +epochs: 30 +log_level: "INFO" diff --git a/applications/PPHuman/README.md b/applications/PPHuman/README.md new file mode 100644 index 0000000000000000000000000000000000000000..24659c9f356306446dc34d85d2ffcd446ea00bc1 --- /dev/null +++ b/applications/PPHuman/README.md @@ -0,0 +1,143 @@ +# PP-Human 行为识别模型 + +实时行人分析工具[PP-Human](https://github.com/PaddlePaddle/PaddleDetection/tree/release/2.4/deploy/pphuman)中集成了基于骨骼点的行为识别模块。本文档介绍如何基于[PaddleVideo](https://github.com/PaddlePaddle/PaddleVideo/),完成行为识别模型的训练流程。 + +## 行为识别模型训练 +目前行为识别模型使用的是[ST-GCN](https://arxiv.org/abs/1801.07455),并在[PaddleVideo训练流程](https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/model_zoo/recognition/stgcn.md)的基础上修改适配,完成模型训练。 + +### 准备训练数据 +STGCN是一个基于骨骼点坐标序列进行预测的模型。在PaddleVideo中,训练数据为采用`.npy`格式存储的`Numpy`数据,标签则可以是`.npy`或`.pkl`格式存储的文件。对于序列数据的维度要求为`(N,C,T,V,M)`。 + +以我们在PPhuman中的模型为例,其中具体说明如下: +| 维度 | 大小 | 说明 | +| ---- | ---- | ---------- | +| N | 不定 | 数据集序列个数 | +| C | 2 | 关键点坐标维度,即(x, y) | +| T | 50 | 动作序列的时序维度(即持续帧数)| +| V | 17 | 每个人物关键点的个数,这里我们使用了`COCO`数据集的定义,具体可见[这里](https://github.com/PaddlePaddle/PaddleDetection/blob/release/2.4/docs/tutorials/PrepareKeypointDataSet_cn.md#COCO%E6%95%B0%E6%8D%AE%E9%9B%86) | +| M | 1 | 人物个数,这里我们每个动作序列只针对单人预测 | + +#### 1. 获取序列的骨骼点坐标 +对于一个待标注的序列(这里序列指一个动作片段,可以是视频或有顺序的图片集合)。可以通过模型预测或人工标注的方式获取骨骼点(也称为关键点)坐标。 +- 模型预测:可以直接选用[PaddleDetection KeyPoint模型系列](https://github.com/PaddlePaddle/PaddleDetection/tree/release/2.4/configs/keypoint) 模型库中的模型,并根据`3、训练与测试 - 部署预测 - 检测+keypoint top-down模型联合部署`中的步骤获取目标序列的17个关键点坐标。 +- 人工标注:若对关键点的数量或是定义有其他需求,也可以直接人工标注各个关键点的坐标位置,注意对于被遮挡或较难标注的点,仍需要标注一个大致坐标,否则后续网络学习过程会受到影响。 + +在完成骨骼点坐标的获取后,建议根据各人物的检测框进行归一化处理,以消除人物位置、尺度的差异给网络带来的收敛难度,这一步可以参考[这里](https://github.com/PaddlePaddle/PaddleDetection/blob/release/2.4/deploy/pphuman/pipe_utils.py#L352-L363)。 + +#### 2. 统一序列的时序长度 +由于实际数据中每个动作的长度不一,首先需要根据您的数据和实际场景预定时序长度(在PP-Human中我们采用50帧为一个动作序列),并对数据做以下处理: +- 实际长度超过预定长度的数据,随机截取一个50帧的片段 +- 实际长度不足预定长度的数据:补0,直到满足50帧 +- 恰好等于预定长度的数据: 无需处理 + +注意:在这一步完成后,请严格确认处理后的数据仍然包含了一个完整的行为动作,不会产生预测上的歧义,建议通过可视化数据的方式进行确认。 + +#### 3. 保存为PaddleVideo可用的文件格式 +在经过前两步处理后,我们得到了每个人物动作片段的标注,此时我们已有一个列表`all_kpts`,这个列表中包含多个关键点序列片段,其中每一个片段形状为(T, V, C) (在我们的例子中即(50, 17, 2)), 下面进一步将其转化为PaddleVideo可用的格式。 +- 调整维度顺序: 可通过`np.transpose`和`np.expand_dims`将每一个片段的维度转化为(C, T, V, M)的格式。 +- 将所有片段组合并保存为一个文件 + +注意:这里的`class_id`是`int`类型,与其他分类任务类似。例如`0:摔倒, 1:其他`。 + +至此,我们得到了可用的训练数据(`.npy`)和对应的标注文件(`.pkl`)。 + +#### 示例:基于UR Fall Detection Dataset的摔倒数据处理 +[UR Fall Detection Dataset](http://fenix.univ.rzeszow.pl/~mkepski/ds/uf.html)是一个包含了不同摄像机视角及不同传感器下的摔倒检测数据集。数据集本身并不包含关键点坐标标注,在这里我们使用平视视角(camera 0)的RGB图像数据,介绍如何依照上面展示的步骤完成数据准备工作。 + +(1)使用[PaddleDetection关键点模型](https://github.com/PaddlePaddle/PaddleDetection/tree/release/2.4/configs/keypoint)完成关键点坐标的检测 +```bash +# current path is under root of PaddleDetection + +# Step 1: download pretrained inference models. +wget https://bj.bcebos.com/v1/paddledet/models/pipeline/mot_ppyoloe_l_36e_pipeline.zip +wget https://bj.bcebos.com/v1/paddledet/models/pipeline/dark_hrnet_w32_256x192.zip +unzip -d output_inference/ mot_ppyoloe_l_36e_pipeline.zip +unzip -d output_inference/ dark_hrnet_w32_256x192.zip + +# Step 2: Get the keypoint coordinarys + +# if your data is image sequence +python deploy/python/det_keypoint_unite_infer.py --det_model_dir=output_inference/mot_ppyoloe_l_36e_pipeline/ --keypoint_model_dir=output_inference/dark_hrnet_w32_256x192 --image_dir={your image directory path} --device=GPU --save_res=True + +# if your data is video +python deploy/python/det_keypoint_unite_infer.py --det_model_dir=output_inference/mot_ppyoloe_l_36e_pipeline/ --keypoint_model_dir=output_inference/dark_hrnet_w32_256x192 --video_file={your video file path} --device=GPU --save_res=True +``` +这样我们会得到一个`det_keypoint_unite_image_results.json`的检测结果文件。内容的具体含义请见[这里](https://github.com/PaddlePaddle/PaddleDetection/blob/release/2.4/deploy/python/det_keypoint_unite_infer.py#L108)。 + +这里我们需要对UR Fall中的每一段数据执行上面介绍的步骤,在每一段执行完成后及时将检测结果文件妥善保存到一个文件夹中。 +```bash + +mkdir {root of PaddleVideo}/applications/PPHuman/datasets/annotations +mv det_keypoint_unite_image_results.json {root of PaddleVideo}/applications/PPHuman/datasets/annotations/det_keypoint_unite_image_results_{video_id}_{camera_id}.json +``` + +(2)将关键点坐标转化为训练数据 + + +在完成上述步骤后,我们得到的骨骼点数据形式如下: +``` +annotations/ +├── det_keypoint_unite_image_results_fall-01-cam0-rgb.json +├── det_keypoint_unite_image_results_fall-02-cam0-rgb.json +├── det_keypoint_unite_image_results_fall-03-cam0-rgb.json +├── det_keypoint_unite_image_results_fall-04-cam0-rgb.json + ... +├── det_keypoint_unite_image_results_fall-28-cam0-rgb.json +├── det_keypoint_unite_image_results_fall-29-cam0-rgb.json +└── det_keypoint_unite_image_results_fall-30-cam0-rgb.json +``` +这里使用我们提供的脚本直接将数据转化为训练数据, 得到数据文件`train_data.npy`, 标签文件`train_label.pkl`。该脚本执行的内容包括解析json文件内容、前述步骤中介绍的整理训练数据及保存数据文件。 +```bash +# current path is {root of PaddleVideo}/applications/PPHuman/datasets/ + +python prepare_dataset.py +``` +几点说明: +- UR Fall的动作大多是100帧左右长度对应一个完整动作,个别视频包含一些无关动作,可以手工去除,也可以裁剪作为负样本 +- 统一将数据整理为100帧,再抽取为50帧,保证动作完整性 +- 上述包含摔倒的动作是正样本,在实际训练中也需要一些其他的动作或正常站立等作为负样本,步骤同上,但注意label的类型取1。 + +这里我们提供了我们处理好的更全面的[数据](https://bj.bcebos.com/v1/paddledet/data/PPhuman/fall_data.zip),包括其他场景中的摔倒及非摔倒的动作场景。 + +### 训练与测试 +在PaddleVideo中,使用以下命令即可开始训练: +```bash +# current path is under root of PaddleVideo +python main.py -c applications/PPHuman/configs/stgcn_pphuman.yaml + +# 由于整个任务可能过拟合,建议同时开启验证以保存最佳模型 +python main.py --validate -c applications/PPHuman/configs/stgcn_pphuman.yaml +``` + +在训练完成后,采用以下命令进行预测: +```bash +python main.py --test -c applications/PPHuman/configs/stgcn_pphuman.yaml -w output/STGCN/STGCN_best.pdparams +``` + +### 导出模型推理 + +- 在PaddleVideo中,通过以下命令实现模型的导出,得到模型结构文件`STGCN.pdmodel`和模型权重文件`STGCN.pdiparams`,并增加配置文件: +```bash +# current path is under root of PaddleVideo +python tools/export_model.py -c applications/PPHuman/configs/stgcn_pphuman.yaml \ + -p output/STGCN/STGCN_best.pdparams \ + -o output_inference/STGCN + +cp applications/PPHuman/configs/infer_cfg.yml output_inference/STGCN + +# 重命名模型文件,适配PP-Human的调用 +cd output_inference/STGCN +mv STGCN.pdiparams model.pdiparams +mv STGCN.pdiparams.info model.pdiparams.info +mv STGCN.pdmodel model.pdmodel +``` +完成后的导出模型目录结构如下: +``` +STGCN +├── infer_cfg.yml +├── model.pdiparams +├── model.pdiparams.info +├── model.pdmodel +``` + +至此,就可以使用[PP-Human](https://github.com/PaddlePaddle/PaddleDetection/tree/release/2.4/deploy/pphuman)进行行为识别的推理了。 diff --git a/applications/PPHuman/configs/infer_cfg.yml b/applications/PPHuman/configs/infer_cfg.yml new file mode 100644 index 0000000000000000000000000000000000000000..aa3a17bbbb677af12786a31059f3019a8f53dab0 --- /dev/null +++ b/applications/PPHuman/configs/infer_cfg.yml @@ -0,0 +1,9 @@ +mode: fluid +use_dynamic_shape: false +arch: STGCN +min_subgraph_size: 3 +Preprocess: +- window_size: 50 + type: AutoPadding +label_list: +- keypoint diff --git a/applications/PPHuman/configs/stgcn_pphuman.yaml b/applications/PPHuman/configs/stgcn_pphuman.yaml new file mode 100644 index 0000000000000000000000000000000000000000..097d1c119305c83b5dc9d4a4ee1a49dd571521e3 --- /dev/null +++ b/applications/PPHuman/configs/stgcn_pphuman.yaml @@ -0,0 +1,72 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "STGCN" #Mandatory, The name of backbone. + in_channels: 2 + dropout: 0.5 + layout: 'coco_keypoint' + data_bn: True + head: + name: "STGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 2 #Optional, the number of classes to be classified. + if_top5: False + +DATASET: #DATASET field + batch_size: 64 #Mandatory, batch size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevideo/loader/dateset' + file_path: "./applications/PPHuman/datasets/train_data.npy" #mandatory, train data index file path + label_path: "./applications/PPHuman/datasets/train_label.pkl" + + valid: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevideo/loader/dateset' + file_path: "./applications/PPHuman/datasets/val_data.npy" #Mandatory, valid data index file path + label_path: "./applications/PPHuman/datasets/val_label.pkl" + + test_mode: True + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevideo/loader/dateset' + file_path: "./applications/PPHuman/datasets/val_data.npy" #Mandatory, valid data index file path + label_path: "./applications/PPHuman/datasets/val_label.pkl" + + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + test: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + name: 'CosineAnnealingDecay' + learning_rate: 0.05 + T_max: 50 + weight_decay: + name: 'L2' + value: 1e-4 + +METRIC: + name: 'SkeletonMetric' + top_k: 2 + +INFERENCE: + name: 'STGCN_Inference_helper' + num_channels: 2 + window_size: 50 + vertex_nums: 17 + person_nums: 1 + +model_name: "STGCN" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch diff --git a/applications/PPHuman/datasets/prepare_dataset.py b/applications/PPHuman/datasets/prepare_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..81d6f1fe6866957f470cc91f7e1ee45f2a350439 --- /dev/null +++ b/applications/PPHuman/datasets/prepare_dataset.py @@ -0,0 +1,98 @@ +import os +import json +import numpy as np +import pickle +""" + This python script is used to convert keypoint results of UR FALL dataset + for training by PaddleVideo +""" + + +def self_norm(kpt, bbox): + # kpt: (2, T, 17, 1), bbox: (T, 4) + tl = bbox[:, 0:2] + wh = bbox[:, 2:] + tl = np.expand_dims(np.transpose(tl, (1, 0)), (2, 3)) + wh = np.expand_dims(np.transpose(wh, (1, 0)), (2, 3)) + + res = (kpt - tl) / wh + res *= np.expand_dims(np.array([[384.], [512.]]), (2, 3)) + return res + + +def convert_to_ppvideo(all_kpts, all_scores, all_bbox): + # shape of all_kpts is (T, 17, 2) + keypoint = np.expand_dims(np.transpose(all_kpts, [2, 0, 1]), + -1) #(2, T, 17, 1) + keypoint = self_norm(keypoint, all_bbox) + + scores = all_scores + if keypoint.shape[1] > 100: + frame_start = (keypoint.shape[1] - 100) // 2 + keypoint = keypoint[:, frame_start:frame_start + 100:2, :, :] + scores = all_scores[frame_start:frame_start + 100:2, :, :] + elif keypoint.shape[1] < 100: + keypoint = np.concatenate([ + keypoint, + np.zeros((2, 100 - keypoint.shape[1], 17, 1), dtype=keypoint.dtype) + ], 1)[:, ::2, :, :] + scores = np.concatenate([ + all_scores, + np.zeros((100 - all_scores.shape[0], 17, 1), dtype=keypoint.dtype) + ], 0)[::2, :, :] + else: + keypoint = keypoint[:, ::2, :, :] + scores = scores[::2, :, :] + return keypoint, scores + + +def decode_json_path(json_path): + content = json.load(open(json_path)) + content = sorted(content, key=lambda x: x[0]) + all_kpts = [] + all_score = [] + all_bbox = [] + for annos in content: + bboxes = annos[1] + kpts = annos[2][0] + frame_id = annos[0] + + if len(bboxes) != 1: + continue + kpt_res = [] + kpt_score = [] + for kpt in kpts[0]: + x, y, score = kpt + kpt_res.append([x, y]) + kpt_score.append([score]) + all_kpts.append(np.array(kpt_res)) + all_score.append(np.array(kpt_score)) + all_bbox.append([ + bboxes[0][0], bboxes[0][1], bboxes[0][2] - bboxes[0][0], + bboxes[0][3] - bboxes[0][1] + ]) + all_kpts_np = np.array(all_kpts) + all_score_np = np.array(all_score) + all_bbox_np = np.array(all_bbox) + video_anno, scores = convert_to_ppvideo(all_kpts_np, all_score_np, + all_bbox_np) + + return video_anno, scores + + +if __name__ == '__main__': + all_keypoints = [] + all_labels = [[], []] + all_scores = [] + for i, path in enumerate(os.listdir("annotations")): + video_anno, score = decode_json_path(os.path.join("annotations", path)) + + all_keypoints.append(video_anno) + all_labels[0].append(str(i)) + all_labels[1].append(0) #label 0 means falling + all_scores.append(score) + all_data = np.stack(all_keypoints, 0) + all_score_data = np.stack(all_scores, 0) + np.save(f"train_data.npy", all_data) + pickle.dump(all_labels, open(f"train_label.pkl", "wb")) + np.save("kptscore_data.npy", all_score_data) diff --git a/applications/README.md b/applications/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a192ea876ae15afff8a180385225ce2209d30c84 --- /dev/null +++ b/applications/README.md @@ -0,0 +1,42 @@ +# Applications + +- [FootballAction](FootballAction): Football action detection solution + +
+
+ +- [BasketballAction](BasketballAction): Basketball action detection solution + +
+
+ +- [TableTennis](TableTennis): Table tennis action recognition solution + + +- [FigureSkating](FigureSkating): Figure skating action recognition solution + +
+
+ +- [VideoTag](VideoTag): 3000-category large-scale video classification solution + +
+
+ +- [MultimodalVideoTag](MultimodalVideoTag): Multimodal video classification solution + +
+
+ +- [VideoQualityAssessment](VideoQualityAssessment): Video quality assessment solution + + +- [PP-Care](PP-Care): 3DMRI medical image recognition solution + + +- [EIVideo](EIVideo): Interactive video segmentation tool + +
+
+ +- [Anti-UAV](Anti-UAV): UAV detection solution diff --git a/applications/T2VLAD/README.md b/applications/T2VLAD/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9ca65b9f531b3bdc579293342a4eafee069f3826 --- /dev/null +++ b/applications/T2VLAD/README.md @@ -0,0 +1,75 @@ +[English](./README_en.md) | 简体中文 + +# T2VLAD: 基于局部全局对齐的文本视频检索 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [参考论文](#参考论文) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install paddlenlp +``` +同时确保paddle版本为2.2.2。 + +## 模型简介 + +T2VLAD是百度在CVPR2021提出的文本视频检索模型。文本视频检索是一项具有挑战的任务,旨在基于自然语言处理描述搜索相关视频内容。这个问题的关键是在联合嵌入空间中测量文本-视频的相似性。T2VLAD设计了一种有效的全局-局部对齐方法,在三个标准的文本视频检索基准上取得了一致的改进,并以明显的优势超越了最先进的技术。 + +
+
+
+ + +## 数据准备 + +MSR-VTT数据下载及准备请参考 [MSR-VTT数据准备](../../docs/zh-CN/dataset/msrvtt.md) + +## 模型训练 + +### MSR-VTT数据集训练 + +下载数据并添加到 `data/MSRVTT` 文件夹下。 + +#### 开始训练 + +- 训练启动命令如下: + +```bash +export CUDA_VISIBLE_DEVICES=0 +python3.7 train.py --config ./configs/msrvtt_transformers.json +``` + +T2VLAD在训练时使用了Ranger优化器,这里我们暂时没有支持Ranger优化器到的实现,目前可以使用AdamW优化器来完成训练。 + + +## 模型测试 + +- 对下游任务:文本-视频检索,在MSR-VTT数据集上评估性能,评估脚本启动方式如下: + +```bash +export CUDA_VISIBLE_DEVICES=0 +python3.7 test.py --config ./configs/msrvtt_transformers.json --resume ./T2VLAD_msrvtt.pdparams +``` + +MSR-VTT数据集测试精度: +Text $\rightarrow$ Video +| R@1 | R@5 | R@10 | Median R | checkpoints | +| :--: | :--: | :--: | :------: | :----------------------------------------------------------: | +| 29.5 | 59.0 | 70.1 | 4 | [T2VLAD.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/T2VLAD_msrvtt.pdparams) | + +Video $\rightarrow$ Text +| R@1 | R@5 | R@10 | Median R | +| :--: | :--: | :--: | :------: | +| 26.1 | 54.7 | 68.1 | 4 | + + +## 参考论文 + +- [T2VLAD: Global-Local Sequence Alignment for Text-Video Retrieval +](https://arxiv.org/pdf/2104.10054.pdf), Xiaohan Wang, Linchao Zhu, Yi Yang diff --git a/applications/T2VLAD/README_en.md b/applications/T2VLAD/README_en.md new file mode 100644 index 0000000000000000000000000000000000000000..0c46a588499380aad7eb9add29167b02420b4a01 --- /dev/null +++ b/applications/T2VLAD/README_en.md @@ -0,0 +1,69 @@ +[简体中文](./README.md) | English + +# T2VLAD + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Reference](#Reference) + +Before getting started, you need to install additional dependencies as follows: +```bash +python -m pip install paddlenlp +``` + +## Introduction +T2VLAD is proposed by Baidu in CVPR2021 for text-video retrieval. Text-video retrieval is a challenging task that aims to search relevant video contents based on natural language descriptions. The key to this problem is to measure text- video similarities in a joint embedding space. T2VLAD designs an efficient global-local alignment method. This model achieves consistent improvements on three standard text-video retrieval benchmarks and outperform the state- of-the-art by a clear margin. + +
+
+
+ + +## Data +Please refer to MSR-VTT data download and preparation doc [MSR-VTT data](../../docs/en/dataset/msrvtt.md) + +## Train +### Train on MSR-VTT +Download data then move to `data/MSRVTT` folder. + +#### Start training + +- Train T2VLAD on MSRVTT scripts: + +```bash +export CUDA_VISIBLE_DEVICES=0 +python3.7 train.py --config ./configs/msrvtt_transformers.json +``` + +T2VLAD uses the Ranger optimizer during training. We haven't supported the implementation of Ranger optimizer, for now, the AdamW optimizer can be used to complete the training. + + +## Test + +- Evaluation performs on downstream task, i.e. text-video clip retrieval on MSR-VTT dataset, test accuracy can be obtained using scripts: + +```bash +export CUDA_VISIBLE_DEVICES=0 +python3.7 test.py --config ./configs/msrvtt_transformers.json --resume ./T2VLAD_msrvtt.pdparams +``` + +Accuracy on MSR-VTT: +Text $\rightarrow$ Video +| R@1 | R@5 | R@10 | Median R | checkpoints | +| :--: | :--: | :--: | :------: | :----------------------------------------------------------: | +| 29.5 | 59.0 | 70.1 | 4 | [T2VLAD.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/T2VLAD_msrvtt.pdparams) | + +Video $\rightarrow$ Text +| R@1 | R@5 | R@10 | Median R | +| :--: | :--: | :--: | :------: | +| 26.1 | 54.7 | 68.1 | 4 | + +## Reference + +- [T2VLAD: Global-Local Sequence Alignment for Text-Video Retrieval +](https://arxiv.org/pdf/2104.10054.pdf), Xiaohan Wang, Linchao Zhu, Yi Yang diff --git a/applications/T2VLAD/base/__init__.py b/applications/T2VLAD/base/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d9d437ad774e9499e8246af6f3c1c76418f08f2c --- /dev/null +++ b/applications/T2VLAD/base/__init__.py @@ -0,0 +1,2 @@ +from .base_model import * +from .base_trainer import * diff --git a/applications/T2VLAD/base/base_dataset.py b/applications/T2VLAD/base/base_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..877b7364e64faa71c8f4fef73b59c77349f05664 --- /dev/null +++ b/applications/T2VLAD/base/base_dataset.py @@ -0,0 +1,562 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import time +import json +import random +import paddle +import inspect +import logging +import functools +import data_loader + +import numpy as np +import pickle as pkl + +from pathlib import Path +from abc import abstractmethod +from typing import Dict, Union +from numpy.random import randint +from typeguard import typechecked +from collections import OrderedDict +from zsvision.zs_utils import memcache +try: + from paddlenlp.transformers import BertTokenizer +except ImportError as e: + print( + f"{e}, [paddlenlp] package and it's dependencies is required for T2VLAD." + ) +from utils import ensure_tensor, expert_tensor_storage + +# For SLURM usage, buffering makes it difficult to see events as they happen, so we set +# the global print statement to enforce flushing +print = functools.partial(print, flush=True) + + +class BaseDataset(paddle.io.Dataset): + @staticmethod + @abstractmethod + @typechecked + def dataset_paths() -> Dict[str, Union[Path, str]]: + """Generates a datastructure containing all the paths required to load features + """ + raise NotImplementedError + + @abstractmethod + def sanity_checks(self): + """Run sanity checks on loaded data + """ + raise NotImplementedError + + @abstractmethod + def load_features(self): + """Load features from disk + """ + raise NotImplementedError + + @typechecked + def __init__( + self, + data_dir: Path, + eval_only: bool, + use_zeros_for_missing: bool, + text_agg: str, + text_feat: str, + split_name: str, + cls_partition: str, + root_feat_folder: str, + text_dim: int, + num_test_captions: int, + restrict_train_captions: int, + max_tokens: Dict[str, int], + logger: logging.Logger, + raw_input_dims: Dict[str, int], + feat_aggregation: Dict[str, Dict], + ): + self.eval_only = eval_only + self.logger = logger + self.text_feat = text_feat + self.data_dir = data_dir + self.text_dim = text_dim + self.restrict_train_captions = restrict_train_captions + self.max_tokens = max_tokens + self.cls_partition = cls_partition + self.num_test_captions = num_test_captions + self.feat_aggregation = feat_aggregation + self.root_feat = data_dir / root_feat_folder + self.experts = set(raw_input_dims.keys()) + self.tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') + # This attributes can be overloaded by different datasets, so it must be set + # before the `load_features() method call` + self.restrict_test_captions = None + self.text_features = None + self.label_features = None + self.video_labels = None + self.raw_captions = None + self.features = None + + self.word2int = json.load(open('word2int.json')) + + # Use a single caption per video when forming training minibatches (different + # captions from the same video may still be used across different minibatches) + self.captions_per_video = 1 + + self.ordered_experts = list(raw_input_dims.keys()) + + # Training and test lists are set by dataset-specific subclasses + self.partition_lists = {} + self.configure_train_test_splits(split_name=split_name) + + # All retrieval-based tasks use a single dataloader (and handle the retrieval + # data separately), whereas for classification we use one dataloader for + # training and one for validation. + self.logger.info("The current task is retrieval") + self.sample_list = self.partition_lists["train"] + self.num_samples = len(self.sample_list) + num_val = len(self.partition_lists["val"]) + + self.raw_input_dims = raw_input_dims + + # we store default paths to enable visualisations (this can be overloaded by + # dataset-specific classes) + self.video_path_retrieval = [ + f"videos/{x}.mp4" for x in self.partition_lists["val"] + ] + + # NOTE: We use nans rather than zeros to indicate missing faces, unless we wish + # to test single modality strength, which requires passing zeroed features for + # missing videos + if use_zeros_for_missing: + self.MISSING_VAL = 0 + else: + self.MISSING_VAL = np.nan + + # load the dataset-specific features into memory + self.load_features() + + if text_agg == "avg": + self.logger.info("averaging the text features...") + for key, val in self.text_features.items(): + self.text_features[key] = [ + np.mean(x, 0, keepdims=1) for x in val + ] + self.logger.info("finished averaging the text features") + + self.trn_config = {} + self.raw_config = {} + self.tensor_storage = expert_tensor_storage(self.experts, + self.feat_aggregation) + for static_expert in self.tensor_storage["fixed"]: + if static_expert in self.feat_aggregation: + if "trn_seg" in self.feat_aggregation[static_expert].keys(): + self.trn_config[static_expert] = \ + self.feat_aggregation[static_expert]["trn_seg"] + if "raw" in self.feat_aggregation[static_expert]["temporal"]: + self.raw_config[static_expert] = 1 + + retrieval = { + expert: np.zeros( + (num_val, self.max_tokens[expert], raw_input_dims[expert])) + for expert in self.tensor_storage["variable"] + } + retrieval.update({ + expert: np.zeros((num_val, raw_input_dims[expert])) + for expert in self.tensor_storage["fixed"] + }) + self.retrieval = retrieval + self.test_ind = { + expert: paddle.ones([num_val]) + for expert in self.experts + } + self.raw_captions_retrieval = [None] * num_val + + # avoid evaluation on missing queries + self.query_masks = np.zeros((num_val, num_test_captions)) + self.text_token_mask = np.zeros((num_val, num_test_captions)) + self.text_retrieval = np.zeros((num_val, self.num_test_captions, + self.max_tokens["text"], self.text_dim)) + self.cap_retrieval = paddle.zeros( + [num_val, self.num_test_captions, self.max_tokens["text"]], + dtype='int64' + ) #self.cap_retrieval = th.zeros((num_val, self.num_test_captions, self.max_tokens["text"])) + self.att_retrieval = paddle.zeros( + [num_val, self.num_test_captions, self.max_tokens["text"]], + dtype='int64' + ) #self.att_retrieval = th.zeros((num_val, self.num_test_captions, self.max_tokens["text"])) + + save_cap = [] + for ii, video_name in enumerate(self.partition_lists["val"]): + + self.raw_captions_retrieval[ii] = self.raw_captions[video_name] + for expert in self.tensor_storage["fixed"].intersection( + self.experts): + feats = self.features[expert][video_name] + drop = self.has_missing_values(feats) + self.test_ind[expert][ii] = not drop + self.retrieval[expert][ii] = feats + if drop: + self.retrieval[expert][ii][:] = self.MISSING_VAL + if self.feat_aggregation[expert].get("binarise", False): + keep = np.logical_not( + np.isnan(self.retrieval[expert][:, 0, 0])) + marker = np.ones_like(self.retrieval[expert][keep]) + self.retrieval[expert][keep] = marker + + for expert in self.tensor_storage["variable"].intersection( + self.experts): + feats = self.features[expert][video_name] + drop = self.has_missing_values(feats) + self.test_ind[expert][ii] = not drop + if drop: + self.retrieval[expert][ii][:] = self.MISSING_VAL + if self.feat_aggregation[expert].get("binarise", False): + keep = np.logical_not( + np.isnan(self.retrieval[expert][:, 0, 0])) + marker = np.ones_like(self.retrieval[expert][keep]) + self.retrieval[expert][keep] = marker + if self.test_ind[expert][ii]: + keep = min(self.max_tokens[expert], len(feats)) + self.retrieval[expert][ii, :keep, :] = feats[:keep] + + candidates_sentences = self.text_features[video_name] + if self.restrict_test_captions is not None: + keep_sent_idx = self.restrict_test_captions[video_name] + candidates_sentences = [candidates_sentences[keep_sent_idx]] + + self.query_masks[ii, :len(candidates_sentences)] = 1 + + for test_caption_idx in range(self.num_test_captions): + if len(candidates_sentences) <= test_caption_idx: + break + keep = min(len(candidates_sentences[test_caption_idx]), + self.max_tokens["text"]) + self.text_token_mask[ii, test_caption_idx] = keep + sent = self.raw_captions_retrieval[ii][test_caption_idx] + sent = " ".join(sent) + sent = sent.strip() + encoded_dict = self.tokenizer.__call__( + sent, + max_seq_len=self.max_tokens["text"], + pad_to_max_seq_len=True, + return_attention_mask=True, + truncation_strategy='longest_first') + cap_ids = paddle.to_tensor(encoded_dict['input_ids']) + attention_mask = paddle.to_tensor( + encoded_dict['attention_mask']) + save_cap.append(sent) + self.cap_retrieval[ii, test_caption_idx, :] = cap_ids + self.att_retrieval[ii, test_caption_idx, :] = attention_mask + if ii % 500 == 0 and test_caption_idx == 0: + msg = ( + f"{ii}/{len(self.partition_lists['val'])} will evaluate " + f"sentence {test_caption_idx} out of " + f"{len(candidates_sentences)} (has {keep} words) " + f"{video_name}") + self.logger.info(msg) + text_feats = candidates_sentences[test_caption_idx][:keep] + if text_feats.shape[0] == 0: + text_feats = 0 + raise ValueError("empty text features!") + self.text_retrieval[ii, test_caption_idx, :keep, :] = text_feats + with open('run_cap.pkl', 'wb') as f: + pkl.dump(save_cap, f) + self.sanity_checks() + + def configure_train_test_splits(self, split_name): + """Partition the datset into train/val/test splits. + + Args: + split_name (str): the name of the split + """ + self.paths = type(self).dataset_paths() + print("loading training/val splits....") + tic = time.time() + for subset, path in self.paths["subset_list_paths"][split_name].items(): + root_feat = Path(self.root_feat) + subset_list_path = root_feat / path + if subset == "train" and self.eval_only: + rows = [] + else: + with open(subset_list_path) as f: + rows = f.read().splitlines() + self.partition_lists[subset] = rows + print("done in {:.3f}s".format(time.time() - tic)) + self.split_name = split_name + + def collate_data(self, data): + batch_size = len(data) + tensors = {} + for expert in self.tensor_storage["fixed"]: + if expert in self.trn_config.keys(): + tensors[expert] = paddle.to_tensor( + np.zeros((batch_size, self.trn_config[expert], + self.raw_input_dims[expert]))) + else: + tensors[expert] = paddle.to_tensor( + np.zeros((batch_size, self.raw_input_dims[expert]))) + + # Track which indices of each modality are available in the present batch + ind = { + expert: paddle.to_tensor(np.zeros(batch_size)) + for expert in self.experts + } + tensors.update({ + expert: paddle.to_tensor( + np.zeros((batch_size, self.max_tokens[expert], + self.raw_input_dims[expert]))) + for expert in self.tensor_storage["variable"] + }) + + text_tensor = paddle.to_tensor( + np.zeros((batch_size, self.captions_per_video, + self.max_tokens["text"], self.text_dim))) + text_token_mask = paddle.to_tensor( + np.zeros((batch_size, self.captions_per_video))) + text_cap_id = paddle.zeros([batch_size, self.max_tokens["text"]], + dtype='int64') + text_att_mask = paddle.zeros([batch_size, self.max_tokens["text"]], + dtype='int64') + + for ii, _ in enumerate(data): + datum = data[ii] + for expert in self.experts: + ind[expert][ii] = datum[f"{expert}_ind"] + for expert in self.tensor_storage["fixed"]: + tensors[expert][ii] = datum[expert] + for expert in self.tensor_storage["variable"]: + if ind[expert][ii]: + keep = min(len(datum[expert]), self.max_tokens[expert]) + if keep: + tensors[expert][ii, :keep, :] = datum[expert][:keep] + else: + tensors[expert][ii, :, :] = self.MISSING_VAL + + text = datum["text"] + cap_id = datum["cap_id"] + att_mask = datum["att_mask"] + text_cap_id[ii, :] = paddle.to_tensor(cap_id) + text_att_mask[ii, :] = paddle.to_tensor(att_mask) + for jj in range(self.captions_per_video): + keep = min(len(text[jj]), self.max_tokens["text"]) + text_tensor[ii, jj, :keep, :] = text[jj][:keep] + text_token_mask[ii, jj] = keep + + ind = {key: ensure_tensor(val) for key, val in ind.items()} + experts = OrderedDict( + (expert, paddle.to_tensor(tensors[expert], dtype='float32')) + for expert in self.ordered_experts) + + for expert in self.experts: + if self.feat_aggregation[expert].get("binarise", False): + replace = np.logical_not(paddle.isnan(experts[expert][:, 0, 0])) + experts[expert][replace] = paddle.ones_like( + experts[expert][replace]) + + minibatch = {"experts": experts, "ind": ind} + minibatch["text"] = paddle.to_tensor(text_tensor, dtype='float32') + minibatch["cap_id"] = paddle.to_tensor(text_cap_id, dtype='int64') + minibatch["att_mask"] = paddle.to_tensor(text_att_mask, dtype='int64') + minibatch["text_token_mask"] = paddle.to_tensor(text_token_mask) + return minibatch + + def process_sent(self, sent, max_words, EOS: int = 1, UNK: int = 2): + # set EOS=1, UNK=2 by default, consistent with file 'word2int.json'. + tokens = [self.word2int.get(w, UNK) for w in sent] + tokens = tokens[:max_words] + tokens_len = len(tokens) + tokens = np.array(tokens + [EOS] * (max_words - tokens_len)) + return tokens, tokens_len + + def __len__(self): + return self.num_samples + + def __getitem__(self, idx): + if idx < self.num_samples: + vid = self.sample_list[idx] + features = {} + for expert in self.experts: + if expert not in self.trn_config.keys(): + if expert in self.raw_config.keys(): + features[expert] = np.mean(self.features[expert][vid], + axis=0) + else: + features[expert] = self.features[expert][vid] + else: + raw_frame_feats = self.features[expert][vid] + new_length = 1 + num_frames = raw_frame_feats.shape[0] + avg_duration = ((num_frames - new_length + 1) // + self.trn_config[expert]) + assert avg_duration > 0, "average duration must be positive" + if avg_duration > 0: + # maybe we could change to use average for each tiny segment + # seems like use everything per iter + offsets = np.multiply( + list(range(self.trn_config[expert])), avg_duration) + offsets += randint(avg_duration, + size=self.trn_config[expert]) + new_frame_feats = np.zeros( + (self.trn_config[expert], raw_frame_feats.shape[1])) + for idx, xx in enumerate(offsets): + new_frame_feats[idx, :] = raw_frame_feats[xx, :] + msg = "returning a wrong feature != segment num" + assert new_frame_feats.shape[0] == self.trn_config[ + expert], msg + features[expert] = new_frame_feats + + ind = {} + for expert in self.ordered_experts: + if expert in self.tensor_storage["flaky"]: + ind[expert] = not self.has_missing_values(features[expert]) + else: + ind[expert] = 1 + + # Handle some inconsistencies between how the text features are stored + text = self.text_features[vid] + if isinstance(text, list): + pick = np.random.choice(len(text), size=self.captions_per_video) + sent = self.raw_captions[vid][pick[0]] + sent = " ".join(sent) + sent = sent.strip() + + text = np.array(text)[pick] + encoded_dict = self.tokenizer.__call__( + sent, + max_seq_len=self.max_tokens["text"], + pad_to_max_seq_len=True, + return_attention_mask=True, + truncation_strategy='longest_first') + cap_id = encoded_dict['input_ids'] + token_type_ids = encoded_dict['token_type_ids'] + attention_mask = encoded_dict['attention_mask'] + else: + pick = None + text = np.random.choice(text, size=self.captions_per_video) + + # Return both the missing indices as well as the tensors + sample = {"text": text} + sample.update({"cap_id": cap_id}) + sample.update({"att_mask": attention_mask}) + sample.update({f"{key}_ind": val for key, val in ind.items()}) + sample.update(features) + return sample + + def get_retrieval_data(self): + experts = OrderedDict( + (expert, paddle.to_tensor(self.retrieval[expert], dtype='float32')) + for expert in self.ordered_experts) + retrieval_data = { + "text": + paddle.to_tensor(ensure_tensor(self.text_retrieval), + dtype='float32'), + "experts": + experts, + "cap_id": + paddle.to_tensor(self.cap_retrieval, dtype='int64'), + "att_mask": + paddle.to_tensor(self.att_retrieval, dtype='int64'), + "ind": + self.test_ind, + "text_token_mask": + paddle.to_tensor(self.text_token_mask) + } + meta = { + "query_masks": self.query_masks, + "raw_captions": self.raw_captions_retrieval, + "paths": self.video_path_retrieval, + } + return retrieval_data, meta + + def has_missing_values(self, x): + return isinstance(x, float) and np.isnan(x) + + def visual_feat_paths(self, model_spec, tag=None): + """Canonical path lookup for visual features + """ + if model_spec not in self.ordered_experts: + self.logger.info( + f"Skipping load for {model_spec} (feature not requested)") + return f"SKIPPED-{model_spec}" + + feat_type, model_name, _ = model_spec.split(".") + aggs = self.feat_aggregation[model_spec] + base = f"aggregated_{feat_type.replace('-', '_')}" + required = ("fps", "pixel_dim", "stride") + fps, pixel_dim, stride = [aggs.get(x, None) for x in required] + if feat_type in {"facecrops", "faceboxes"}: + base = f"{base}_{fps}fps_{pixel_dim}px_stride{stride}" + elif feat_type not in {"ocr", "speech", "audio"}: + base = f"{base}_{fps}fps_{pixel_dim}px_stride{stride}" + + for option in "offset", "inner_stride": + if aggs.get(option, None) is not None: + base += f"_{option}{aggs[option]}" + + feat_paths = [] + for agg in aggs["temporal"].split("-"): + fname = f"{model_name}-{agg}" + if aggs["type"] == "logits": + fname = f"{fname}-logits" + if tag is not None: + fname += f"-{tag}" + feat_paths.append(Path(base) / f"{fname}.pickle") + return feat_paths + + def log_assert(self, bool_, msg="", verbose=True): + """Use assertions that will be written to the logs. This is a recipe from: + http://code.activestate.com/recipes/577074-logging-asserts/ + """ + try: + assert bool_, msg + except AssertionError: + # construct an exception message from the code of the calling frame + last_stackframe = inspect.stack()[-2] + source_file, line_no, func = last_stackframe[1:4] + source = f"Traceback (most recent call last):\n" + \ + f" File {source_file}, line {line_no}, in {func}\n" + if verbose: + # include more lines than that where the statement was made + source_code = open(source_file).readlines() + source += "".join(source_code[line_no - 3:line_no + 1]) + else: + source += last_stackframe[-2][0].strip() + self.logger.debug(f"{msg}\n{source}") + raise AssertionError(f"{msg}\n{source}") + + def summary_stats(self): + """Report basic statistics about feature availability and variable lengths + across the different subsets of the data. + """ + self.logger.info("Computing feature stats...") + queries = self.ordered_experts + ["text"] + for subset, keep in self.partition_lists.items(): + keep = set(keep) + print(f"Summary for {subset}") + for expert in queries: + if expert in self.features: + feats = self.features[expert] + else: + feats = self.text_features + vals = [feats[key] for key in keep] + missing = 0 + sizes = [] + for val in vals: + if self.has_missing_values(val): + missing += 1 + else: + sizes.append(len(val)) + if sizes: + stat_str = (f"min: {np.min(sizes):4}, " + f"max: {np.max(sizes):4}, " + f"mean: {np.mean(sizes):.1f}") + print( + f"{subset}: missing: {missing:4}, {stat_str} {expert}") diff --git a/applications/T2VLAD/base/base_model.py b/applications/T2VLAD/base/base_model.py new file mode 100644 index 0000000000000000000000000000000000000000..29af41c766b9ba92f06dd84c92e49fca3b9e50d6 --- /dev/null +++ b/applications/T2VLAD/base/base_model.py @@ -0,0 +1,37 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np +import paddle.nn as nn + +from abc import abstractmethod + +class BaseModel(nn.Layer): + """ + Base class for all models + """ + @abstractmethod + def forward(self, *inputs): + """ + Forward pass logic + + :return: Model output + """ + raise NotImplementedError + + def __str__(self): + """ + Model prints with number of trainable parameters + """ + model_parameters = filter(lambda p: p.stop_gradient==False, self.parameters()) + params = sum([np.prod(p.shape) for p in model_parameters]) + return super().__str__() + f"\nTrainable parameters: {params}" diff --git a/applications/T2VLAD/base/base_trainer.py b/applications/T2VLAD/base/base_trainer.py new file mode 100644 index 0000000000000000000000000000000000000000..31ef04a559b62fe5e6817e384efc74bd73c1f902 --- /dev/null +++ b/applications/T2VLAD/base/base_trainer.py @@ -0,0 +1,258 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import re +import copy +import time +import paddle +import pickle +import numpy as np +from pathlib import Path +from abc import abstractmethod + +class BaseTrainer: + """ Base class for all trainers + """ + def __init__(self, model, loss, metrics, optimizer, config, mini_train, + num_keep_ckpts, skip_tboard): + self.config = config + self.logger = config.get_logger( + 'trainer', config['trainer']['verbosity']) + + self.model = model + self.loss = loss + self.metrics = metrics + self.optimizer = optimizer + self.num_keep_ckpts = num_keep_ckpts + self.skip_tboard = skip_tboard or mini_train + + # This property can be overriden in the subclass + self.skip_first_n_saves = 0 + + cfg_trainer = config['trainer'] + self.epochs = cfg_trainer['epochs'] + self.save_period = cfg_trainer['save_period'] + self.monitor = cfg_trainer.get('monitor', 'off') + self.save_only_best = cfg_trainer.get("save_only_best", True) + self.val_freq = cfg_trainer['val_freq'] + # configuration to monitor model performance and save best + if self.monitor == 'off': + self.mnt_mode = 'off' + self.mnt_best = 0 + else: + self.mnt_mode, self.mnt_metric = self.monitor.split() + assert self.mnt_mode in ['min', 'max'] + + self.mnt_best = np.inf if self.mnt_mode == 'min' else -np.inf + self.early_stop = cfg_trainer.get('early_stop', np.inf) + + self.start_epoch = 1 + + self.model_dir = config.save_dir + + self.include_optim_in_save_model = config["trainer"].get("include_optim_in_save_model", 1) + if config.resume is not None: + self._resume_model(config.resume) + + @abstractmethod + def _train_epoch(self, epoch): + """Training logic for an epoch + + :param epoch: Current epoch number + """ + raise NotImplementedError + + def train(self): + """Full training logic. Responsible for iterating over epochs, early stopping, + modeling and logging metrics. + """ + for epoch in range(self.start_epoch, self.epochs + 1): + result, cached_preds = self._train_epoch(epoch) + + if epoch % self.val_freq != 0: + continue + # save logged informations into log dict + log = {'epoch': epoch} + for key, value in result.items(): + if key == 'metrics': + log.update({mtr.__name__: value[i] + for i, mtr in enumerate(self.metrics)}) + elif key == 'val_metrics': + log.update({'val_' + mtr.__name__: value[i] + for i, mtr in enumerate(self.metrics)}) + elif key == 'nested_val_metrics': + # NOTE: currently only supports two layers of nesting + for subkey, subval in value.items(): + for subsubkey, subsubval in subval.items(): + log[f"val_{subkey}_{subsubkey}"] = subsubval + else: + log[key] = value + + # print logged informations to the screen + for key, value in log.items(): + self.logger.info(' {:15s}: {}'.format(str(key), value)) + + # eval model according to configured metric, save best # ckpt as trained_model + not_improved_count = 0 + best = False + if self.mnt_mode != 'off': + try: + # check whether specified metric improved or not, according to + # specified metric(mnt_metric) + lower = log[self.mnt_metric] <= self.mnt_best + higher = log[self.mnt_metric] >= self.mnt_best + improved = (self.mnt_mode == 'min' and lower) or \ + (self.mnt_mode == 'max' and higher) + except KeyError: + msg = "Warning: Metric '{}' not found, perf monitoring is disabled." + self.logger.warning(msg.format(self.mnt_metric)) + self.mnt_mode = 'off' + improved = False + not_improved_count = 0 + raise ValueError("Pick a metric that will save models!!!!!!!!") + + if improved: + self.mnt_best = log[self.mnt_metric] + # TODO(Samuel): refactor the code so that we don't move the model + # off the GPU or duplicate on the GPU (we should be able to safely + # copy the state dict directly to CPU) + copy_model = copy.deepcopy(self.model) + self.best_model = {"epoch": epoch, "model": copy_model} + not_improved_count = 0 + best = True + else: + not_improved_count += 1 + + if not_improved_count > self.early_stop: + self.logger.info("Val performance didn\'t improve for {} epochs. " + "Training stops.".format(self.early_stop)) + break + + if self.save_only_best: + if epoch == self.epochs: + best_model = self.best_model + self.model = best_model["model"] + print(f"saving the best model to disk (epoch {epoch})") + self._save_model(best_model["epoch"], save_best=True) + continue + + # If modeling is done intermittently, still save models that outperform + # the best metric + # save_best = best and not self.mnt_metric == "epoch" + save_best = True + + # Due to the fast runtime/slow HDD combination, modeling can dominate + # the total training time, so we optionally skip models for some of + # the first epochs + if epoch < self.skip_first_n_saves and not self.save_only_best: + msg = f"Skipping model save at epoch {epoch} <= {self.skip_first_n_saves}" + self.logger.info(msg) + continue + + if epoch % self.save_period == 0 and save_best: + self._save_model(epoch, save_best=best) + print("This epoch, the save best :{}".format(best)) + if best: + for key, cached in cached_preds.items(): + log_dir = Path(self.config.log_dir) + prediction_path = log_dir / f"{key}_preds.txt" + prediction_logits_path = log_dir / f"{key}_preds_logits.npy" + np.save(prediction_logits_path, cached["preds"]) + gt_logits_path = log_dir / f"{key}_gt_logits.npy" + np.save(gt_logits_path, cached["labels"].cpu().numpy()) + vid_names = [] + sort_predict = np.argsort(cached["preds"])[:, ::-1] + with open(str(prediction_path), 'w') as f: + for kk in range(cached["preds"].shape[0]): + pred_classes = [str(v) for v in sort_predict[kk, :]] + vid_name = cached["vid_name"][kk] + if key == "test": + vid_name = vid_name[kk].split('/')[-1] + '.mp4' + row = f"{vid_name} {' '.join(pred_classes)}" + print(row, file=f) + vid_names.append(vid_name) + save_name_path = log_dir / f"{key}_vid_name.pkl" + with open(save_name_path, 'wb') as f: + pickle.dump(vid_names, f) + self.logger.info(f"All {key} preds saved") + self.logger.info(f"Wrote result to: {str(prediction_path)}") + + if epoch > self.num_keep_ckpts: + self.purge_stale_models() + + def purge_stale_models(self): + """Remove models that are no longer neededself. + + NOTE: This function assumes that the `best` model has already been renamed + to have a format that differs from `model-epoch.pth` + """ + all_ckpts = list(self.model_dir.glob("*.pdparams")) + found_epoch_ckpts = list(self.model_dir.glob("model-epoch*.pdparams")) + if len(all_ckpts) <= self.num_keep_ckpts: + return + + msg = "Expected at the best model to have been renamed to a different format" + if not len(all_ckpts) > len(found_epoch_ckpts): + print("Warning, purging model, but the best epoch was not saved!") + # assert len(all_ckpts) > len(found_epoch_ckpts), msg + + # purge the oldest models + regex = r".*model-epoch(\d+)[.pdparams$" + epochs = [int(re.search(regex, str(x)).groups()[0]) for x in found_epoch_ckpts] + sorted_ckpts = sorted(list(zip(epochs, found_epoch_ckpts)), key=lambda x: -x[0]) + + for epoch, stale_ckpt in sorted_ckpts[self.num_keep_ckpts:]: + tic = time.time() + stale_ckpt.unlink() + msg = f"removing stale model [epoch {epoch}] [took {time.time() - tic:.2f}s]" + self.logger.info(msg) + + def _save_model(self, epoch, save_best=False): + """Saving models + + :param epoch: current epoch number + :param log: logging information of the epoch + :param save_best: if True, rename the saved model to 'trained_model.pdparams' + """ + arch = type(self.model).__name__ + state = { + 'arch': arch, + 'epoch': epoch, + 'state_dict': self.model.state_dict(), + 'monitor_best': self.mnt_best, + 'config': self.config + } + if self.include_optim_in_save_model: + state["optimizer"] = self.optimizer.state_dict() + + filename = str(self.model_dir / + 'model-epoch{}.pdparams'.format(epoch)) + tic = time.time() + self.logger.info("Saving model: {} ...".format(filename)) + paddle.save(state, filename) + self.logger.info(f"Done in {time.time() - tic:.3f}s") + if save_best: + self.logger.info("Updating 'best' model: {} ...".format(filename)) + best_path = str(self.model_dir / 'trained_model.pdparams') + paddle.save(state, best_path) + self.logger.info(f"Done in {time.time() - tic:.3f}s") + + def _resume_model(self, resume_path): + """ Resume from saved models + + :param resume_path: model path to be resumed + """ + resume_path = str(resume_path) + self.logger.info("Loading model: {} ...".format(resume_path)) + model = paddle.load(resume_path) + self.model.load_dict(model) + self.logger.info(f"model loaded. Resume training from epoch {self.start_epoch}") diff --git a/applications/T2VLAD/configs/base_config_transformers.json b/applications/T2VLAD/configs/base_config_transformers.json new file mode 100644 index 0000000000000000000000000000000000000000..8cd524bf930d1efa5aba117eb7cd986d4bf27806 --- /dev/null +++ b/applications/T2VLAD/configs/base_config_transformers.json @@ -0,0 +1,153 @@ +{ + "n_gpu": 1, + "seed": 0, + "log_name": "info.json", + "experts": { + "drop_feats": "", + "text_feat": "openai", + "text_agg": "vlad", + "text_dim": 768, + "modalities": [], + "ce_shared_dim": 768 + }, + "arch": { + "type": "CENet", + "args": { + "use_mish": 1, + "vlad_clusters": { + "features_audio": 16, + "features_speech": 16, + "features_face": 16, + "features_scene": 16, + "features_s3d": 16, + "features_rgb": 16, + "features_ocr": 16, + "features_flow": 16, + "text": 16 + }, + "ghost_clusters": { + "text": 0 + }, + "mimic_ce_dims": 0 + } + }, + + "data_loader": { + "type": "ExpertDataLoader", + "args": { + "batch_size": 32, + "num_workers": 8, + "root_feat_folder": "", + "feat_aggregation": { + "features_audio": { + "model": "mmt", + "flaky": true, + "temporal": "vlad", + "type": "embed", + "binarise": false, + "feat_dims": { + "embed": 128 + } + }, + "features_speech": { + "model": "mmt", + "flaky": true, + "temporal": "vlad", + "type": "embed", + "binarise": false, + "feat_dims": { + "embed": 300 + } + }, + "features_face": { + "model": "mmt", + "flaky": true, + "temporal": "vlad", + "type": "embed", + "binarise": false, + "feat_dims": { + "embed": 512 + } + }, + "features_ocr": { + "model": "mmt", + "flaky": true, + "temporal": "vlad", + "type": "embed", + "binarise": false, + "feat_dims": { + "embed": 300 + } + }, + "features_rgb": { + "model": "mmt", + "temporal": "vlad", + "type": "embed", + "binarise": false, + "feat_dims": { + "embed": 2048 + } + }, + "features_flow": { + "model": "mmt", + "temporal": "vlad", + "type": "embed", + "binarise": false, + "feat_dims": { + "embed": 1024 + } + }, + "features_s3d": { + "model": "mmt", + "temporal": "vlad", + "type": "embed", + "binarise": false, + "feat_dims": { + "embed": 1024 + } + }, + "features_scene": { + "model": "mmt", + "temporal": "vlad", + "type": "embed", + "binarise": false, + "feat_dims": { + "embed": 2208 + } + } + } + } + }, + "optimizer": { + "type": "Ranger", + "args":{ + "lr": 0.1, + "weight_decay": 1e-3 + } + }, + "loss": { + "type": "MaxMarginRankingLoss", + "args": { + "margin": 0.09381161988446174 + } + }, + "metrics": [ + "t2v_metrics", + "v2t_metrics" + ], + "lr_scheduler": { + "type": "StepLR", + "args": { + "step_size": 1, + "gamma": 0.95 + } + }, + "trainer": { + "save_only_best": true, + "save_dir": "data/saved/", + "save_period": 5, + "val_freq": 5, + "verbosity": 2, + "monitor": "max val_t2v_metrics_geometric_mean_R1-R5-R10" + } +} diff --git a/applications/T2VLAD/configs/msrvtt_transformers.json b/applications/T2VLAD/configs/msrvtt_transformers.json new file mode 100644 index 0000000000000000000000000000000000000000..f4730877ead51fd543c6b0cfb2636ef502c3db2c --- /dev/null +++ b/applications/T2VLAD/configs/msrvtt_transformers.json @@ -0,0 +1,96 @@ +{ + "inherit_from": "configs/base_config_transformers.json", + "eval_mode": "test_run", + "experts": { + "modalities": [ + "features_audio", + "features_rgb", + "features_ocr", + "features_speech", + "features_scene", + "features_flow", + "features_s3d", + "features_face" + ], + "face_dim": 512 + }, + "arch": { + "type": "CENet", + "args": { + "use_mish": 1, + "vlad_clusters": { + "text": 9 + }, + "ghost_clusters": { + "text": 1, + "features_ocr": 1, + "features_rgb": 1, + "features_flow": 1, + "features_scene": 1, + "features_s3d": 1, + "features_audio": 1, + "features_speech": 1, + "features_face": 1 + }, + "mimic_ce_dims": 1 + } + }, + "data_loader": { + "type": "ExpertDataLoader", + "args": { + "dataset_name": "MSRVTT", + "data_dir": "data/MSRVTT", + "split_name": "jsfusion", + "batch_size": 64, + "num_test_captions": 1, + "max_tokens": { + "features_ocr": 30, + "features_face": 30, + "features_rgb": 51, + "features_flow": 49, + "features_s3d": 30, + "features_scene": 31, + "features_speech": 112, + "features_audio": 31, + "text": 37 + } + } + }, + "loss": { + "type": "ContrastiveLoss", + "args": { + "margin": 0.2, + "topk": 1 + } + }, + "trainer": { + "epochs": 150 + }, + "optimizer": { + "type": "AdamW", + "args":{ + "weight_decay": 1e-4 + } + }, + "lr_scheduler": { + "type": "StepDecay", + "args": { + "learning_rate": 0.0001, + "step_size": 5, + "gamma": 0.9 + } + }, + "eval_settings": { + "data_loader": { + "args":{ + "split_name": "jsfusion", + "num_test_captions": 1 + } + }, + "tester": { + "save_dir": "data/saved/", + "verbosity": 2 + }, + "disable_gpu": false + } +} diff --git a/applications/T2VLAD/data/download_features.sh b/applications/T2VLAD/data/download_features.sh new file mode 100644 index 0000000000000000000000000000000000000000..3d325fd485858518e03dcd11f9eb47e7f7137f6a --- /dev/null +++ b/applications/T2VLAD/data/download_features.sh @@ -0,0 +1,9 @@ +mkdir MSRVTT +cd MSRVTT +wget https://videotag.bj.bcebos.com/Data/MSRVTT/aggregated_text_feats.tar +wget https://videotag.bj.bcebos.com/Data/MSRVTT/mmt_feats.tar +wget https://videotag.bj.bcebos.com/Data/MSRVTT/raw-captions.pkl +wget https://videotag.bj.bcebos.com/Data/MSRVTT/train_list_jsfusion.txt +wget https://videotag.bj.bcebos.com/Data/MSRVTT/val_list_jsfusion.txt +tar -xvf aggregated_text_feats.tar +tar -xvf mmt_feats.tar diff --git a/applications/T2VLAD/data_loader/MSRVTT_dataset.py b/applications/T2VLAD/data_loader/MSRVTT_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..fe41d3ea60f80933351eef46646836557b5fe67e --- /dev/null +++ b/applications/T2VLAD/data_loader/MSRVTT_dataset.py @@ -0,0 +1,126 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import copy + +from pathlib import Path +from utils import memory_summary +from typeguard import typechecked +from typing import Dict, Union, List +from base.base_dataset import BaseDataset +from zsvision.zs_utils import memcache, concat_features + +class MSRVTT(BaseDataset): + @staticmethod + @typechecked + def dataset_paths() -> Dict[str, Union[str, List[str], Path, Dict]]: + subset_paths = {} + split_name = "jsfusion" + train_list_path = "train_list_jsfusion.txt" + test_list_path = "val_list_jsfusion.txt" + # NOTE: The JSFusion split (referred to as 1k-A in the paper) uses all + # videos, but randomly samples a single caption per video from the test + # set for evaluation. To reproduce this evaluation, we use the indices + # of the test captions, and restrict to this subset during eval. + js_test_cap_idx_path = "jsfusion_val_caption_idx.pkl" + subset_paths[split_name] = {"train": train_list_path, "val": test_list_path} + custom_paths = { + "features_audio": ["mmt_feats/features.audio.pkl"], + "features_flow": ["mmt_feats/features.flow_agg.pkl"], + "features_rgb": ["mmt_feats/features.rgb_agg.pkl"], + "features_scene": ["mmt_feats/features.scene.pkl"], + "features_face": ["mmt_feats/features.face_agg.pkl"], + "features_ocr": ["mmt_feats/features.ocr.pkl"], + "features_s3d": ["mmt_feats/features.s3d.pkl"], + "features_speech": ["mmt_feats/features.speech.pkl"], + } + text_feat_paths = { + "openai": "w2v_MSRVTT_openAIGPT.pickle", + } + text_feat_paths = {key: Path("aggregated_text_feats") / fname + for key, fname in text_feat_paths.items()} + feature_info = { + "custom_paths": custom_paths, + "subset_list_paths": subset_paths, + "text_feat_paths": text_feat_paths, + "raw_captions_path": "raw-captions.pkl", + "js_test_cap_idx_path": js_test_cap_idx_path, + } + return feature_info + + def load_features(self): + root_feat = Path(self.root_feat) + feat_names = {} + custom_path_key = "custom_paths" + feat_names.update(self.paths[custom_path_key]) + features = {} + for expert, rel_names in feat_names.items(): + if expert not in self.ordered_experts: + continue + feat_paths = tuple([root_feat / rel_name for rel_name in rel_names]) + if len(feat_paths) == 1: + features[expert] = memcache(feat_paths[0]) + else: + # support multiple forms of feature (e.g. max and avg pooling). For + # now, we only support direct concatenation + msg = f"{expert}: Only direct concatenation of muliple feats is possible" + print(f"Concatenating aggregates for {expert}....") + is_concat = self.feat_aggregation[expert]["aggregate"] == "concat" + self.log_assert(is_concat, msg=msg) + axis = self.feat_aggregation[expert]["aggregate-axis"] + x = concat_features.cache_info() # pylint: disable=no-value-for-parameter + print(f"concat cache info: {x}") + features_ = concat_features(feat_paths, axis=axis) + memory_summary() + + # Make separate feature copies for each split to allow in-place filtering + features[expert] = copy.deepcopy(features_) + + self.features = features + self.raw_captions = memcache(root_feat / self.paths["raw_captions_path"]) + text_feat_path = root_feat / self.paths["text_feat_paths"][self.text_feat] + self.text_features = memcache(text_feat_path) + + if self.restrict_train_captions: + # hash the video names to avoid O(n) lookups in long lists + train_list = set(self.partition_lists["train"]) + for key, val in self.text_features.items(): + if key not in train_list: + continue + + if not self.split_name == "full-test": + # Note that we do not perform this sanity check for the full-test + # split, because the text features in the cached dataset will + # already have been cropped to the specified + # `resstrict_train_captions` + expect = {19, 20} + msg = f"expected train text feats as lists with length {expect}" + has_expected_feats = isinstance(val, list) and len(val) in expect + self.log_assert(has_expected_feats, msg=msg) + + # restrict to the first N captions (deterministic) + self.text_features[key] = val[:self.restrict_train_captions] + self.summary_stats() + + def sanity_checks(self): + if self.num_test_captions == 20: + if len(self.partition_lists["val"]) == 2990: + missing = 6 + elif len(self.partition_lists["val"]) == 1000: + missing = 2 + elif len(self.partition_lists["val"]) == 497: + missing = 0 + else: + raise ValueError("unrecognised test set") + msg = "Expected to find two missing queries in MSRVTT for full eval" + correct_missing = self.query_masks.sum() == self.query_masks.size - missing + self.log_assert(correct_missing, msg=msg) diff --git a/applications/T2VLAD/data_loader/data_loaders.py b/applications/T2VLAD/data_loader/data_loaders.py new file mode 100644 index 0000000000000000000000000000000000000000..fe64e61921eafc20b2fe6360206ad043202ec2a7 --- /dev/null +++ b/applications/T2VLAD/data_loader/data_loaders.py @@ -0,0 +1,145 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import paddle +import logging +import functools +from pathlib import Path +from typing import Dict, List + +from typeguard import typechecked +from zsvision.zs_utils import memcache + +from data_loader.MSRVTT_dataset import MSRVTT +from utils import HashableDict, HashableOrderedDict + + + +@functools.lru_cache(maxsize=64, typed=False) +def dataset_loader( + use_zeros_for_missing: bool, + eval_only: bool, + data_dir: str, + text_agg: str, + text_feat: str, + split_name: str, + dataset_name: str, + cls_partition: str, + root_feat_folder: str, + text_dim: int, + num_test_captions: int, + restrict_train_captions: int, + logger: logging.Logger, + max_tokens: Dict[str, int], + raw_input_dims: HashableOrderedDict, + feat_aggregation: HashableDict, +): + print(f"refreshing cache for {dataset_name} data loader [{split_name}]") + kwargs = dict( + data_dir=Path(data_dir), + text_dim=text_dim, + logger=logger, + eval_only=eval_only, + text_agg=text_agg, + text_feat=text_feat, + max_tokens=max_tokens, + split_name=split_name, + cls_partition=cls_partition, + raw_input_dims=raw_input_dims, + root_feat_folder=root_feat_folder, + feat_aggregation=feat_aggregation, + num_test_captions=num_test_captions, + use_zeros_for_missing=use_zeros_for_missing, + restrict_train_captions=restrict_train_captions, + ) + if dataset_name == "MSRVTT": + dataset = MSRVTT(**kwargs) + return dataset + + +class ExpertDataLoader: + + @typechecked + def __init__( + self, + eval_only: bool, + use_zeros_for_missing: bool, + text_dim: int, + batch_size: int, + num_workers: int, + num_test_captions: int, + data_dir: str, + text_agg: str, + text_feat: str, + split_name: str, + dataset_name: str, + root_feat_folder: str, + max_tokens: Dict[str, int], + raw_input_dims: Dict[str, int], + feat_aggregation: Dict[str, Dict], + logger: logging.Logger, + restrict_train_captions: int = 0, + drop_last: bool = False, + refresh_lru_cache: bool = False, + ): + + # Ensure that the dictionaries are hashable to allow use of caching + raw_input_dims = HashableOrderedDict(raw_input_dims) + feat_aggregation = HashableDict(feat_aggregation) + max_tokens = HashableDict(max_tokens) + + if refresh_lru_cache: + logger.info("Explicitly refreshing dataloader and cuda cache") + dataset_loader.cache_clear() + memcache.cache_clear() + + common_kwargs = dict( + logger=logger, + data_dir=data_dir, + text_dim=text_dim, + text_agg=text_agg, + eval_only=eval_only, + text_feat=text_feat, + max_tokens=max_tokens, + dataset_name=dataset_name, + split_name=split_name, + root_feat_folder=root_feat_folder, + use_zeros_for_missing=use_zeros_for_missing, + num_test_captions=num_test_captions, + raw_input_dims=raw_input_dims, + feat_aggregation=feat_aggregation, + restrict_train_captions=restrict_train_captions, + ) + + dataset = dataset_loader(cls_partition="train", **common_kwargs) + x = dataset_loader.cache_info() # pylint: disable=no-value-for-parameter + logger.info(f"cache info {x}") + self.dataloaders = {"dataset": dataset} + self.dataloaders["retrieval"] = dataset.get_retrieval_data() + + if not eval_only: + train_loader = paddle.io.DataLoader( + dataset=dataset, + batch_size=batch_size, + num_workers=num_workers, + collate_fn=dataset.collate_data, + drop_last=drop_last, + shuffle=True, + ) + self.dataloaders["train"] = train_loader + + logger.info(f"Loading data loaders with {num_workers} workers") + self.num_test_captions = num_test_captions + self.dataset_name = dataset_name + + def __getitem__(self, key): + return self.dataloaders[key] diff --git a/applications/T2VLAD/imgs/t2vlad.png b/applications/T2VLAD/imgs/t2vlad.png new file mode 100644 index 0000000000000000000000000000000000000000..883ada0985d8777c1364d40bf7c029354a7416a0 Binary files /dev/null and b/applications/T2VLAD/imgs/t2vlad.png differ diff --git a/applications/T2VLAD/logger/__init__.py b/applications/T2VLAD/logger/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..086cb23716c9336116ccef95a86bbea16dd61d5e --- /dev/null +++ b/applications/T2VLAD/logger/__init__.py @@ -0,0 +1,2 @@ +from .logger import * +from .log_parser import * \ No newline at end of file diff --git a/applications/T2VLAD/logger/log_parser.py b/applications/T2VLAD/logger/log_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..73fa355a3be7b356fc637784d626db64d03822cb --- /dev/null +++ b/applications/T2VLAD/logger/log_parser.py @@ -0,0 +1,104 @@ +import re +import scipy.stats +import logging +import numpy as np +from collections import defaultdict + + +def log_summary(logger, log_path, eval_mode="test_run", fixed_num_epochs=None): + """Extract performace statistics from experiment log files. + + Args: + logger (logger): reference to primary logging instance + log_path (Path): the path to the log file + eval_mode (str): the method use to collect the statistics. Can be one of: + `test_run`, `fixed_num_epochs` or `geometric_mean` + + NOTE: The `eval_mode` argument differs by dataset: for datasets which provide a + validation set, we use validation set performance to complete a single test run. For + datasets where no validation set is available, we aim to match prior work by either + fixing the number of training epochs, or selecting directly from validation set + performance (Details can be found in the supplementary material of the paper.) + """ + with open(str(log_path), "r") as f: + log = f.read().splitlines() + + # keep track of the random seed used for the part of the logfile being processed + current_seed = None + + # Regex tag for finding the seed + seed_tag = "Setting experiment random seed to" + + if eval_mode == "test_run": + subset = "test" + else: + subset = "val" + + for mode in "t2v", "v2t": + logger.info("") + logger.info("----------------------------------------------------") + logger.info(f"[{mode}] loaded log file with {len(log)} lines....") + logger.info("----------------------------------------------------") + + # Search for the following metrics + scores = { + "R1": defaultdict(list), + "R5": defaultdict(list), + "R10": defaultdict(list), + "R50": defaultdict(list), + "MedR": defaultdict(list), + "MeanR": defaultdict(list), + } + + for row in log: + if seed_tag in row: + # Search for the log file entry describing the current random seed + match = re.search(seed_tag + " (\d+)$", row) # NOQA + assert len(match.groups()) == 1, "expected a single regex match" + current_seed = match.groups()[0] + + if f"{subset}_{mode}_metrics" in row: + tokens = row.split(" ") + for key in scores: + tag = f"{subset}_{mode}_metrics_{key}:" + if tag in tokens: + pos = tokens.index(tag) + 1 + val = tokens[pos] + val = float(val) + assert current_seed is not None, "failed to determine the seed" + scores[key][current_seed].append(val) + + agg_scores = {"R1": [], "R5": [], "R10": [], "R50": [], "MedR": [], "MeanR": []} + + # compute the best performance for a single epoch (i.e. sharing the same model + # to compute all stats) + geometric_stats = defaultdict(list) + best_epochs = {} + if eval_mode == "geometric_mean": + raise NotImplementedError("Need to fix this for new log format") + consider = ["R1", "R5", "R10"] + seeds = list(scores["R1"].keys()) + for seed in seeds: + for metric, subdict in scores.items(): + if metric in consider: + geometric_stats[seed].append(subdict[seed]) + gms_raw = np.array(geometric_stats[seed]) + geo_means = scipy.stats.mstats.gmean(gms_raw, axis=0) + best_epochs[seed] = np.argmax(geo_means) + + for metric, subdict in scores.items(): + for seed, values in subdict.items(): + if eval_mode == "test_run": + stat = values[0] + elif eval_mode == "fixed_num_epochs": + stat = values[fixed_num_epochs - 1] + elif "LSMDC" in log_path and eval_mode == "geometric_mean": + stat = values[best_epochs[seed]] + else: + raise ValueError(f"unrecognised eval_mode: {eval_mode}") + agg_scores[metric].append(stat) + + if eval_mode == "fixed_num_epochs": + logger.info(f"Reporting stats with fixed training length: {fixed_num_epochs}") + for metric, values in agg_scores.items(): + logger.info(f"{metric}: {np.mean(values):.1f}, {np.std(values, ddof=1):.1f}") diff --git a/applications/T2VLAD/logger/logger.py b/applications/T2VLAD/logger/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..b2fbebd31dbf52e68cd7ff532694cbb09f283b92 --- /dev/null +++ b/applications/T2VLAD/logger/logger.py @@ -0,0 +1,25 @@ +import os +import logging +import logging.config +from pathlib import Path +from utils import read_json + + +def setup_logging(save_dir, log_config='logger/logger_config.json', + default_level=logging.INFO): + """Setup logging configuration.""" + print(os.getcwd()) + log_config = Path(log_config) + print(f"log config: {log_config} exists: {log_config.exists()}") + if log_config.is_file(): + config = read_json(log_config) + # modify logging paths based on run config + for _, handler in config['handlers'].items(): + if 'filename' in handler: + handler['filename'] = str(save_dir / handler['filename']) + + logging.config.dictConfig(config) + else: + print(f"Warning: logging configuration file is not found in {log_config}.") + logging.basicConfig(level=default_level) + return config["handlers"]["info_file_handler"]["filename"] diff --git a/applications/T2VLAD/logger/logger_config.json b/applications/T2VLAD/logger/logger_config.json new file mode 100644 index 0000000000000000000000000000000000000000..c3e7e02c12cbf1083b23c124fbdd37f66d7f2ce1 --- /dev/null +++ b/applications/T2VLAD/logger/logger_config.json @@ -0,0 +1,32 @@ + +{ + "version": 1, + "disable_existing_loggers": false, + "formatters": { + "simple": {"format": "%(message)s"}, + "datetime": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"} + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout" + }, + "info_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "datetime", + "filename": "info.log", + "maxBytes": 10485760, + "backupCount": 20, "encoding": "utf8" + } + }, + "root": { + "level": "INFO", + "handlers": [ + "console", + "info_file_handler" + ] + } +} \ No newline at end of file diff --git a/applications/T2VLAD/model/loss.py b/applications/T2VLAD/model/loss.py new file mode 100644 index 0000000000000000000000000000000000000000..263fda15d95e988f776ef878e7e905545fdbe932 --- /dev/null +++ b/applications/T2VLAD/model/loss.py @@ -0,0 +1,102 @@ +"""This module contains an implementation of the max margin ranking loss, slightly +modified from this code: +https://github.com/antoine77340/Mixture-of-Embedding-Experts/blob/master/loss.py + +The modification is the `fix_norm` conditional, which removes zero terms from the +diagonal when performing the averaging calculation. + +Original licence below. +""" +# Copyright 2021 Antoine Miech All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +def cosine_sim(im, s): + '''cosine similarity between all the image and sentence pairs + ''' + inner_prod = im.mm(s.t()) + im_norm = paddle.sqrt((im ** 2).sum(axis=1).reshape([-1, 1]) + 1e-18) + s_norm = paddle.sqrt((s ** 2).sum(axis=1).reshape([-1, 1]) + 1e-18) + sim = inner_prod / (im_norm * s_norm) + return sim + +class ContrastiveLoss(nn.Layer): + '''compute contrastive loss + ''' + def __init__(self, margin=0, max_violation=True, direction='bi', topk=1): + '''Args: + direction: i2t for negative sentence, t2i for negative image, bi for both + ''' + super().__init__() + self.margin = margin + self.max_violation = max_violation + self.direction = direction + self.topk = topk + + def forward(self, scores, margin=None, average_batch=True): + ''' + Args: + scores: image-sentence score matrix, (batch, batch) + the same row of im and s are positive pairs, different rows are negative pairs + ''' + + if margin is None: + margin = self.margin + + batch_size = scores.shape[0] + diagonal = paddle.diagonal(scores).reshape([batch_size, 1]) + # mask to clear diagonals which are positive pairs + pos_masks = paddle.eye(batch_size).astype('bool') + + batch_topk = min(batch_size, self.topk) + if self.direction == 'i2t' or self.direction == 'bi': + d1 = diagonal.expand_as(scores) # same collumn for im2s (negative sentence) + # compare every diagonal score to scores in its collumn + # caption retrieval + cost_s = (margin + scores - d1).clip(min=0) + cost_s[pos_masks] = 0 + if self.max_violation: + cost_s, _ = paddle.topk(cost_s, batch_topk, axis=1) + cost_s = cost_s / batch_topk + if average_batch: + cost_s = cost_s / batch_size + else: + if average_batch: + cost_s = cost_s / (batch_size * (batch_size - 1)) + cost_s = paddle.sum(cost_s) + + if self.direction == 't2i' or self.direction == 'bi': + d2 = diagonal.t().expand_as(scores) # same row for s2im (negative image) + # compare every diagonal score to scores in its row + cost_im = (margin + scores - d2).clip(min=0) + cost_im[pos_masks] = 0 + if self.max_violation: + cost_im, _ = paddle.topk(cost_im, batch_topk, axis=0) + cost_im = cost_im / batch_topk + if average_batch: + cost_im = cost_im / batch_size + else: + if average_batch: + cost_im = cost_im / (batch_size * (batch_size - 1)) + cost_im = paddle.sum(cost_im) + + if self.direction == 'i2t': + return cost_s + elif self.direction == 't2i': + return cost_im + else: + return cost_s + cost_im diff --git a/applications/T2VLAD/model/metric.py b/applications/T2VLAD/model/metric.py new file mode 100644 index 0000000000000000000000000000000000000000..0db6eeb4b1b17291b9319987031ec44d18440726 --- /dev/null +++ b/applications/T2VLAD/model/metric.py @@ -0,0 +1,243 @@ +# Copyright 2021 Antoine Miech All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import paddle +import numbers +import scipy.stats +import numpy as np + +from pathlib import Path +from sklearn.metrics import average_precision_score + +def t2v_metrics(sims, query_masks=None): + """Compute retrieval metrics from a similiarity matrix. + + Args: + sims (th.Tensor): N x M matrix of similarities between embeddings, where + x_{i,j} = + query_masks (th.Tensor): mask any missing queries from the dataset (two videos + in MSRVTT only have 19, rather than 20 captions) + + Returns: + (dict[str:float]): retrieval metrics + """ + assert sims.ndim == 2, "expected a matrix" + num_queries, num_vids = sims.shape + dists = -sims + sorted_dists = np.sort(dists, axis=1) + + if False: + import sys + import matplotlib + from pathlib import Path + matplotlib.use("Agg") + import matplotlib.pyplot as plt + sys.path.insert(0, str(Path.home() / "coding/src/zsvision/python")) + from zsvision.zs_iterm import zs_dispFig # NOQA + plt.matshow(dists) + zs_dispFig() + import ipdb; ipdb.set_trace() + + # The indices are computed such that they slice out the ground truth distances + # from the psuedo-rectangular dist matrix + queries_per_video = num_queries // num_vids + gt_idx = [[np.ravel_multi_index([ii, jj], (num_queries, num_vids)) + for ii in range(jj * queries_per_video, (jj + 1) * queries_per_video)] + for jj in range(num_vids)] + gt_idx = np.array(gt_idx) + gt_dists = dists.reshape(-1)[gt_idx.reshape(-1)] + gt_dists = gt_dists[:, np.newaxis] + rows, cols = np.where((sorted_dists - gt_dists) == 0) # find column position of GT + + # -------------------------------- + # NOTE: Breaking ties + # -------------------------------- + # We sometimes need to break ties (in general, these should occur extremely rarely, + # but there are pathological cases when they can distort the scores, such as when + # the similarity matrix is all zeros). Previous implementations (e.g. the t2i + # evaluation function used + # here: https://github.com/niluthpol/multimodal_vtt/blob/master/evaluation.py and + # here: https://github.com/linxd5/VSE_Pytorch/blob/master/evaluation.py#L87) generally + # break ties "optimistically". However, if the similarity matrix is constant this + # can evaluate to a perfect ranking. A principled option is to average over all + # possible partial orderings implied by the ties. See # this paper for a discussion: + # McSherry, Frank, and Marc Najork, + # "Computing information retrieval performance measures efficiently in the presence + # of tied scores." European conference on information retrieval. Springer, Berlin, + # Heidelberg, 2008. + # http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.145.8892&rep=rep1&type=pdf + + # break_ties = "optimistically" + break_ties = "averaging" + + if rows.size > num_queries: + assert np.unique(rows).size == num_queries, "issue in metric evaluation" + if break_ties == "optimistically": + _, idx = np.unique(rows, return_index=True) + cols = cols[idx] + elif break_ties == "averaging": + # fast implementation, based on this code: + # https://stackoverflow.com/a/49239335 + locs = np.argwhere((sorted_dists - gt_dists) == 0) + + # Find the split indices + steps = np.diff(locs[:, 0]) + splits = np.nonzero(steps)[0] + 1 + splits = np.insert(splits, 0, 0) + + # Compute the result columns + summed_cols = np.add.reduceat(locs[:, 1], splits) + counts = np.diff(np.append(splits, locs.shape[0])) + avg_cols = summed_cols / counts + if False: + print("Running slower code to verify rank averaging across ties") + # slow, but more interpretable version, used for testing + avg_cols_slow = [np.mean(cols[rows == idx]) for idx in range(num_queries)] + assert np.array_equal(avg_cols, avg_cols_slow), "slow vs fast difference" + print("passed num check") + cols = avg_cols + + msg = "expected ranks to match queries ({} vs {}) " + if cols.size != num_queries: + import ipdb; ipdb.set_trace() + assert cols.size == num_queries, msg + + if False: + # overload mask to check that we can recover the scores for single-query + # retrieval + print("DEBUGGING MODE") + query_masks = np.zeros_like(query_masks) + query_masks[:, 0] = 1 # recover single query score + + if query_masks is not None: + # remove invalid queries + assert query_masks.size == num_queries, "invalid query mask shape" + cols = cols[query_masks.reshape(-1).astype(np.bool)] + assert cols.size == query_masks.sum(), "masking was not applied correctly" + # update number of queries to account for those that were missing + num_queries = query_masks.sum() + + if False: + # sanity check against old logic for square matrices + gt_dists_old = np.diag(dists) + gt_dists_old = gt_dists_old[:, np.newaxis] + _, cols_old = np.where((sorted_dists - gt_dists_old) == 0) + assert np.array_equal(cols_old, cols), "new metric doesn't match" + + return cols2metrics(cols, num_queries) + + +def v2t_metrics(sims, query_masks=None): + """Compute retrieval metrics from a similiarity matrix. + + Args: + sims (th.Tensor): N x M matrix of similarities between embeddings, where + x_{i,j} = + query_masks (th.Tensor): mask any missing captions from the dataset + + Returns: + (dict[str:float]): retrieval metrics + + NOTES: We find the closest "GT caption" in the style of VSE, which corresponds + to finding the rank of the closest relevant caption in embedding space: + github.com/ryankiros/visual-semantic-embedding/blob/master/evaluation.py#L52-L56 + """ + # switch axes of text and video + sims = sims.T + + if False: + # experiment with toy example + sims = np.ones((3, 3)) + sims[0, 0] = 2 + sims[1, 1:2] = 2 + sims[2, :] = 2 + query_masks = None + + assert sims.ndim == 2, "expected a matrix" + num_queries, num_caps = sims.shape + dists = -sims + caps_per_video = num_caps // num_queries + break_ties = "averaging" + + MISSING_VAL = 1E8 + query_ranks = [] + for ii in range(num_queries): + row_dists = dists[ii, :] + if query_masks is not None: + # Set missing queries to have a distance of infinity. A missing query + # refers to a query position `n` for a video that had less than `n` + # captions (for example, a few MSRVTT videos only have 19 queries) + row_dists[np.logical_not(query_masks.reshape(-1))] = MISSING_VAL + + # NOTE: Using distance subtraction to perform the ranking is easier to make + # deterministic than using argsort, which suffers from the issue of defining + # "stability" for equal distances. Example of distance subtraction code: + # github.com/antoine77340/Mixture-of-Embedding-Experts/blob/master/train.py + sorted_dists = np.sort(row_dists) + + min_rank = np.inf + for jj in range(ii * caps_per_video, (ii + 1) * caps_per_video): + if row_dists[jj] == MISSING_VAL: + # skip rankings of missing captions + continue + ranks = np.where((sorted_dists - row_dists[jj]) == 0)[0] + if break_ties == "optimistically": + rank = ranks[0] + elif break_ties == "averaging": + # NOTE: If there is more than one caption per video, its possible for the + # method to do "worse than chance" in the degenerate case when all + # similarities are tied. TODO(Samuel): Address this case. + rank = ranks.mean() + if rank < min_rank: + min_rank = rank + query_ranks.append(min_rank) + query_ranks = np.array(query_ranks) + + # sanity check against old version of code + if False: + sorted_dists = np.sort(dists, axis=1) + gt_dists_old = np.diag(dists) + gt_dists_old = gt_dists_old[:, np.newaxis] + rows_old, cols_old = np.where((sorted_dists - gt_dists_old) == 0) + if rows_old.size > num_queries: + _, idx = np.unique(rows_old, return_index=True) + cols_old = cols_old[idx] + num_diffs = (1 - (cols_old == query_ranks)).sum() + msg = f"new metric doesn't match in {num_diffs} places" + assert np.array_equal(cols_old, query_ranks), msg + + # visualise the distance matrix + import sys + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + sys.path.insert(0, str(Path.home() / "coding/src/zsvision/python")) + from zsvision.zs_iterm import zs_dispFig # NOQA + plt.matshow(dists) + zs_dispFig() + + return cols2metrics(query_ranks, num_queries) + +def cols2metrics(cols, num_queries): + metrics = {} + metrics["R1"] = 100 * float(np.sum(cols == 0)) / num_queries + metrics["R5"] = 100 * float(np.sum(cols < 5)) / num_queries + metrics["R10"] = 100 * float(np.sum(cols < 10)) / num_queries + metrics["R50"] = 100 * float(np.sum(cols < 50)) / num_queries + metrics["MedR"] = np.median(cols) + 1 + metrics["MeanR"] = np.mean(cols) + 1 + stats = [metrics[x] for x in ("R1", "R5", "R10")] + metrics["geometric_mean_R1-R5-R10"] = scipy.stats.mstats.gmean(stats) + return metrics diff --git a/applications/T2VLAD/model/model.py b/applications/T2VLAD/model/model.py new file mode 100644 index 0000000000000000000000000000000000000000..a2e9fc99a73a6d091d974224681b1d4fd311e7d6 --- /dev/null +++ b/applications/T2VLAD/model/model.py @@ -0,0 +1,533 @@ +# Copyright 2021 Antoine Miech All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import time +import itertools + +import paddle +import numpy as np +import paddle.nn as nn +import paddle.nn.functional as F + +from paddle import Tensor +from typing import Optional +from collections import OrderedDict + +from base import BaseModel +from model.net_vlad import NetVLAD +try: + from paddlenlp.transformers import BertModel +except ImportError as e: + print( + f"{e}, [paddlenlp] package and it's dependencies is required for T2VLAD." + ) + + +class Mish(nn.Layer): + ''' + Applies the mish function element-wise: + mish(x) = x * tanh(softplus(x)) = x * tanh(ln(1 + exp(x))) + SRC: https://github.com/digantamisra98/Mish/blob/master/Mish/Torch/mish.py + ''' + def forward(self, input): + ''' + Forward pass of the function. + ''' + return input * paddle.tanh(F.softplus(input)) + + +def kronecker_prod(t1, t2): + # kronecker is performed along the last dim + kron = paddle.bmm(t1.reshape([-1, t1.size(-1)], 1), + t2.reshape([-1, 1, t2.size(-1)])) + return kron.reshape[(t1.shape[0], t1.shape[1], -1)] + + +def drop_nans(x, ind, validate_missing): + """Remove nans, which we expect to find at missing indices. + Args: + x (paddle.Tensor): features + ind (paddle.Tensor): binary values denoting whether or not a given feature is present + validate_missing (bool): whether to validate that the missing location contains a nan. + + Returns: + (paddle.tensor): the features, with the missing values masked to zero. + """ + + missing = paddle.nonzero(ind == 0).flatten() + if missing.numel(): + if validate_missing: + vals = x[missing[0]] + assert paddle.isnan(vals.reshape( + [-1])[0]), "expected nans at missing locations" + #Prevent overwrite of the original tensor + x_ = x + x_[missing] = 0 + x = x_ + if paddle.isnan(x).sum() > 0: + raise ValueError("Still find nans after removing it!") + return x + + +class CENet(BaseModel): + def __init__(self, text_dim, expert_dims, vlad_clusters, ghost_clusters, + feat_aggregation, ce_shared_dim, use_mish, mimic_ce_dims): + super().__init__() + self.expert_dims = expert_dims + self.feat_aggregation = feat_aggregation + + vlad_feat_sizes = {key: val for key, val in vlad_clusters.items()} + + if vlad_clusters["text"] == 0: + self.text_pooling = nn.Sequential() + else: + self.text_pooling = NetVLAD( + feature_size=text_dim, + cluster_size=vlad_clusters["text"], + ghost_clusters=ghost_clusters["text"], + ) + self.text_bert = BertModel.from_pretrained('bert-base-uncased') + text_dim = self.text_pooling.out_dim + + self.ce = CEModule( + text_dim=text_dim, + expert_dims=expert_dims, + vlad_feat_sizes=vlad_feat_sizes, + mimic_ce_dims=mimic_ce_dims, + use_mish=use_mish, + same_dim=ce_shared_dim, + ) + + def forward(self, + experts, + ind, + cap_id=None, + att_mask=None, + text=None, + raw_captions=None, + text_token_mask=None): + aggregated_experts = OrderedDict() + + # Handle all nan-checks + for mod in self.expert_dims: + experts[mod] = drop_nans(x=experts[mod], + ind=ind[mod], + validate_missing=True) + aggregated_experts[mod] = experts[mod] + + start = time.time() + # When pooling multiple captions for a single video, we treat them as separate + # members of the minibatch, so the total pooling op does the following: + # pooling: B x captions_per_video x max_sentence_length x text_feat_dim + # -> B x captions_per_video (cluster_dim * text_feat_dim) + B, captions_per_video, max_words, text_feat_dim = text.shape + text = text.reshape([B * captions_per_video, max_words, text_feat_dim]) + if isinstance(self.text_pooling, NetVLAD): + kwargs = {"mask": text_token_mask} + else: + kwargs = {} + cap_id = cap_id.reshape([B * captions_per_video, -1]) + att_mask = att_mask.reshape([B * captions_per_video, -1]) + att_mask = att_mask.unsqueeze(axis=[1, 2]) + bert_out = self.text_bert(cap_id, + token_type_ids=None, + attention_mask=att_mask) + text = bert_out[0] + text, _, save_ass = self.text_pooling(text, **kwargs) + text = text.reshape([B, captions_per_video, -1]) + + return self.ce(text, aggregated_experts, ind, raw_captions, + self.text_pooling, start) + + +def _get_clones(module, N): + return nn.LayerList([copy.deepcopy(module) for i in range(N)]) + + +class TransformerLayer(nn.Layer): + def __init__(self, + d_model, + nhead, + dim_feedforward=2048, + dropout=0.1, + activation="relu", + normalize_before=True): + super().__init__() + self.self_attn = nn.MultiHeadAttention(d_model, nhead, dropout=dropout) + # Implementation of Feedforward model + self.linear1 = nn.Linear(d_model, dim_feedforward) + self.dropout = nn.Dropout(dropout) + self.linear2 = nn.Linear(dim_feedforward, d_model) + + self.norm1 = nn.LayerNorm(d_model) + self.norm2 = nn.LayerNorm(d_model) + self.dropout1 = nn.Dropout(dropout) + self.dropout2 = nn.Dropout(dropout) + + self.activation = F.relu + self.normalize_before = normalize_before + + def with_pos_embed(self, tensor, pos: Optional[Tensor]): + return tensor if pos is None else tensor + pos + + def forward_post(self, + src, + src_mask: Optional[Tensor] = None, + pos: Optional[Tensor] = None): + q = k = self.with_pos_embed(src, pos) + q = q.transpose([1, 0, 2]) + k = k.transpose([1, 0, 2]) + src = src.transpose([1, 0, 2]) + src2 = self.self_attn(q, k, value=src, attn_mask=src_mask) + src2 = src2.transpose([1, 0, 2]) + src = src + self.dropout1(src2) + src = self.norm1(src) + src2 = self.linear2(self.dropout(self.activation(self.linear1(src)))) + src = src + self.dropout2(src2) + src = self.norm2(src) + return src + + def forward_pre(self, + src, + src_mask: Optional[Tensor] = None, + pos: Optional[Tensor] = None): + src2 = self.norm1(src) + q = k = self.with_pos_embed(src2, pos) + q = q.transpose([1, 0, 2]) + k = k.transpose([1, 0, 2]) + src2 = src2.transpose([1, 0, 2]) + src2 = self.self_attn(q, key=k, value=src2, attn_mask=src_mask) + src2 = src2.transpose([1, 0, 2]) + src = src + self.dropout1(src2) + src2 = self.norm2(src) + src2 = self.linear2(self.dropout(self.activation(self.linear1(src2)))) + src = src + self.dropout2(src2) + return src + + def forward(self, + src, + src_mask: Optional[Tensor] = None, + pos: Optional[Tensor] = None): + if self.normalize_before: + return self.forward_pre(src, src_mask, pos) + return self.forward_post(src, src_mask, pos) + + +class Transformer(nn.Layer): + def __init__(self, encoder_layer, num_layers, norm=None): + super().__init__() + self.layers = _get_clones(encoder_layer, num_layers) + self.num_layers = num_layers + self.norm = norm + self._reset_parameters() + + def _reset_parameters(self): + for p in self.parameters(): # may have a problem + if p.dim() > 1: + nn.initializer.XavierUniform(p) + + def forward(self, + src, + mask: Optional[Tensor] = None, + pos: Optional[Tensor] = None): + output = src + + for layer in self.layers: + output = layer(output) + + if self.norm is not None: + output = self.norm(output) + + return output + + +class CEModule(nn.Layer): + def __init__(self, expert_dims, text_dim, use_mish, mimic_ce_dims, + vlad_feat_sizes, same_dim): + super().__init__() + + modalities = list(expert_dims.keys()) + self.expert_dims = expert_dims + self.modalities = modalities + self.mimic_ce_dims = mimic_ce_dims + self.same_dim = same_dim + self.use_mish = use_mish + self.vlad_feat_sizes = vlad_feat_sizes + self.reduce_dim = 64 + self.moe_cg = ContextGating + self.vis_transformer = True + + if self.use_mish: + self.non_lin = Mish() + else: + self.non_lin = nn.ReLU() + + num_mods = len(expert_dims) + self.moe_fc = nn.Linear(text_dim, len(expert_dims)) + self.moe_weights = paddle.ones([1, num_mods]) / num_mods + + # The batch size of the face input can vary (due to missing inputs), so we + # probably shouldn't use BN on this branch. It's probably fine to leave it + # n for the corresponding text inputs, (but we should switch to GN) + use_bns = [True for modality in self.modalities] + + # NOTE: When use_ce is not used, the text features are projected to + # subspaces of different dimensions. When use_ce is used, they must all + # be projected to `same_dim` (to allow fusion). The only excpetion is for an + # ablation in which we mimic the `same_dim` reduction to measure whether this + # projection influences overall performance. + + self.repeat_temporal = {} + for mod in modalities: + self.repeat_temporal[mod] = 1 + + in_dims = [ + expert_dims[mod][0] * self.repeat_temporal[mod] + for mod in modalities + ] + agg_dims = [ + expert_dims[mod][1] * self.repeat_temporal[mod] + for mod in modalities + ] + feat_dims = [ + expert_dims[mod][0] // self.vlad_feat_sizes[mod] + for mod in modalities + ] + if self.vis_transformer: + num_encoder_layers = 1 + d_model = 768 + nhead = 4 + dim_feedforward = 768 + dropout = 0 #dropout=0.1 + normalize_before = True + encoder_layer = TransformerLayer(d_model, nhead, dim_feedforward, + dropout) + encoder_norm = nn.LayerNorm(d_model) if normalize_before else None + self.transformers = Transformer(encoder_layer, num_encoder_layers, + encoder_norm) + + if self.mimic_ce_dims: + dim_reducers = [ReduceDim(in_dim, same_dim) for in_dim in feat_dims] + self.video_dim_reduce = nn.LayerList(dim_reducers) + + gated_vid_embds = [ + GatedEmbeddingUnit(in_dim, same_dim, use_bn=True) + for in_dim in feat_dims + ] + text_out_dims = [same_dim for _ in agg_dims] + self.video_GU = nn.LayerList(gated_vid_embds) + gated_text_embds = [ + GatedEmbeddingUnit(text_dim, dim, use_bn=True) + for dim in text_out_dims + ] + self.text_GU = nn.LayerList(gated_text_embds) + + def compute_moe_weights(self, text, ind): + # compute weights for all captions (including when assigned K captions to + # the same video) + B, K, D = text.shape + M = len(self.modalities) + msg = f"expected between 1 and 10 modalities, found {M} ({self.modalities})" + assert 1 <= M <= 10, msg + + # Treat each caption independently in the softmax (which runs over modalities) + text = text.reshape([B * K, D]) + + moe_weights = self.moe_fc(text) # BK x D -> BK x M + moe_weights = F.softmax(moe_weights, axis=1) + moe_weights = moe_weights.reshape([B, K, M]) + return moe_weights + + def forward(self, text, experts, ind, raw_captions, vis_vlad, stime): + """Compute joint embeddings and, if requested, a confusion matrix between + video and text representations in the minibatch. + + Notation: B = batch size, M = number of modalities + """ + + # Pass text embeddings through gated units + text_embd = {} + + # Unroll repeated captions into present minibatch + B, captions_per_video, feat_dim = text.shape + text = text.reshape([B * captions_per_video, feat_dim]) + for modality, layer in zip(self.modalities, self.text_GU): + # NOTE: Due to the batch norm, the gated units are sensitive to passing + # in a lot of zeroes, so we do the masking step after the forwards pass + text_ = layer(text) + + # We always assume that text is available for retrieval + text_ = text_.reshape([B, captions_per_video, -1]) + text_embd[modality] = text_ + text = text.reshape([B, captions_per_video, -1]) + + # vladded nans are handled earlier (during pooling) + # We also avoid zeroing random features, since this will leak information + # exclude = list(self.vlad_feat_sizes.keys()) + list(self.random_feats) + # experts = self.mask_missing_embeddings(experts, ind, exclude=exclude) + + # MOE weights computation + normalization - note that we use the first caption + # sample to predict the weights + moe_weights = self.compute_moe_weights(text, ind=ind) + text_local = text.reshape([B * captions_per_video, -1]) + + vis_local = {} + for modality in self.modalities: + vis_local[modality] = experts[modality] + + all_vis_feat = [] + if hasattr(self, "video_dim_reduce"): + # Embed all features to a common dimension + for modality, layer in zip(self.modalities, self.video_dim_reduce): + all_vis_feat.append(layer(vis_local[modality])) + all_vis_feat = paddle.concat(all_vis_feat, axis=1) + + if self.vis_transformer: + experts_tensor = all_vis_feat + experts_tensor = experts_tensor.transpose([1, 0, 2]) + att_out = self.transformers(experts_tensor, mask=None, pos=None) + all_vis_feat = att_out.transpose([1, 0, 2]) + + vis_local, _, save_ass = vis_vlad(all_vis_feat, freeze=True) + cross_view_conf_matrix_tv = paddle.matmul(text_local, vis_local.t()) + + for modality in self.modalities: + experts[modality] = experts[modality].max(axis=1) + + for modality, layer in zip(self.modalities, self.video_GU): + experts[modality] = layer(experts[modality]) + + cross_view_conf_matrix = sharded_cross_view_inner_product( + ind=ind, + vid_embds=experts, + text_embds=text_embd, + text_weights=moe_weights, + subspaces=self.modalities, + raw_captions=raw_captions, + ) + cross_view_conf_matrix = 0.5 * cross_view_conf_matrix + 0.5 * cross_view_conf_matrix_tv + return { + "modalities": self.modalities, + "cross_view_conf_matrix": cross_view_conf_matrix, + } + + +class GatedEmbeddingUnit(nn.Layer): + def __init__(self, input_dimension, output_dimension, use_bn): + super(GatedEmbeddingUnit, self).__init__() + self.fc = nn.Linear(input_dimension, output_dimension) + self.cg = ContextGating(output_dimension, add_batch_norm=use_bn) + + def forward(self, x): + x = self.fc(x) + x = self.cg(x) + x = F.normalize(x) + return x + + +class ReduceDim(nn.Layer): + def __init__(self, input_dimension, output_dimension): + super(ReduceDim, self).__init__() + self.fc = nn.Linear(input_dimension, output_dimension) + + def forward(self, x): + x = self.fc(x) + x = F.normalize(x, axis=-1) + return x + + +class ContextGating(nn.Layer): + def __init__(self, dimension, add_batch_norm=True): + super(ContextGating, self).__init__() + self.fc = nn.Linear(dimension, dimension) + self.add_batch_norm = add_batch_norm + self.batch_norm = nn.BatchNorm1D(dimension) + + def forward(self, x): + x1 = self.fc(x) + if self.add_batch_norm: + x1 = self.batch_norm(x1) + x = paddle.concat([x, x1], axis=1) + return F.glu(x, axis=1) + + +def sharded_cross_view_inner_product(vid_embds, + text_embds, + text_weights, + subspaces, + ind, + tol=1E-5, + raw_captions=None): + """Compute a similarity matrix from sharded vectors. + + Args: + embds1 (dict[str:paddle.Tensor]): the set of sub-embeddings that, when + concatenated, form the whole. The ith shard has shape `B x K x F_i` + (i.e. they can differ in the last dimension). + embds2 (dict[str:paddle.Tensor]): same format. + weights2 (paddle.Tensor): weights for the shards in `embds2`. + + Returns: + (paddle.tensor): similarity matrix of size `BK x BK`. + + NOTE: If multiple captions are provided, we can aggregate their similarities to + provide a single video-text similarity score. + """ + B = vid_embds[subspaces[0]].shape[0] + T, num_caps, _ = text_embds[subspaces[0]].shape + + # unroll separate captions onto first dimension and treat them separately + sims = paddle.zeros([T * num_caps, B]) + text_weights = text_weights.reshape([T * num_caps, -1]) + if True: + mus = [round(x, 3) for x in text_weights.mean(0).numpy().tolist()] + stds = [round(x, 3) for x in text_weights.std(0).numpy().tolist()] + summary = ">>>" + for mod, mu, std in zip(subspaces, mus, stds): + summary += f"{mod}: {mu} +/- {std} " + + # mark expert availabilities along the second axis + available = paddle.ones([1, B, len(subspaces)], dtype=text_weights.dtype) + for ii, modality in enumerate(subspaces): + ind[modality] = paddle.to_tensor(ind[modality], dtype='float32') + available[:, :, ii] = ind[modality] + msg = "expected `available` modality mask to only contain 0s or 1s" + assert set(paddle.unique(available).cpu().numpy()).issubset(set([0, + 1])), msg + # set the text weights along the first axis and combine with availabilities to + # produce a tensor + text_weight_tensor = text_weights.reshape([T * num_caps, 1, + len(subspaces)]) * available + # normalise to account for missing experts + normalising_weights = text_weight_tensor.sum(2).reshape( + [T * num_caps, B, 1]) + text_weight_tensor = paddle.divide(text_weight_tensor, normalising_weights) + + l2_mass_text, l2_mass_vid = 1, 1 + + for idx, modality in enumerate(subspaces): + vid_embd_ = vid_embds[modality].reshape([B, -1]) / l2_mass_vid + text_embd_ = text_embds[modality].reshape([T * num_caps, -1]) + msg = "expected weights to be applied to text embeddings" + assert text_embd_.shape[0] == text_weights.shape[0], msg + text_embd_ = text_embd_ / l2_mass_text + weighting = text_weight_tensor[:, :, idx] + sims += weighting * paddle.matmul(text_embd_, + vid_embd_.t()) # (T x num_caps) x (B) + + if paddle.isnan(sims).sum().item(): + raise ValueError("Found nans in similarity matrix!") + + return sims diff --git a/applications/T2VLAD/model/net_vlad.py b/applications/T2VLAD/model/net_vlad.py new file mode 100644 index 0000000000000000000000000000000000000000..99ef7a1935847ce506180fe31faa71c5361cd79d --- /dev/null +++ b/applications/T2VLAD/model/net_vlad.py @@ -0,0 +1,100 @@ +"""NetVLAD implementation. +""" +# Copyright 2021 Antoine Miech All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import paddle +import numpy as np +import paddle.nn as nn +import paddle.nn.functional as F + + +class NetVLAD(nn.Layer): + def __init__(self, cluster_size, feature_size, ghost_clusters=0, + add_batch_norm=True): + super().__init__() + + self.feature_size = feature_size + self.cluster_size = cluster_size + self.ghost_clusters = ghost_clusters + + init_sc = (1 / math.sqrt(feature_size)) + init_sc = paddle.to_tensor(init_sc) + clusters = cluster_size + ghost_clusters + + # The `clusters` weights are the `(w,b)` in the paper + self.clusters = paddle.create_parameter([feature_size, clusters], dtype='float32', default_initializer=nn.initializer.Assign(paddle.randn([feature_size, clusters]) * init_sc)) + self.batch_norm1 = nn.BatchNorm1D(clusters) if add_batch_norm else None + self.batch_norm2 = nn.BatchNorm1D(clusters) if add_batch_norm else None + # The `clusters2` weights are the visual words `c_k` in the paper + self.clusters1 = paddle.create_parameter([1, feature_size, cluster_size], dtype='float32', default_initializer=nn.initializer.Assign(paddle.randn([1, feature_size, cluster_size]) * init_sc)) + self.clusters2 = paddle.create_parameter([1, feature_size, cluster_size], dtype='float32', default_initializer=nn.initializer.Assign(paddle.randn([1, feature_size, cluster_size]) * init_sc)) + self.out_dim = self.cluster_size * feature_size + + def sanity_checks(self, x): + """Catch any nans in the inputs/clusters""" + if paddle.isnan(paddle.sum(x)): + raise ValueError("nan inputs") + if paddle.isnan(self.clusters[0][0]): + raise ValueError("nan clusters") + + def forward(self, x, freeze=False, mask=None): + """Aggregates feature maps into a fixed size representation. In the following + notation, B = batch_size, N = num_features, K = num_clusters, D = feature_size. + + Args: + x (th.Tensor): B x N x D + + Returns: + (th.Tensor): B x DK + """ + self.sanity_checks(x) + max_sample = x.shape[1] + x = x.reshape([-1, self.feature_size]) # B x N x D -> BN x D + + if freeze == True: + clusters = self.clusters.detach() + clusters2 = self.clusters1 + batch_norm = self.batch_norm1 + else: + clusters = self.clusters + clusters2 = self.clusters2 + batch_norm = self.batch_norm2 + + assignment = paddle.matmul(x, clusters) # (BN x D) x (D x (K+G)) -> BN x (K+G) + if batch_norm: + assignment = batch_norm(assignment) + + assignment = F.softmax(assignment, axis=1) # BN x (K+G) -> BN x (K+G) + save_ass = assignment.reshape([-1, max_sample, self.cluster_size+1]) + + assignment = assignment[:, :self.cluster_size] + assignment = assignment.reshape([-1, max_sample, self.cluster_size]) # -> B x N x K + a_sum = paddle.sum(assignment, axis=1, keepdim=True) # B x N x K -> B x 1 x K + a = a_sum * self.clusters2 + assignment = assignment.transpose([0, 2, 1]) # B x N x K -> B x K x N + + x = x.reshape([-1, max_sample, self.feature_size]) # BN x D -> B x N x D + vlad = paddle.matmul(assignment, x) # (B x K x N) x (B x N x D) -> B x K x D + vlad = vlad.transpose([0, 2, 1]) # -> B x D x K + vlad = vlad - a + + # L2 intra norm + vlad_ = F.normalize(vlad) + + # flattening + L2 norm + vlad = vlad_.reshape([-1, self.cluster_size * self.feature_size]) # -> B x DK + vlad = F.normalize(vlad) + return vlad, vlad_, save_ass # B x DK \ No newline at end of file diff --git a/applications/T2VLAD/model/text.py b/applications/T2VLAD/model/text.py new file mode 100644 index 0000000000000000000000000000000000000000..fbf32ab1cf70bfce331bcf506e54f18bd52d98f5 --- /dev/null +++ b/applications/T2VLAD/model/text.py @@ -0,0 +1,148 @@ +"""This module defines the TextEmbedding interface for converting video descriptions and +queries into embeddings. +""" +import zipfile +import functools +from abc import abstractmethod +from pathlib import Path + +import numpy as np +import paddle +import gensim +import requests +import transformers +from typeguard import typechecked +from zsvision.zs_utils import BlockTimer + +from model.s3dg import S3D + +class TextEmbedding: + def __init__(self, model, dim: int): + self.model = model + self.dim = dim + #self.device = None + + @abstractmethod + def text2vec(self, text: str) -> np.ndarray: + """Convert a string of text into an embedding. + + Args: + text: the content to be embedded + + Returns: + (d x n) array, where d is the dimensionality of the embedding and `n` is the + number of words that were successfully parsed from the text string. + + NOTE: For some text embedding models (such as word2vec), not all words are + converted to vectors (e.g. certain kinds of stop words) - these are dropped from + the output. + """ + raise NotImplementedError + + #@typechecked + #def set_device(self, device: torch.device): + # self.model = self.model.to(device) + # self.device = device + + +@functools.lru_cache(maxsize=64, typed=False) +def load_w2v_model_from_cache( + w2v_weights: Path, +) -> gensim.models.keyedvectors.Word2VecKeyedVectors: + with BlockTimer("Loading w2v from disk"): + model = gensim.models.KeyedVectors.load_word2vec_format( + fname=w2v_weights, + binary=True, + ) + return model + + +@typechecked +def fetch_model(url: str, weights_path: Path): + weights_path.parent.mkdir(exist_ok=True, parents=True) + with BlockTimer(f"Fetching weights {url} -> {weights_path}"): + resp = requests.get(url, verify=False) + with open(weights_path, "wb") as f: + f.write(resp.content) + + +class W2VEmbedding(TextEmbedding): + """This model embeds text using the google-released implementation of the word2vec + model introduced in: + + Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., & Dean, J. (2013). + Distributed representations of words and phrases and their compositionality. + In Advances in neural information processing systems (pp. 3111-3119). + + For words that are present in the w2v vocabulary, a 300-dimensional embedding is + produced via a lookup table. + """ + @typechecked + def __init__( + self, + dim: int, + mirror: str, + weights_path: Path, + fetch_weights: bool = True, + ): + if not weights_path.exists(): + if fetch_weights: + fetch_model(url=mirror, weights_path=weights_path) + else: + raise ValueError(f"w2v weights missing at {weights_path}") + + model = load_w2v_model_from_cache(weights_path) + super().__init__(model=model, dim=dim) + + @typechecked + def text2vec(self, text: str) -> np.ndarray: + # convert the text string to tokens that can be processed by w2v. We handle + # 'a' as a special case. + tokens = [x for x in text.split(" ") if x != "a" and x in self.model.vocab] + + embeddings = [] + for token in tokens: + embeddings.append(self.model.get_vector(token)) + embeddings = np.array(embeddings) + # For empty sequences, we use zeros with the dimensionality of the features on + # the second dimension (this is the format expected by the CE codebase) + if embeddings.size == 0: + embeddings = np.zeros((0, self.dim)) + return embeddings + + #@typechecked + #def set_device(self, device: torch.device): + # msg = f"w2v only supports CPU-based execution found {device.type}" + # assert device.type == "cpu", msg + + +class OpenAI_GPT(TextEmbedding): + """This model produces 768-embeddings using a pretrained GPT model, introduced + in the paper: + + Radford, A., Narasimhan, K., Salimans, T., & Sutskever, I. (2018). + Improving language understanding by generative pre-training, + https://cdn.openai.com/research-covers/language-unsupervised/language_understanding + _paper.pdf + """ + + def __init__(self): + self.tokenizer = transformers.OpenAIGPTTokenizer.from_pretrained("openai-gpt") + model = transformers.OpenAIGPTModel.from_pretrained("openai-gpt") + model.eval() + super().__init__(model=model) + + @typechecked + def text2vec(self, text: str) -> np.ndarray: + tokenized_text = self.tokenizer.tokenize(text) + + # Convert token to vocabulary indices + indexed_tokens = self.tokenizer.convert_tokens_to_ids(tokenized_text) + tokens_tensor = paddle.to_tensor(indexed_tokens, dtype='int64') #tokens_tensor = torch.LongTensor([indexed_tokens]).to(self.model.device) + + with paddle.no_grad(): + hidden_states = self.model(tokens_tensor) + embeddings = hidden_states[0].numpy() + return embeddings.squeeze(0) + + diff --git a/applications/T2VLAD/parse_config.py b/applications/T2VLAD/parse_config.py new file mode 100644 index 0000000000000000000000000000000000000000..c952b9d61674315ce4c7c25d51bbc27ee6f7927d --- /dev/null +++ b/applications/T2VLAD/parse_config.py @@ -0,0 +1,239 @@ +# Copyright 2021 Antoine Miech All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +import paddle +import pprint +import logging +from typing import Dict +from pathlib import Path +from datetime import datetime +from operator import getitem +from functools import reduce + +from mergedeep import Strategy, merge +from zsvision.zs_utils import set_nested_key_val +from typeguard import typechecked + +from utils import read_json, write_json +from logger import setup_logging + + +class ConfigParser: + def __init__(self, args, options='', timestamp=True, slave_mode=False): + # slave_mode - when calling the config parser form an existing process, we + # avoid reinitialising the logger and ignore sys.argv when argparsing. + + # parse default and custom cli options + for opt in options: + args.add_argument(*opt.flags, default=None, type=opt.type) + + if slave_mode: + args = args.parse_args(args=[]) + else: + args = args.parse_args() + + if args.resume and not slave_mode: + self.resume = Path(args.resume) + else: + msg_no_cfg = "Config file must be specified" + assert args.config is not None, msg_no_cfg + self.resume = None + self.cfg_fname = Path(args.config) + + config = self.load_config(self.cfg_fname) + self._config = _update_config(config, options, args) + + if self._config.get("eval_config", False): + # validate path to evaluation file + eval_cfg_path = self._config.get("eval_config") + msg = f"eval_config was specified, but `{eval_cfg_path}` does not exist" + assert Path(self._config.get("eval_config")).exists(), msg + + # set save_dir where trained model and log will be saved. + if "tester" in self.config: + save_dir = Path(self.config['tester']['save_dir']) + else: + save_dir = Path(self.config['trainer']['save_dir']) + timestamp = datetime.now().strftime(r"%Y-%m-%d_%H-%M-%S") if timestamp else "" + + if slave_mode: + timestamp = f"{timestamp}-eval-worker" + + exper_name = self.set_exper_name(args, config=config) + + if getattr(args, "group_id", False): + subdir = Path(args.group_id) / f"seed-{args.group_seed}" / timestamp + else: + subdir = timestamp + + self._save_dir = save_dir / 'models' / exper_name / subdir + self._log_dir = save_dir / 'log' / exper_name / subdir + self._exper_name = exper_name + self._args = args + + # if set, remove all previous experiments with the current config + if vars(args).get("purge_exp_dir", False): + for dirpath in (self._save_dir, self._log_dir): + config_dir = dirpath.parent + existing = list(config_dir.glob("*")) + print(f"purging {len(existing)} directories from config_dir...") + tic = time.time() + os.system(f"rm -rf {config_dir}") + print(f"Finished purge in {time.time() - tic:.3f}s") + + self.save_dir.mkdir(parents=True, exist_ok=True) + self.log_dir.mkdir(parents=True, exist_ok=True) + + # save updated config file to the checkpoint dir + write_json(self.config, self.save_dir / 'config.json') + + # configure logging module + if not slave_mode: + self.log_path = setup_logging(self.log_dir) + + self.log_levels = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG} + + def set_exper_name(self, args, config): + # We assume that the config files are organised into directories such that + # each directory has the name of the dataset. + dataset_name = self.cfg_fname.parent.stem + exper_name = f"{dataset_name}-{self.cfg_fname.stem}" + if args.custom_args: + key_val_lists = args.custom_args.split("+") + for key_val_pair in key_val_lists: + print(f"parsing key-val pair : {key_val_pair}") + key, val = key_val_pair.split("@") + set_nested_key_val(key, val, self._config) + # remove periods from key names + key_ = key.replace("_.", "--") + # remove commas from value names + val = val.replace(",", "--") + custom_tag = "-".join(key_.split(".")[-2:]) + exper_name = f"{exper_name}-{custom_tag}-{val}" + + if getattr(args, "disable_workers", False): + print("Disabling data loader workers....") + config["data_loader"]["args"]["num_workers"] = 0 + + if getattr(args, "train_single_epoch", False): + print("Restricting training to a single epoch....") + config["trainer"]["epochs"] = 1 + config["trainer"]["save_period"] = 1 + config["trainer"]["skip_first_n_saves"] = 0 + exper_name = f"{exper_name}-train-single-epoch" + return exper_name + + @staticmethod + @typechecked + def load_config(cfg_fname: Path) -> Dict: + config = read_json(cfg_fname) + # apply inheritance through config hierarchy + descendant, ancestors = config, [] + while "inherit_from" in descendant: + parent_config = read_json(Path(descendant["inherit_from"])) + ancestors.append(parent_config) + descendant = parent_config + for ancestor in ancestors: + merge(ancestor, config, strategy=Strategy.REPLACE) + config = ancestor + return config + + def init(self, name, module, *args, **kwargs): + """Finds a function handle with the name given as 'type' in config, and returns + the instance initialized with corresponding keyword args given as 'args'. + """ + module_name = self[name]['type'] + module_args = dict(self[name]['args']) + msg = (f"Fail for {module_name}\n" + f"overwriting kwargs given in config file is not allowed\n" + f"passed kwargs: {kwargs}\n" + f"for module_args: {module_args})") + assert all([k not in module_args for k in kwargs]), msg + module_args.update(kwargs) + return getattr(module, module_name)(*args, **module_args) + + def __getitem__(self, name): + return self.config[name] + + def __len__(self): + # NOTE: This is used for boolean checking deep inside ray.tune, so we required it + # to be defined. + return len(self.config) + + def __setitem__(self, name, value): + self.config[name] = value + + def __contains__(self, name): + return name in self.config + + def get(self, name, default): + return self.config.get(name, default) + + def keys(self): + return self.config.keys() + + def get_logger(self, name, verbosity=2): + msg_verbosity = "verbosity option {} is invalid. Valid options are {}." + msg_verbosity = msg_verbosity.format(verbosity, self.log_levels.keys()) + assert verbosity in self.log_levels, msg_verbosity + logger = logging.getLogger(name) + logger.setLevel(self.log_levels[verbosity]) + return logger + + # setting read-only attributes + @property + def config(self): + return self._config + + @property + def save_dir(self): + return self._save_dir + + @property + def log_dir(self): + return self._log_dir + + def __repr__(self): + return pprint.PrettyPrinter().pformat(self.__dict__) + + def items(self): + return self._config.items() + + +# helper functions used to update config dict with custom cli options +def _update_config(config, options, args): + for opt in options: + value = getattr(args, _get_opt_name(opt.flags)) + if value is not None: + _set_by_path(config, opt.target, value) + return config + + +def _get_opt_name(flags): + for flg in flags: + if flg.startswith('--'): + return flg.replace('--', '') + return flags[0].replace('--', '') + + +def _set_by_path(tree, keys, value): + """Set a value in a nested object in tree by sequence of keys.""" + _get_by_path(tree, keys[:-1])[keys[-1]] = value + + +def _get_by_path(tree, keys): + """Access a nested object in tree by sequence of keys.""" + return reduce(getitem, keys, tree) diff --git a/applications/T2VLAD/run_cap.pkl b/applications/T2VLAD/run_cap.pkl new file mode 100644 index 0000000000000000000000000000000000000000..96afad9afa74def0f009e96b3fdae021226375d3 Binary files /dev/null and b/applications/T2VLAD/run_cap.pkl differ diff --git a/applications/T2VLAD/test.py b/applications/T2VLAD/test.py new file mode 100644 index 0000000000000000000000000000000000000000..d1ce9e4546835518e2181474939bf739866836d6 --- /dev/null +++ b/applications/T2VLAD/test.py @@ -0,0 +1,206 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import copy +import random +import paddle +import logging +import argparse + +import numpy as np +import model.model as module_arch +import model.metric as module_metric +import data_loader.data_loaders as module_data + +from typing import Tuple +from pathlib import Path +from typeguard import typechecked +from mergedeep import Strategy, merge +from parse_config import ConfigParser +from trainer.trainer import verbose, ctxt_mgr +from utils.util import compute_dims, compute_trn_config + +@typechecked +def compress_predictions(query_masks: np.ndarray, sims: np.ndarray, topk: int = 10): + """We store the indices of the top-k predictions, rather than the full similarity + matrix, to reduce storage requirements. + + NOTE: The similarity matrix contains `num_queries x num_videos` elements, where + `num_queries = num_videos x max_num_queries_per_video`. We first mask out + locations in the similarity matrix that correspond to invalid queries (these are + produced by videos with fewer than `max_num_queries_per_video` descriptions). + """ + + # validate the input shapes + assert query_masks.ndim == 2, "Expected query_masks to be a matrix" + query_num_videos, query_max_per_video = query_masks.shape + sims_queries, sims_num_videos = sims.shape + msg = (f"Expected sims and query masks to represent the same number of videos " + f"(found {sims_num_videos} v {query_num_videos}") + assert query_num_videos == sims_num_videos, msg + msg = (f"Expected sims and query masks to represent the same number of queries " + f"(found {sims_queries} v {query_num_videos * query_max_per_video}") + assert query_max_per_video * query_num_videos == sims_queries, msg + + valid_sims = sims[query_masks.flatten().astype(np.bool)] + ranks = np.argsort(-valid_sims, axis=1) + return ranks[:, :topk] + + +@typechecked +def get_model_and_data_loaders( + config: ConfigParser, + logger: logging.Logger, + model_path: Path, +) -> Tuple[paddle.nn.Layer, module_data.ExpertDataLoader]: + expert_dims, raw_input_dims = compute_dims(config) + trn_config = compute_trn_config(config) + + data_loaders = config.init( + name='data_loader', + module=module_data, + logger=logger, + raw_input_dims=raw_input_dims, + text_feat=config["experts"]["text_feat"], + text_dim=config["experts"]["text_dim"], + text_agg=config["experts"]["text_agg"], + use_zeros_for_missing=config["experts"].get("use_zeros_for_missing", False), + eval_only=True, + ) + + model = config.init( + name='arch', + module=module_arch, + expert_dims=expert_dims, + text_dim=config["experts"]["text_dim"], + ce_shared_dim=config["experts"].get("ce_shared_dim", None), + feat_aggregation=config["data_loader"]["args"]["feat_aggregation"], + ) + model_path = config._args.resume + logger.info(f"Loading checkpoint: {model_path} ...") + checkpoint = paddle.load(model_path) + state_dict = checkpoint + if config['n_gpu'] > 1: + model = paddle.DataParallel(model) + model.load_dict(state_dict) + + return model, data_loaders + + +def evaluation(config, logger=None, trainer=None): + + if logger is None: + logger = config.get_logger('test') + + if getattr(config._args, "eval_from_training_config", False): + eval_conf = copy.deepcopy(config) + merge(eval_conf._config, config["eval_settings"], strategy=Strategy.REPLACE) + config = eval_conf + + logger.info("Running evaluation with configuration:") + logger.info(config) + + # Set the random initial seeds + seed = config["seed"] + logger.info(f"Setting experiment random seed to {seed}") + random.seed(seed) + np.random.seed(seed) + paddle.seed(seed) + + model, data_loaders = get_model_and_data_loaders( + config=config, + logger=logger, + model_path=Path(config._args.resume), + ) + logger.info(model) + + metrics = [getattr(module_metric, met) for met in config['metrics']] + + # prepare model for testing. Note that some datasets fail to fit the retrieval + # set on the GPU, so we run them on the CPU + model.eval() + + with paddle.no_grad(): + samples, meta = data_loaders["retrieval"] + #import pdb; pdb.set_trace() + # To use the nan-checks safely, we need make temporary copies of the data + all_text_num = samples['text'].shape[0] + text_keys = ['text', 'cap_id', 'att_mask', 'text_token_mask'] + chk = 100 + tck = 100 + + if samples['text'].shape[0] % chk == 0: + vid_batch = samples['text'].shape[0] // chk + else: + vid_batch = samples['text'].shape[0] // chk + 1 + if samples['text'].shape[0] % tck == 0: + text_batch = samples['text'].shape[0] // tck + else: + text_batch = samples['text'].shape[0] // tck + 1 + sub_sims = [] + for idx in range(text_batch): + if idx % 5 == 0: + print(idx,'/',text_batch) + sub_samples = {} + for key in text_keys: + sub_samples.update({key: samples[key][idx*tck:idx*tck+tck]}) + subsub_sims = [] + for vid in range(vid_batch): + sub_samples['experts'] = {} + sub_samples['ind'] = {} + for expert in samples['experts'].keys(): + sub_samples['experts'][expert] = samples['experts'][expert][vid*chk:vid*chk+chk] + sub_samples['ind'][expert] = samples['ind'][expert][vid*chk:vid*chk+chk] + with ctxt_mgr(sub_samples) as valid: + output = model(**valid) + subsub_sims.append(output["cross_view_conf_matrix"].cpu()) + subsub_sims = paddle.concat(subsub_sims, axis=1) + sub_sims.append(subsub_sims) + sub_sims = paddle.concat(sub_sims, axis=0) + sims = paddle.to_tensor(sub_sims, dtype='float32').numpy() + dataset = data_loaders.dataset_name + + nested_metrics = {} + for metric in metrics: + metric_name = metric.__name__ + res = metric(sims, query_masks=meta["query_masks"]) + verbose(epoch=0, metrics=res, name=dataset, mode=metric_name) + if trainer is not None: + if not trainer.mini_train: + trainer.writer.set_step(step=0, mode="val") + # avoid tensboard folding by prefixing + metric_name_ = f"test_{metric_name}" + trainer.log_metrics(res, metric_name=metric_name_, mode="val") + nested_metrics[metric_name] = res + + log = {} + for subkey, subval in nested_metrics.items(): + for subsubkey, subsubval in subval.items(): + log[f"test_{subkey}_{subsubkey}"] = subsubval + for key, value in log.items(): + logger.info(" {:15s}: {}".format(str(key), value)) + + +if __name__ == '__main__': + args = argparse.ArgumentParser(description='PyTorch Template') + args.add_argument('--config', default=None, type=str, help="config file path") + args.add_argument('--resume', default=None, help='path to checkpoint for evaluation') + args.add_argument('--eval_from_training_config', action="store_true", + help="if true, evaluate directly from a training config file.") + args.add_argument("--custom_args", help="qualified key,val pairs") + eval_config = ConfigParser(args) + + cfg_msg = "For evaluation, a model checkpoint must be specified via the --resume flag" + assert eval_config._args.resume, cfg_msg + if eval_config._config.get("eval_settings", False): + merge(eval_config._config, eval_config["eval_settings"], strategy=Strategy.REPLACE) + evaluation(eval_config) diff --git a/applications/T2VLAD/train.py b/applications/T2VLAD/train.py new file mode 100644 index 0000000000000000000000000000000000000000..9e093b221d4bb8e715ccd0d9232af2840bcaecc0 --- /dev/null +++ b/applications/T2VLAD/train.py @@ -0,0 +1,151 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +import time +import copy +import socket +import paddle +import argparse +import warnings + +import numpy as np +import model.loss as module_loss +import model.model as module_arch +import model.metric as module_metric +import data_loader.data_loaders as module_data + + +from pathlib import Path +from utils import set_seeds +from trainer import Trainer +from test import evaluation +from mergedeep import merge, Strategy +from parse_config import ConfigParser +from logger.log_parser import log_summary +from utils import compute_dims, compute_trn_config + +def run_exp(config): + warnings.filterwarnings('ignore') + logger = config.get_logger('train') + + expert_dims, raw_input_dims = compute_dims(config, logger) + trn_config = compute_trn_config(config) + + if config._args.group_seed: + seeds = [int(config._args.group_seed)] + else: + seeds = [int(x) for x in config._args.seeds.split(",")] + + for ii, seed in enumerate(seeds): + tic = time.time() + logger.info(f"{ii + 1}/{len(seeds)} Setting experiment random seed to {seed}") + set_seeds(seed) + config["seed"] = seed + + model = config.init( + name='arch', + module=module_arch, + expert_dims=expert_dims, + text_dim=config["experts"]["text_dim"], + ce_shared_dim=config["experts"].get("ce_shared_dim", None), + feat_aggregation=config["data_loader"]["args"]["feat_aggregation"], + ) + logger.info(model) + + data_loaders = config.init( + name='data_loader', + module=module_data, + logger=logger, + raw_input_dims=raw_input_dims, + text_feat=config["experts"]["text_feat"], + text_dim=config["experts"]["text_dim"], + text_agg=config["experts"]["text_agg"], + use_zeros_for_missing=config["experts"].get("use_zeros_for_missing", False), + eval_only=False, + ) + + loss = config.init(name="loss", module=module_loss) + metrics = [getattr(module_metric, met) for met in config['metrics']] + + lr_scheduler = paddle.optimizer.lr.StepDecay(learning_rate=0.0001, step_size=5, gamma=0.9) + optimizer = paddle.optimizer.AdamW(learning_rate=lr_scheduler, weight_decay=1e-4, parameters=model.parameters(), grad_clip=paddle.nn.ClipGradByGlobalNorm(2)) + + trainer = Trainer( + model, + loss, + metrics, + optimizer, + config=config, + data_loaders=data_loaders, + lr_scheduler=lr_scheduler, + mini_train=config._args.mini_train, + visualizer=None, + val_freq=config["trainer"].get("val_freq", 1), + force_cpu_val=config.get("force_cpu_val", False), + skip_first_n_saves=config["trainer"].get("skip_first_n_saves", 0), + include_optim_in_save_model=config["trainer"].get("include_optim_in_save_model", 1), + cache_targets=set(config.get("cache_targets", [])), + ) + trainer.train() + best_model_path = config.save_dir / "trained_model.pdparams" + duration = time.strftime('%Hh%Mm%Ss', time.gmtime(time.time() - tic)) + logger.info(f"Training took {duration}") + + # If multiple runs were conducted, report relevant statistics + if len(seeds) > 1: + log_summary( + logger=logger, + log_path=config.log_path, + eval_mode=config["eval_mode"], + fixed_num_epochs=config["trainer"]["epochs"], + ) + print(f"Log file stored at {config.log_path}") + + # Report the location of the "best" model of the final seeded run (here + # "best" corresponds to the model with the highest geometric mean over the + # R@1, R@5 and R@10 metrics when a validation set is used, or simply the final + # epoch of training for fixed-length schedules). + print(f"The best performing model can be found at {str(best_model_path)}") + + +def main(): + args = argparse.ArgumentParser(description='Main entry point for training') + args.add_argument('--config', help='config file path') + args.add_argument('--resume', help='path to latest model (default: None)') + args.add_argument('--mini_train', action="store_true") + args.add_argument('--group_id', help="if supplied, group these experiments") + args.add_argument('--disable_workers', action="store_true") + args.add_argument('--refresh_lru_cache', action="store_true") + args.add_argument('--train_single_epoch', action="store_true") + args.add_argument('--purge_exp_dir', action="store_true", + help="remove all previous experiments with the given config") + args.add_argument("--dbg", default="ipdb.set_trace") + args.add_argument("--custom_args", help="qualified key,val pairs") + + # Seeds can either be passed directly as a comma separated list at the command line, + # or individually for separate experiments as a group (used for slurm experiments) + seed_args = args.add_mutually_exclusive_group() + seed_args.add_argument('--seeds', default="0", help="comma separated list of seeds") + seed_args.add_argument('--group_seed', help="seed for group member") + args = ConfigParser(args) + os.environ["PYTHONBREAKPOINT"] = args._args.dbg + args["data_loader"]["args"]["refresh_lru_cache"] = args._args.refresh_lru_cache + msg = (f"Expected the number of training epochs ({args['trainer']['epochs']})" + f"to exceed the save period ({args['trainer']['save_period']}), otherwise" + " no checkpoints will be saved.") + assert args["trainer"]["epochs"] >= args["trainer"]["save_period"], msg + run_exp(config=args) + + +if __name__ == '__main__': + main() diff --git a/applications/T2VLAD/trainer/__init__.py b/applications/T2VLAD/trainer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5c0a8a4a91724515aee0aecd8217cfe16ee5ec80 --- /dev/null +++ b/applications/T2VLAD/trainer/__init__.py @@ -0,0 +1 @@ +from .trainer import * diff --git a/applications/T2VLAD/trainer/trainer.py b/applications/T2VLAD/trainer/trainer.py new file mode 100644 index 0000000000000000000000000000000000000000..55f1d5011dcce490c0d00c187f2d8bb19f6bef13 --- /dev/null +++ b/applications/T2VLAD/trainer/trainer.py @@ -0,0 +1,280 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import paddle +import numpy as np + +from base import BaseTrainer +from utils import memory_summary +from contextlib import contextmanager + + +def verbose(epoch, metrics, mode, name="TEST"): + r1, r5, r10, r50 = metrics["R1"], metrics["R5"], metrics["R10"], metrics["R50"] + msg = f"[{mode}]{name:s} epoch {epoch}, R@1: {r1:.1f}" + msg += f", R@5: {r5:.1f}, R@10 {r10:.1f}, R@50 {r50:.1f}" + msg += f"MedR: {metrics['MedR']:g}, MeanR: {metrics['MeanR']:.1f}" + print(msg) + + +@contextmanager +def ctxt_mgr(samples): + """Provide a context for managing temporary, cloned copies of retrieval + sample tensors. + + The rationale here is that to use nan-checking in the model (to validate the + positions of missing experts), we need to modify the underlying tensors. This + function lets the evaluation code run (and modify) temporary copies, without + modifying the originals. + """ + + exp_dict = samples["experts"].items() + experts = {key: val.clone() for key, val in exp_dict} + samples_ = { + "experts": experts, + "ind": samples["ind"], + "text": samples["text"], + "cap_id": samples["cap_id"], + "att_mask": samples["att_mask"], + } + if "text_token_mask" in samples: + samples_["text_token_mask"] = samples["text_token_mask"] + try: + yield samples_ + finally: + del samples_ + + +class Trainer(BaseTrainer): + """ + Trainer class + + Note: + Inherited from BaseTrainer. + """ + def __init__(self, model, loss, metrics, optimizer, config, data_loaders, + lr_scheduler, visualizer, skip_first_n_saves, + include_optim_in_save_model, force_cpu_val, cache_targets=set(), + num_keep_ckpts=3, mini_train=False, val_freq=1, skip_tboard=False): + super().__init__(model, loss, metrics, optimizer, config, mini_train=mini_train, + skip_tboard=skip_tboard, num_keep_ckpts=num_keep_ckpts) + self.config = config + self.cache_targets = cache_targets + self.data_loaders = data_loaders + self.lr_scheduler = lr_scheduler + self.mini_train = mini_train + self.len_epoch = len(self.data_loaders["train"]) + self.log_step = int(np.sqrt(data_loaders["train"].batch_size)) + self.visualizer = visualizer + self.force_cpu_val = force_cpu_val + self.val_freq = val_freq + self.skip_first_n_saves = skip_first_n_saves + self.include_optim_in_save_model = include_optim_in_save_model + self.seen = {"train": 0, "val": 0} + + def _train_epoch(self, epoch): + """ + Training logic for an epoch + + :param epoch: Current training epoch. + :return: A log that contains all information you want to save. + + Note: + If you have additional information to record, for example: + > additional_log = {"x": x, "y": y} + merge it with log before return. i.e. + > log = {**log, **additional_log} + > return log + + The metrics in log must have the key 'metrics'. + """ + total_loss = 0 + self.model.train() + memory_summary() + + for batch_idx, minibatch in enumerate(self.data_loaders["train"]): + output = self.model(**minibatch) + if "retrieval" in self.data_loaders.dataloaders: + loss = self.loss(output["cross_view_conf_matrix"]) + else: + loss = self.loss(x=output["class_preds"], target=labels) + + loss.backward() + self.optimizer.step() + self.optimizer.clear_grad() + + sample_key = list(minibatch["experts"].keys())[0] + batch_size = minibatch["experts"][sample_key].shape[0] + self.seen["train"] += batch_size + + total_loss += loss.item() + + if batch_idx % self.log_step == 0: + prog = self._progress(batch_idx) + self.logger.info(f"Train Epoch: {epoch} {prog} Loss: {loss.item():.6f}") + + if batch_idx == self.len_epoch or (self.mini_train and batch_idx > 3): + break + + log = {'loss': total_loss / self.len_epoch} + if epoch % self.val_freq == 0: + nested_log, cached_preds = self._valid_epoch(epoch) + log.update(nested_log) + else: + nested_log, cached_preds = {}, None + self.logger.info(f"skipping val for epoch: {epoch}") + + self.lr_scheduler.step() + + self.logger.info(f"LR {self.lr_scheduler.get_lr()}") + return log, cached_preds + + def _valid_epoch(self, epoch): + """Validate model after an epoch of training and store results to disk. + + Args: + epoch (int): the current epoch + + Returns: + A log that contains information about validation + + NOTE: The validation metrics in log must have the key 'val_metrics'. + """ + self.model.eval() + cached_preds = {key: {"vid_name": [], "preds": [], "labels": []} + for key in self.cache_targets} + + with paddle.no_grad(): + if "retrieval" in self.data_loaders.dataloaders: + samples, meta = self.data_loaders["retrieval"] + sample_key = list(samples["experts"].keys())[0] + batch_size = samples["experts"][sample_key].shape[0] + self.seen["val"] += batch_size + num_queries = samples["text"].shape[0] * samples["text"].shape[1] + safe_queries = 1 + text_keys = ['text', 'cap_id', 'att_mask', 'text_token_mask'] + if num_queries > safe_queries: + chk = 50 + tck = 50 + if samples['text'].shape[0] % chk == 0: + vid_batch = samples['text'].shape[0] // chk + else: + vid_batch = samples['text'].shape[0] // chk + 1 + if samples['text'].shape[0] % tck == 0: + text_batch = samples['text'].shape[0] // tck + else: + text_batch = samples['text'].shape[0] // tck + 1 + + sub_sims = [] + for idx in range(text_batch): + if idx % 5 == 0: + print(idx,'/',text_batch) + sub_samples = {} + for key in text_keys: + sub_samples.update({key: samples[key][idx*tck:idx*tck+tck]}) + subsub_sims = [] + for vid in range(vid_batch): + sub_samples['experts'] = {} + sub_samples['ind'] = {} + for expert in samples['experts'].keys(): + sub_samples['experts'][expert] = samples['experts'][expert][vid*chk:vid*chk+chk] + sub_samples['ind'][expert] = samples['ind'][expert][vid*chk:vid*chk+chk] + with ctxt_mgr(sub_samples) as xx: + output = self.model(**xx) + subsub_sims.append(output["cross_view_conf_matrix"].cpu()) + + subsub_sims = paddle.concat(subsub_sims, axis=1) + sub_sims.append(subsub_sims) + + sims = paddle.concat(sub_sims, axis=0) + sims = paddle.to_tensor(sims, dtype='float32').cpu().numpy() + else: + with ctxt_mgr(samples) as xx: + output = self.model(**xx) + sims = paddle.to_tensor(output["cross_view_conf_matrix"], dtype='float32').cpu().numpy() + + # sample the loss (using only the first query for each video) + queries_per_vid = meta["query_masks"].shape[1] + sims_ = paddle.to_tensor(sims).reshape([-1, queries_per_vid, sims.shape[-1]]) + loss = self.loss(sims_[:, 0, :]) + dataset = self.data_loaders.dataset_name + nested_metrics = {} + for metric in self.metrics: + metric_name = metric.__name__ + res = metric(sims, query_masks=meta["query_masks"]) + if metric_name == "mean_average_precision": + print(f"Epoch: {epoch}, mean AP: {res['mAP']}") + else: + verbose(epoch=epoch, metrics=res, name=dataset, mode=metric_name) + nested_metrics[metric_name] = res + + # TODO(Samuel) disabled visualisation for now, simple to add in later + num_test_caps = self.data_loaders.num_test_captions + if num_test_caps == 1 and meta["raw_captions"] is not None: + if self.visualizer is not None: + self.visualizer.visualize_ranking( + sims=sims, + meta=meta, + epoch=epoch, + nested_metrics=nested_metrics, + ) + return {"nested_val_metrics": nested_metrics}, cached_preds + + elif "val" in self.data_loaders.dataloaders: + metrics = [x() for x in self.metrics] + for batch_idx, minibatch in enumerate(self.data_loaders["val"]): + labels = minibatch.pop("labels") + vid_name = minibatch.pop("vid_name") + output = self.model(**minibatch) + if "val" in self.cache_targets: + cached_preds["val"]["vid_name"].append(vid_name) + cached_preds["val"]["preds"].append(output["class_preds"]) + + for metric in metrics: + metric.add(output=output["class_preds"], target=labels) + if batch_idx % self.log_step == 0: + prog = self._progress(batch_idx) + self.logger.info(f"Val Epoch: {epoch} {prog}") + + nested_metrics = {} + for metric in metrics: + if hasattr(metric, "topk"): + res = {f"top{key}": val for key, val in + zip(metric.topk, metric.value())} + nested_metrics["accuracy"] = res + else: + raise ValueError(f"unsupported mettric: {type(metric)}") + nested = {"nested_val_metrics": nested_metrics} + + for target in self.cache_targets - {"val"}: + for batch_idx, minibatch in enumerate(self.data_loaders["tiny"]): + if "labels" in minibatch: + cached_preds[target]["labels"].append(minibatch.pop("labels")) + cached_preds[target]["vid_name"].append(minibatch.pop("vid_name")) + output = self.model(**minibatch) + cached_preds[target]["preds"].append(output["class_preds"]) + + # aggregate all cached predictions + for target in self.cache_targets: + for key, val in cached_preds[target].items(): + cached_preds[key] = paddle.concat(val).cpu().numpy() + return nested, cached_preds + + def _progress(self, batch_idx): + base = '[{}/{} ({:.0f}%)]' + if hasattr(self.data_loaders, 'n_samples'): + current = batch_idx * self.data_loaders.batch_size + total = self.data_loaders.n_samples + else: + current = batch_idx + total = self.len_epoch + return base.format(current, total, 100.0 * current / total) diff --git a/applications/T2VLAD/utils/__init__.py b/applications/T2VLAD/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..46d3a156a78c6ef994a0ba7e92a334a4a6b16b8b --- /dev/null +++ b/applications/T2VLAD/utils/__init__.py @@ -0,0 +1 @@ +from .util import * diff --git a/applications/T2VLAD/utils/util.py b/applications/T2VLAD/utils/util.py new file mode 100644 index 0000000000000000000000000000000000000000..7a5899577b3ad6ad1239b611409690be9beafe1e --- /dev/null +++ b/applications/T2VLAD/utils/util.py @@ -0,0 +1,327 @@ +""" +Exclude from autoreload +%aimport -util.utils +""" +import os +import json +import random +from pathlib import Path +from datetime import datetime +from typing import List +from itertools import repeat +from collections import OrderedDict + +import numpy as np +import paddle +import psutil +import humanize +from PIL import Image +from typeguard import typechecked + + +@typechecked +def filter_cmd_args(cmd_args: List[str], remove: List[str]) -> List[str]: + drop = [] + for key in remove: + if key not in cmd_args: + continue + pos = cmd_args.index(key) + drop.append(pos) + if len(cmd_args) > (pos + 1) and not cmd_args[pos + 1].startswith("--"): + drop.append(pos + 1) + for pos in reversed(drop): + cmd_args.pop(pos) + return cmd_args + + +@typechecked +def set_seeds(seed: int): + """Set seeds for randomisation libraries. + + Args: + seed: the seed value + """ + random.seed(seed) + np.random.seed(seed) + paddle.seed(seed) + +def memory_summary(): + vmem = psutil.virtual_memory() + msg = ( + f">>> Currently using {vmem.percent}% of system memory " + f"{humanize.naturalsize(vmem.used)}/{humanize.naturalsize(vmem.available)}" + ) + print(msg) + + +def flatten_dict(x, keysep="-"): + flat_dict = {} + for key, val in x.items(): + if isinstance(val, dict): + flat_subdict = flatten_dict(val) + flat_dict.update({f"{key}{keysep}{subkey}": subval + for subkey, subval in flat_subdict.items()}) + else: + flat_dict.update({key: val}) + return flat_dict + + +def expert_tensor_storage(experts, feat_aggregation): + expert_storage = {"fixed": set(), "variable": set(), "flaky": set()} + # fixed_sz_experts, variable_sz_experts, flaky_experts = set(), set(), set() + for expert, config in feat_aggregation.items(): + if config["temporal"] in {"vlad", "fixed_seg"}: + expert_storage["variable"].add(expert) + elif config["temporal"] in {"avg", "max", "avg-max", "max-avg", "avg-max-ent", + "max-avg-ent"}: + expert_storage["fixed"].add(expert) + else: + raise ValueError(f"unknown temporal strategy: {config['temporal']}") + # some "flaky" experts are only available for a fraction of videos - we need + # to pass this information (in the form of indices) into the network for any + # experts present in the current dataset + if config.get("flaky", False): + expert_storage["flaky"].add(expert) + + # we only allocate storage for experts used by the current dataset + for key, value in expert_storage.items(): + expert_storage[key] = value.intersection(set(experts)) + return expert_storage + + +def read_json(fname): + with fname.open('rt') as handle: + return json.load(handle, object_hook=OrderedDict) + + +def path2str(x): + """Recursively convert pathlib objects to strings to enable serialization""" + for key, val in x.items(): + if isinstance(val, dict): + path2str(val) + elif isinstance(val, Path): + x[key] = str(val) + + +def write_json(content, fname, paths2strs=False): + if paths2strs: + path2str(content) + with fname.open('wt') as handle: + json.dump(content, handle, indent=4, sort_keys=False) + + +def inf_loop(data_loader): + ''' wrapper function for endless data loader. ''' + for loader in repeat(data_loader): + yield from loader + + +class HashableDict(dict): + def __hash__(self): + return hash(frozenset(self)) + + +class HashableOrderedDict(dict): + def __hash__(self): + return hash(frozenset(self)) + + +def compute_trn_config(config, logger=None): + trn_config = {} + feat_agg = config["data_loader"]["args"]["feat_aggregation"] + for static_expert in feat_agg.keys(): + if static_expert in feat_agg: + if "trn_seg" in feat_agg[static_expert].keys(): + trn_config[static_expert] = feat_agg[static_expert]["trn_seg"] + return trn_config + + +def compute_dims(config, logger=None): + if logger is None: + logger = config.get_logger('utils') + + experts = config["experts"] + # TODO(Samuel): clean up the logic since it's a little convoluted + ordered = sorted(config["experts"]["modalities"]) + + if experts["drop_feats"]: + to_drop = experts["drop_feats"].split(",") + logger.info(f"dropping: {to_drop}") + ordered = [x for x in ordered if x not in to_drop] + + feat_agg = config["data_loader"]["args"]["feat_aggregation"] + dims = [] + arch_args = config["arch"]["args"] + vlad_clusters = arch_args["vlad_clusters"] + for expert in ordered: + temporal = feat_agg[expert]["temporal"] + if expert == "face": + in_dim, out_dim = experts["face_dim"], experts["face_dim"] + elif expert == "features_scene" and temporal == "vlad": + in_dim, out_dim = 2208 * vlad_clusters["features_scene"], 2208 + elif expert == "features_s3d" and temporal == "vlad": + in_dim, out_dim = 1024 * vlad_clusters["features_s3d"], 1024 + elif expert == "features_flow" and temporal == "vlad": + in_dim, out_dim = 1024 * vlad_clusters["features_flow"], 1024 + elif expert == "features_rgb" and temporal == "vlad": + in_dim, out_dim = 2048 * vlad_clusters["features_rgb"], 2048 + elif expert == "features_ocr" and temporal == "vlad": + in_dim, out_dim = 300 * vlad_clusters["features_ocr"], 300 + elif expert == "features_face" and temporal == "vlad": + in_dim, out_dim = 512 * vlad_clusters["features_face"], 512 + elif expert == "features_speech" and temporal == "vlad": + in_dim, out_dim = 300 * vlad_clusters["features_speech"], 300 + elif expert == "features_audio" and temporal == "vlad": + in_dim, out_dim = 128 * vlad_clusters["features_audio"], 128 + elif expert == "audio" and temporal == "vlad": + in_dim, out_dim = 128 * vlad_clusters["audio"], 128 + elif expert == "audio" and temporal == "vlad": + in_dim, out_dim = 128 * vlad_clusters["audio"], 128 + elif expert == "speech" and temporal == "vlad": + in_dim, out_dim = 300 * vlad_clusters["speech"], 300 + elif expert == "ocr" and temporal == "vlad": + in_dim, out_dim = 300 * vlad_clusters["ocr"], 300 + elif expert == "detection": + # allow for avg pooling + det_clusters = arch_args["vlad_clusters"].get("detection", 1) + in_dim, out_dim = 1541 * det_clusters, 1541 + elif expert == "detection-sem": + if config["data_loader"]["args"].get("spatial_feats", False): + base = 300 + 16 + else: + base = 300 + 5 + det_clusters = arch_args["vlad_clusters"].get("detection-sem", 1) + in_dim, out_dim = base * det_clusters, base + elif expert == "openpose": + base = 54 + det_clusters = arch_args["vlad_clusters"].get("openpose", 1) + in_dim, out_dim = base * det_clusters, base + else: + common_dim = feat_agg[expert]["feat_dims"][feat_agg[expert]["type"]] + # account for aggregation of multilpe forms (e.g. avg + max pooling) + common_dim = common_dim * len(feat_agg[expert]["temporal"].split("-")) + in_dim, out_dim = common_dim, common_dim + + # For the CE architecture, we need to project all features to a common + # dimensionality + if arch_args.get("mimic_ce_dims", False): + out_dim = experts["ce_shared_dim"] + + dims.append((expert, (in_dim, out_dim))) + expert_dims = OrderedDict(dims) + + if vlad_clusters["text"] == 0: + msg = "vlad can only be disabled for text with single tokens" + assert config["data_loader"]["args"]["max_tokens"]["text"] == 1, msg + + if config["experts"]["text_agg"] == "avg": + msg = "averaging can only be performed with text using single tokens" + assert config["arch"]["args"]["vlad_clusters"]["text"] == 0 + assert config["data_loader"]["args"]["max_tokens"]["text"] == 1 + + # To remove the dependency of dataloader on the model architecture, we create a + # second copy of the expert dimensions which accounts for the number of vlad + # clusters + raw_input_dims = OrderedDict() + for expert, dim_pair in expert_dims.items(): + raw_dim = dim_pair[0] + if expert in {"audio", "speech", "ocr", "detection", "detection-sem", "openpose", "features_audio", "features_speech", "features_face", "features_ocr", "features_rgb", "features_flow", "features_s3d", "features_scene", + "speech.mozilla.0"}: + if feat_agg[expert]["temporal"] == "vlad": + raw_dim = raw_dim // vlad_clusters.get(expert, 1) + raw_input_dims[expert] = raw_dim + + return expert_dims, raw_input_dims + + +def ensure_tensor(x): + if not isinstance(x, paddle.Tensor): #if not isinstance(x, torch.Tensor): + x = paddle.to_tensor(x) # x = torch.from_numpy(x) + return x + + +class Timer: + def __init__(self): + self.cache = datetime.now() + + def check(self): + now = datetime.now() + duration = now - self.cache + self.cache = now + return duration.total_seconds() + + def reset(self): + self.cache = datetime.now() + + +def tensor2im(input_image, imtype=np.uint8): + """"Converts a Tensor array into a numpy image array. + + Parameters: + input_image (tensor) -- the input image tensor array + imtype (type) -- the desired type of the converted numpy array + """ + if not isinstance(input_image, np.ndarray): + if isinstance(input_image, paddle.Tensor): #if isinstance(input_image, torch.Tensor): # get the data from a variable + image_tensor = input_image #image_tensor = input_image.data + else: + return input_image + # convert it into a numpy array + image_numpy = image_tensor[0].cpu().float().numpy() + if image_numpy.shape[0] == 1: # grayscale to RGB + image_numpy = np.tile(image_numpy, (3, 1, 1)) + # post-processing: tranpose and scaling + image_numpy = (np.transpose(image_numpy, (1, 2, 0)) + 1) / 2.0 * 255.0 + else: # if it is a numpy array, do nothing + image_numpy = input_image + return image_numpy.astype(imtype) + + +def save_image(image_numpy, image_path): + """Save a numpy image to the disk + + Parameters: + image_numpy (numpy array) -- input numpy array + image_path (str) -- the path of the image + """ + image_pil = Image.fromarray(image_numpy) + image_pil.save(image_path) + + +def print_numpy(x, val=True, shp=False): + """Print the mean, min, max, median, std, and size of a numpy array + + Parameters: + val (bool) -- if print the values of the numpy array + shp (bool) -- if print the shape of the numpy array + """ + x = x.astype(np.float64) + if shp: + print('shape,', x.shape) + if val: + x = x.flatten() + print('mean = %3.3f, min = %3.3f, max = %3.3f, median = %3.3f, std=%3.3f' % ( + np.mean(x), np.min(x), np.max(x), np.median(x), np.std(x))) + + +def mkdirs(paths): + """create empty directories if they don't exist + + Parameters: + paths (str list) -- a list of directory paths + """ + if isinstance(paths, list) and not isinstance(paths, str): + for path in paths: + mkdir(path) + else: + mkdir(paths) + + +def mkdir(path): + """create a single empty directory if it didn't exist + + Parameters: + path (str) -- a single directory path + """ + if not os.path.exists(path): + os.makedirs(path) diff --git a/applications/T2VLAD/word2int.json b/applications/T2VLAD/word2int.json new file mode 100644 index 0000000000000000000000000000000000000000..014e3e72727c44fb5aba9efd190ea55363bd3bf9 --- /dev/null +++ b/applications/T2VLAD/word2int.json @@ -0,0 +1 @@ +{"": 0, "": 1, "": 2, "a": 3, "is": 4, "the": 5, "in": 6, "man": 7, "and": 8, "of": 9, "on": 10, "to": 11, "woman": 12, "are": 13, "with": 14, "about": 15, "talking": 16, "video": 17, "person": 18, "playing": 19, "an": 20, "people": 21, "game": 22, "two": 23, "girl": 24, "car": 25, "some": 26, "for": 27, "from": 28, "his": 29, "men": 30, "singing": 31, "while": 32, "at": 33, "show": 34, "her": 35, "cartoon": 36, "someone": 37, "showing": 38, "how": 39, "being": 40, "there": 41, "black": 42, "it": 43, "talks": 44, "white": 45, "shown": 46, "stage": 47, "young": 48, "group": 49, "women": 50, "by": 51, "music": 52, "other": 53, "dancing": 54, "movie": 55, "guy": 56, "into": 57, "boy": 58, "song": 59, "s": 60, "shirt": 61, "up": 62, "down": 63, "food": 64, "something": 65, "wearing": 66, "around": 67, "sitting": 68, "walking": 69, "screen": 70, "as": 71, "news": 72, "another": 73, "blue": 74, "clip": 75, "lady": 76, "that": 77, "red": 78, "their": 79, "speaking": 80, "out": 81, "one": 82, "different": 83, "cooking": 84, "plays": 85, "kitchen": 86, "animated": 87, "driving": 88, "camera": 89, "-": 90, "shows": 91, "giving": 92, "doing": 93, "water": 94, "characters": 95, "he": 96, "front": 97, "dress": 98, "then": 99, "each": 100, "table": 101, "tv": 102, "standing": 103, "making": 104, "scene": 105, "kids": 106, "through": 107, "room": 108, "play": 109, "ball": 110, "this": 111, "character": 112, "paper": 113, "talk": 114, "girls": 115, "hair": 116, "running": 117, "over": 118, "explaining": 119, "road": 120, "off": 121, "player": 122, "basketball": 123, "band": 124, "performing": 125, "sports": 126, "green": 127, "inside": 128, "small": 129, "large": 130, "dish": 131, "holding": 132, "players": 133, "looking": 134, "computer": 135, "make": 136, "wrestling": 137, "three": 138, "together": 139, "baby": 140, "dog": 141, "having": 142, "children": 143, "football": 144, "she": 145, "very": 146, "audience": 147, "sings": 148, "minecraft": 149, "fighting": 150, "street": 151, "displaying": 152, "horse": 153, "soccer": 154, "riding": 155, "bowl": 156, "going": 157, "has": 158, "dance": 159, "little": 160, "various": 161, "match": 162, "using": 163, "speaks": 164, "child": 165, "watching": 166, "suit": 167, "they": 168, "new": 169, "discussing": 170, "guitar": 171, "outside": 172, "crowd": 173, "ingredients": 174, "pan": 175, "old": 176, "tennis": 177, "its": 178, "cars": 179, "toy": 180, "voice": 181, "cat": 182, "phone": 183, "hand": 184, "played": 185, "getting": 186, "them": 187, "couple": 188, "describing": 189, "taking": 190, "clips": 191, "footage": 192, "television": 193, "who": 194, "where": 195, "preparing": 196, "kid": 197, "beach": 198, "pictures": 199, "him": 200, "field": 201, "background": 202, "moving": 203, "guys": 204, "bed": 205, "face": 206, "putting": 207, "many": 208, "race": 209, "eating": 210, "several": 211, "floor": 212, "interview": 213, "interviewed": 214, "discusses": 215, "back": 216, "ground": 217, "yellow": 218, "makeup": 219, "color": 220, "walks": 221, "female": 222, "top": 223, "be": 224, "was": 225, "baseball": 226, "explains": 227, "which": 228, "near": 229, "what": 230, "trying": 231, "recipe": 232, "like": 233, "brown": 234, "animals": 235, "animation": 236, "beautiful": 237, "piece": 238, "dressed": 239, "describes": 240, "pot": 241, "jumping": 242, "building": 243, "stroller": 244, "all": 245, "picture": 246, "pink": 247, "working": 248, "chef": 249, "track": 250, "cook": 251, "house": 252, "hands": 253, "sits": 254, "big": 255, "blonde": 256, "scenes": 257, "male": 258, "toys": 259, "fish": 260, "program": 261, "sing": 262, "looks": 263, "vehicle": 264, "displayed": 265, "city": 266, "motorcycle": 267, "telling": 268, "side": 269, "trailer": 270, "mixing": 271, "next": 272, "gives": 273, "truck": 274, "spongebob": 275, "bike": 276, "fashion": 277, "long": 278, "short": 279, "live": 280, "animal": 281, "t": 282, "boat": 283, "glasses": 284, "family": 285, "gun": 286, "walk": 287, "folding": 288, "behind": 289, "team": 290, "dark": 291, "boys": 292, "wall": 293, "games": 294, "school": 295, "asian": 296, "lego": 297, "gameplay": 298, "between": 299, "film": 300, "use": 301, "drives": 302, "demonstrating": 303, "get": 304, "fast": 305, "judges": 306, "runs": 307, "monkey": 308, "conversation": 309, "shooting": 310, "bunch": 311, "during": 312, "swimming": 313, "pool": 314, "set": 315, "do": 316, "advertisement": 317, "head": 318, "flying": 319, "prepare": 320, "channel": 321, "makes": 322, "along": 323, "home": 324, "gets": 325, "hat": 326, "forest": 327, "plate": 328, "reporter": 329, "commercial": 330, "chair": 331, "piano": 332, "cutting": 333, "microphone": 334, "can": 335, "features": 336, "model": 337, "stands": 338, "have": 339, "after": 340, "work": 341, "glass": 342, "fight": 343, "machine": 344, "images": 345, "view": 346, "away": 347, "things": 348, "speech": 349, "competition": 350, "jacket": 351, "mountain": 352, "box": 353, "door": 354, "court": 355, "chicken": 356, "does": 357, "airplane": 358, "snow": 359, "watch": 360, "recording": 361, "persons": 362, "laughing": 363, "trampoline": 364, "soldiers": 365, "stove": 366, "you": 367, "trees": 368, "still": 369, "across": 370, "place": 371, "fire": 372, "indian": 373, "meat": 374, "before": 375, "high": 376, "vegetables": 377, "put": 378, "made": 379, "drawing": 380, "review": 381, "oil": 382, "studio": 383, "dances": 384, "time": 385, "bag": 386, "lot": 387, "or": 388, "singer": 389, "ready": 390, "host": 391, "demonstrates": 392, "racing": 393, "ping": 394, "were": 395, "few": 396, "performs": 397, "four": 398, "eat": 399, "pong": 400, "grass": 401, "not": 402, "first": 403, "tutorial": 404, "coat": 405, "discuss": 406, "product": 407, "star": 408, "runway": 409, "puts": 410, "funny": 411, "body": 412, "holds": 413, "train": 414, "shot": 415, "adding": 416, "light": 417, "photos": 418, "reading": 419, "area": 420, "photo": 421, "rides": 422, "purple": 423, "look": 424, "laying": 425, "engine": 426, "against": 427, "takes": 428, "world": 429, "onto": 430, "teaching": 431, "applying": 432, "dogs": 433, "plane": 434, "sit": 435, "orange": 436, "night": 437, "here": 438, "spider": 439, "store": 440, "sea": 441, "cloth": 442, "models": 443, "rock": 444, "hockey": 445, "air": 446, "police": 447, "take": 448, "stand": 449, "older": 450, "angry": 451, "goal": 452, "hot": 453, "board": 454, "basket": 455, "day": 456, "full": 457, "others": 458, "concert": 459, "see": 460, "beside": 461, "language": 462, "sauce": 463, "image": 464, "grey": 465, "interviewing": 466, "under": 467, "performance": 468, "river": 469, "war": 470, "hit": 471, "highlights": 472, "birds": 473, "run": 474, "way": 475, "plastic": 476, "sky": 477, "part": 478, "adds": 479, "go": 480, "ice": 481, "friends": 482, "kissing": 483, "wrestlers": 484, "foreign": 485, "jumps": 486, "when": 487, "well": 488, "book": 489, "restaurant": 490, "stadium": 491, "appears": 492, "story": 493, "pieces": 494, "event": 495, "spoon": 496, "gray": 497, "commentating": 498, "ocean": 499, "instructions": 500, "taken": 501, "beauty": 502, "pokemon": 503, "opening": 504, "tree": 505, "clothes": 506, "meal": 507, "bus": 508, "goes": 509, "dolls": 510, "action": 511, "drinking": 512, "open": 513, "segment": 514, "enjoying": 515, "movies": 516, "videos": 517, "country": 518, "motion": 519, "disney": 520, "seen": 521, "drink": 522, "metal": 523, "featuring": 524, "will": 525, "posing": 526, "cut": 527, "falls": 528, "commentary": 529, "freddy": 530, "text": 531, "teams": 532, "tie": 533, "tshirt": 534, "demonstration": 535, "hitting": 536, "letterman": 537, "frying": 538, "presenting": 539, "couch": 540, "science": 541, "mountains": 542, "items": 543, "asking": 544, "tries": 545, "party": 546, "egg": 547, "life": 548, "haired": 549, "himself": 550, "slideshow": 551, "good": 552, "desk": 553, "drive": 554, "nice": 555, "close": 556, "pouring": 557, "middle": 558, "cup": 559, "students": 560, "eye": 561, "throwing": 562, "window": 563, "potatoes": 564, "wooden": 565, "mother": 566, "falling": 567, "david": 568, "information": 569, "bill": 570, "pet": 571, "professional": 572, "narrator": 573, "park": 574, "smiling": 575, "talent": 576, "technology": 577, "robot": 578, "cats": 579, "fun": 580, "horses": 581, "saying": 582, "perform": 583, "presentation": 584, "uses": 585, "moves": 586, "friend": 587, "ring": 588, "eggs": 589, "parts": 590, "towards": 591, "anime": 592, "office": 593, "cap": 594, "mouse": 595, "doll": 596, "works": 597, "style": 598, "dough": 599, "tank": 600, "more": 601, "filming": 602, "president": 603, "gym": 604, "actor": 605, "interviews": 606, "bottle": 607, "cute": 608, "us": 609, "space": 610, "avengers": 611, "clothing": 612, "been": 613, "craft": 614, "coming": 615, "wood": 616, "cartoons": 617, "narrates": 618, "thing": 619, "eyes": 620, "obama": 621, "line": 622, "bird": 623, "series": 624, "lying": 625, "type": 626, "slow": 627, "ship": 628, "words": 629, "colored": 630, "your": 631, "shots": 632, "end": 633, "speak": 634, "prepares": 635, "display": 636, "famous": 637, "we": 638, "outdoors": 639, "volleyball": 640, "2": 641, "parked": 642, "puppy": 643, "cream": 644, "sexy": 645, "tiger": 646, "class": 647, "i": 648, "ride": 649, "athletes": 650, "squidward": 651, "love": 652, "item": 653, "battle": 654, "american": 655, "classroom": 656, "wrestler": 657, "stirring": 658, "laptop": 659, "multiple": 660, "bar": 661, "watches": 662, "brush": 663, "if": 664, "living": 665, "comedy": 666, "report": 667, "filled": 668, "cuts": 669, "throws": 670, "cake": 671, "pop": 672, "silver": 673, "colorful": 674, "rugby": 675, "shop": 676, "cheering": 677, "so": 678, "called": 679, "montage": 680, "cooks": 681, "woods": 682, "money": 683, "covered": 684, "panel": 685, "colour": 686, "origami": 687, "used": 688, "website": 689, "fixing": 690, "ladies": 691, "mortal": 692, "knife": 693, "reviewing": 694, "ad": 695, "jump": 696, "shrimp": 697, "parking": 698, "noodles": 699, "kept": 700, "teacher": 701, "stick": 702, "hits": 703, "wears": 704, "liquid": 705, "puppies": 706, "comes": 707, "bear": 708, "dirt": 709, "demo": 710, "musical": 711, "serve": 712, "military": 713, "lyrics": 714, "busy": 715, "human": 716, "sleeping": 717, "but": 718, "starts": 719, "practicing": 720, "questions": 721, "wear": 722, "explain": 723, "shoots": 724, "flower": 725, "pours": 726, "device": 727, "past": 728, "products": 729, "spanish": 730, "asks": 731, "interior": 732, "prepared": 733, "also": 734, "says": 735, "lake": 736, "anchor": 737, "mobile": 738, "streets": 739, "celebrity": 740, "paint": 741, "bicycle": 742, "opens": 743, "reporting": 744, "super": 745, "cooked": 746, "bridge": 747, "graphics": 748, "bat": 749, "doctor": 750, "brunette": 751, "online": 752, "vehicles": 753, "huge": 754, "object": 755, "boiling": 756, "waves": 757, "murray": 758, "reviews": 759, "help": 760, "care": 761, "lecture": 762, "cube": 763, "tells": 764, "acting": 765, "move": 766, "art": 767, "great": 768, "hill": 769, "just": 770, "counter": 771, "both": 772, "dresses": 773, "pair": 774, "father": 775, "company": 776, "cage": 777, "points": 778, "kind": 779, "mirror": 780, "places": 781, "youtube": 782, "potato": 783, "soup": 784, "fireworks": 785, "business": 786, "heavy": 787, "wedding": 788, "same": 789, "lights": 790, "experiment": 791, "mask": 792, "clinton": 793, "onions": 794, "experience": 795, "herself": 796, "oven": 797, "software": 798, "popular": 799, "carrying": 800, "point": 801, "leg": 802, "exercise": 803, "dancers": 804, "topic": 805, "iron": 806, "discussion": 807, "had": 808, "map": 809, "interacting": 810, "tricks": 811, "no": 812, "kombat": 813, "cell": 814, "system": 815, "pretty": 816, "nature": 817, "ted": 818, "land": 819, "dressing": 820, "speed": 821, "rapping": 822, "rap": 823, "scores": 824, "driver": 825, "colors": 826, "stars": 827, "fox": 828, "spiderman": 829, "batman": 830, "tips": 831, "best": 832, "folds": 833, "bad": 834, "fold": 835, "crying": 836, "outfit": 837, "media": 838, "try": 839, "helmet": 840, "candy": 841, "right": 842, "history": 843, "design": 844, "showcasing": 845, "applies": 846, "subscribe": 847, "waving": 848, "hugging": 849, "slowly": 850, "cover": 851, "rain": 852, "golf": 853, "placed": 854, "half": 855, "leaves": 856, "english": 857, "motorcycles": 858, "chasing": 859, "shorts": 860, "mat": 861, "bob": 862, "church": 863, "3d": 864, "roof": 865, "instruments": 866, "narrating": 867, "figure": 868, "setting": 869, "rocks": 870, "rolling": 871, "earth": 872, "shoot": 873, "town": 874, "poses": 875, "african": 876, "events": 877, "compilation": 878, "slide": 879, "fans": 880, "actors": 881, "bikini": 882, "given": 883, "involving": 884, "hold": 885, "listening": 886, "catch": 887, "celebrating": 888, "buildings": 889, "spices": 890, "powder": 891, "test": 892, "hanging": 893, "number": 894, "done": 895, "pants": 896, "left": 897, "net": 898, "painting": 899, "stirs": 900, "meeting": 901, "preforms": 902, "surrounded": 903, "flies": 904, "pointing": 905, "added": 906, "writing": 907, "pirates": 908, "lion": 909, "sponge": 910, "politics": 911, "shirts": 912, "start": 913, "neck": 914, "sound": 915, "garage": 916, "container": 917, "special": 918, "scary": 919, "graphic": 920, "boxing": 921, "flowers": 922, "surfing": 923, "bald": 924, "amazing": 925, "auto": 926, "turns": 927, "arms": 928, "filmed": 929, "kiss": 930, "hero": 931, "juice": 932, "outdoor": 933, "garlic": 934, "shopping": 935, "seat": 936, "seeing": 937, "wheel": 938, "motor": 939, "types": 940, "son": 941, "bread": 942, "jeans": 943, "course": 944, "gymnastics": 945, "pets": 946, "seems": 947, "mouth": 948, "3": 949, "process": 950, "viewers": 951, "monster": 952, "blond": 953, "including": 954, "babies": 955, "stairs": 956, "views": 957, "got": 958, "sun": 959, "hard": 960, "sims": 961, "project": 962, "vegetable": 963, "stuff": 964, "logo": 965, "christmas": 966, "tall": 967, "horror": 968, "electronic": 969, "path": 970, "chairs": 971, "artist": 972, "clapping": 973, "competing": 974, "washing": 975, "figures": 976, "comments": 977, "sweater": 978, "british": 979, "soldier": 980, "equipment": 981, "hall": 982, "drinks": 983, "garden": 984, "square": 985, "teenage": 986, "costume": 987, "vine": 988, "mic": 989, "contestant": 990, "five": 991, "snake": 992, "giant": 993, "balls": 994, "cast": 995, "political": 996, "fall": 997, "factory": 998, "couples": 999, "really": 1000, "skating": 1001, "officer": 1002, "helicopter": 1003, "swim": 1004, "student": 1005, "sport": 1006, "driven": 1007, "sign": 1008, "drums": 1009, "mixes": 1010, "practice": 1011, "sunglasses": 1012, "sword": 1013, "stop": 1014, "followed": 1015, "reality": 1016, "plants": 1017, "yelling": 1018, "suits": 1019, "wars": 1020, "celebrities": 1021, "shooter": 1022, "education": 1023, "hallway": 1024, "smoking": 1025, "placing": 1026, "uniform": 1027, "moon": 1028, "dishes": 1029, "variety": 1030, "songs": 1031, "bra": 1032, "honda": 1033, "lab": 1034, "advertising": 1035, "delicious": 1036, "pasta": 1037, "highway": 1038, "training": 1039, "actress": 1040, "lays": 1041, "wild": 1042, "win": 1043, "selfie": 1044, "times": 1045, "give": 1046, "shaking": 1047, "preview": 1048, "apply": 1049, "serving": 1050, "striped": 1051, "bedroom": 1052, "begins": 1053, "late": 1054, "singers": 1055, "baking": 1056, "judge": 1057, "nail": 1058, "dinosaur": 1059, "winning": 1060, "why": 1061, "america": 1062, "coach": 1063, "legos": 1064, "dinner": 1065, "tub": 1066, "appear": 1067, "dragon": 1068, "films": 1069, "faces": 1070, "reporters": 1071, "pulls": 1072, "army": 1073, "clear": 1074, "pulling": 1075, "flooring": 1076, "mario": 1077, "happy": 1078, "badminton": 1079, "meet": 1080, "skills": 1081, "fried": 1082, "scientist": 1083, "hospital": 1084, "public": 1085, "thor": 1086, "parrot": 1087, "tools": 1088, "question": 1089, "musicians": 1090, "squarepants": 1091, "accident": 1092, "market": 1093, "traffic": 1094, "batter": 1095, "hip": 1096, "salt": 1097, "introducing": 1098, "certain": 1099, "routine": 1100, "steps": 1101, "outfits": 1102, "breaking": 1103, "eats": 1104, "daughter": 1105, "commenting": 1106, "peoples": 1107, "episode": 1108, "shoes": 1109, "grand": 1110, "skiing": 1111, "adults": 1112, "japanese": 1113, "pepper": 1114, "ellen": 1115, "upcoming": 1116, "parody": 1117, "without": 1118, "station": 1119, "cleaning": 1120, "much": 1121, "fence": 1122, "touching": 1123, "milk": 1124, "rice": 1125, "barbie": 1126, "gold": 1127, "comedian": 1128, "laugh": 1129, "elephant": 1130, "climbing": 1131, "showed": 1132, "shore": 1133, "lots": 1134, "cheese": 1135, "seated": 1136, "hop": 1137, "sand": 1138, "hamster": 1139, "contestants": 1140, "tan": 1141, "name": 1142, "reads": 1143, "krueger": 1144, "opponent": 1145, "legs": 1146, "desert": 1147, "knocking": 1148, "hole": 1149, "club": 1150, "athlete": 1151, "attack": 1152, "power": 1153, "fake": 1154, "laughs": 1155, "photographs": 1156, "find": 1157, "pony": 1158, "excited": 1159, "traveling": 1160, "lifting": 1161, "compete": 1162, "travelling": 1163, "conference": 1164, "debate": 1165, "mine": 1166, "teenagers": 1167, "flips": 1168, "teen": 1169, "surface": 1170, "donald": 1171, "hillary": 1172, "presented": 1173, "waiting": 1174, "scoring": 1175, "check": 1176, "teaches": 1177, "guest": 1178, "weather": 1179, "carpet": 1180, "bbc": 1181, "jeep": 1182, "beard": 1183, "floating": 1184, "sunny": 1185, "cheer": 1186, "indoor": 1187, "documentary": 1188, "butter": 1189, "attacking": 1190, "word": 1191, "sidewalk": 1192, "cnn": 1193, "waterfall": 1194, "sugar": 1195, "onion": 1196, "arguing": 1197, "records": 1198, "modeling": 1199, "round": 1200, "above": 1201, "finger": 1202, "come": 1203, "named": 1204, "globe": 1205, "ramp": 1206, "screaming": 1207, "activities": 1208, "skillet": 1209, "tigers": 1210, "college": 1211, "jungle": 1212, "themselves": 1213, "letter": 1214, "monkeys": 1215, "distance": 1216, "sort": 1217, "own": 1218, "learning": 1219, "o": 1220, "brain": 1221, "creating": 1222, "introduction": 1223, "starting": 1224, "/": 1225, "sofa": 1226, "list": 1227, "apron": 1228, "construction": 1229, "discussed": 1230, "dead": 1231, "bench": 1232, "bowls": 1233, "latest": 1234, "matches": 1235, "creature": 1236, "travel": 1237, "blender": 1238, "skit": 1239, "mini": 1240, "foot": 1241, "gathered": 1242, "checking": 1243, "chefs": 1244, "catching": 1245, "dunk": 1246, "details": 1247, "fly": 1248, "sequence": 1249, "sporting": 1250, "described": 1251, "german": 1252, "rolls": 1253, "flag": 1254, "slicing": 1255, "salad": 1256, "add": 1257, "vs": 1258, "jimmy": 1259, "chinese": 1260, "pans": 1261, "form": 1262, "fights": 1263, "bikes": 1264, "wine": 1265, "mom": 1266, "crash": 1267, "empty": 1268, "math": 1269, "strange": 1270, "center": 1271, "objects": 1272, "foods": 1273, "lipstick": 1274, "entering": 1275, "sex": 1276, "bright": 1277, "regarding": 1278, "circle": 1279, "yard": 1280, "answering": 1281, "attacks": 1282, "firing": 1283, "trucks": 1284, "flip": 1285, "trail": 1286, "slam": 1287, "captain": 1288, "trump": 1289, "owl": 1290, "stream": 1291, "z": 1292, "real": 1293, "held": 1294, "stuck": 1295, "rope": 1296, "michael": 1297, "states": 1298, "speaker": 1299, "talked": 1300, "fry": 1301, "mix": 1302, "change": 1303, "alien": 1304, "tire": 1305, "sheet": 1306, "preparation": 1307, "tube": 1308, "podium": 1309, "mixer": 1310, "guns": 1311, "intro": 1312, "weight": 1313, "tablet": 1314, "flash": 1315, "material": 1316, "mall": 1317, "job": 1318, "creatures": 1319, "tiny": 1320, "bath": 1321, "pirate": 1322, "nasa": 1323, "forth": 1324, "data": 1325, "actions": 1326, "fancy": 1327, "killing": 1328, "skin": 1329, "bottom": 1330, "deep": 1331, "parents": 1332, "library": 1333, "catches": 1334, "pushing": 1335, "folded": 1336, "feeding": 1337, "wave": 1338, "pizza": 1339, "fix": 1340, "superman": 1341, "app": 1342, "gamer": 1343, "health": 1344, "superhero": 1345, "cow": 1346, "snowy": 1347, "taste": 1348, "cheers": 1349, "kisses": 1350, "base": 1351, "pose": 1352, "benefits": 1353, "dunks": 1354, "spinning": 1355, "groups": 1356, "kicks": 1357, "injured": 1358, "hollywood": 1359, "squid": 1360, "passing": 1361, "gear": 1362, "orchestra": 1363, "volley": 1364, "frozen": 1365, "taylor": 1366, "carrot": 1367, "than": 1368, "explained": 1369, "voices": 1370, "newscaster": 1371, "react": 1372, "step": 1373, "petting": 1374, "ways": 1375, "feature": 1376, "announcer": 1377, "removing": 1378, "patient": 1379, "nae": 1380, "mans": 1381, "too": 1382, "natural": 1383, "loud": 1384, "flour": 1385, "tool": 1386, "swinging": 1387, "(": 1388, "blow": 1389, "adult": 1390, "problem": 1391, "searching": 1392, "attractive": 1393, "weird": 1394, "cave": 1395, "styles": 1396, "talkshow": 1397, "exercises": 1398, "press": 1399, "climbs": 1400, "displays": 1401, "hotel": 1402, "gloves": 1403, "princess": 1404, "mixed": 1405, "tom": 1406, "arm": 1407, "grill": 1408, "sharing": 1409, "crazy": 1410, "french": 1411, "wins": 1412, "sticks": 1413, "tail": 1414, "digital": 1415, "act": 1416, "instructional": 1417, "signing": 1418, "level": 1419, "mickey": 1420, "problems": 1421, "cricket": 1422, "workers": 1423, "accent": 1424, "twinkle": 1425, "drops": 1426, "bearded": 1427, "sad": 1428, "highlight": 1429, "apple": 1430, "clean": 1431, "role": 1432, "laser": 1433, "books": 1434, "clouds": 1435, "case": 1436, "scared": 1437, "picks": 1438, "blocks": 1439, "enemies": 1440, "relationships": 1441, "kicking": 1442, "let": 1443, "safety": 1444, "leather": 1445, "facial": 1446, "modern": 1447, "village": 1448, "kitten": 1449, "spears": 1450, "combat": 1451, "specs": 1452, "mike": 1453, "king": 1454, "5": 1455, "teens": 1456, "pack": 1457, "stuffed": 1458, "introduces": 1459, "stone": 1460, "tea": 1461, "thrown": 1462, "landscape": 1463, "important": 1464, "quickly": 1465, "button": 1466, "these": 1467, "roll": 1468, "reports": 1469, "corner": 1470, "individual": 1471, "hood": 1472, "ninja": 1473, "presidential": 1474, "rocket": 1475, "lines": 1476, "commentator": 1477, "beginning": 1478, "block": 1479, "interviewer": 1480, "turn": 1481, "grassy": 1482, "medical": 1483, "shape": 1484, "members": 1485, "enjoy": 1486, "nearby": 1487, "knocks": 1488, "my": 1489, "testing": 1490, "south": 1491, "low": 1492, "backflip": 1493, "scooter": 1494, "tournament": 1495, "smart": 1496, "somebody": 1497, "favorite": 1498, "jokes": 1499, "would": 1500, "caribbean": 1501, "pass": 1502, "kruger": 1503, "crashes": 1504, "fishes": 1505, "instrument": 1506, "card": 1507, "york": 1508, "flags": 1509, "texting": 1510, "clay": 1511, "wrestle": 1512, "control": 1513, "statue": 1514, "volkswagen": 1515, "need": 1516, "costumes": 1517, "contest": 1518, "infront": 1519, "cowboy": 1520, "lesson": 1521, "enters": 1522, "purse": 1523, "random": 1524, "fan": 1525, "underwater": 1526, "keyboard": 1527, "tracks": 1528, "internet": 1529, "election": 1530, "hulk": 1531, "coffee": 1532, "locations": 1533, "letters": 1534, "hosting": 1535, "racket": 1536, "related": 1537, "van": 1538, "sounds": 1539, "flat": 1540, "future": 1541, "signs": 1542, "covering": 1543, "gravy": 1544, "magazine": 1545, "cross": 1546, "diagram": 1547, "score": 1548, "arena": 1549, "theft": 1550, "pen": 1551, "poured": 1552, "jar": 1553, "chopping": 1554, "nails": 1555, "did": 1556, "videogame": 1557, "angles": 1558, "reacting": 1559, "wore": 1560, "lift": 1561, "depicts": 1562, "island": 1563, "stir": 1564, "polish": 1565, "environment": 1566, "cart": 1567, "paste": 1568, "tied": 1569, "motorbike": 1570, "intense": 1571, "want": 1572, "frog": 1573, "jack": 1574, "issues": 1575, "me": 1576, "victory": 1577, "iphone": 1578, "childrens": 1579, "heads": 1580, "feet": 1581, "won": 1582, "because": 1583, "break": 1584, "changing": 1585, "based": 1586, "particular": 1587, "golden": 1588, "cliff": 1589, "theater": 1590, "entertainment": 1591, "bow": 1592, "lions": 1593, "lemon": 1594, "bathroom": 1595, "aquarium": 1596, "heart": 1597, "staring": 1598, "planet": 1599, "slices": 1600, "lunch": 1601, "lizard": 1602, "boxes": 1603, "site": 1604, "picking": 1605, "lip": 1606, "scenery": 1607, "smiles": 1608, "frame": 1609, "description": 1610, "page": 1611, "speeds": 1612, "award": 1613, "caught": 1614, "plant": 1615, "blowing": 1616, "smoke": 1617, "countdown": 1618, "installing": 1619, "written": 1620, "attempting": 1621, "scarf": 1622, "repair": 1623, "eachother": 1624, "monitor": 1625, "lost": 1626, "package": 1627, "credits": 1628, "fat": 1629, "draws": 1630, "draw": 1631, "duck": 1632, "explanation": 1633, "pushes": 1634, "needed": 1635, "dad": 1636, "tasting": 1637, "trick": 1638, "throw": 1639, "install": 1640, "tour": 1641, "flight": 1642, "spray": 1643, "target": 1644, "skunk": 1645, "narrow": 1646, "covers": 1647, "menu": 1648, "our": 1649, "describe": 1650, "technique": 1651, "call": 1652, "races": 1653, "moments": 1654, "year": 1655, "sets": 1656, "changes": 1657, "helping": 1658, "social": 1659, "kittens": 1660, "bollywood": 1661, "formal": 1662, "touchdown": 1663, "lamborghini": 1664, "apart": 1665, "bucket": 1666, "swift": 1667, "secret": 1668, "performed": 1669, "guitars": 1670, "lid": 1671, "elderly": 1672, "wheels": 1673, "catwalk": 1674, "gather": 1675, ":": 1676, "chris": 1677, "breakfast": 1678, "uniforms": 1679, "aged": 1680, "owner": 1681, "hammer": 1682, "cycle": 1683, "carrots": 1684, "sees": 1685, "eyebrows": 1686, "options": 1687, "built": 1688, "zombies": 1689, "touch": 1690, "fruit": 1691, "lit": 1692, "featured": 1693, "tunnel": 1694, "crowded": 1695, "skeleton": 1696, "interesting": 1697, "arrow": 1698, "dining": 1699, "grabs": 1700, "grocery": 1701, "assembling": 1702, "only": 1703, "found": 1704, "screens": 1705, "fishing": 1706, "tower": 1707, "leopard": 1708, "tomato": 1709, "flashing": 1710, "photographed": 1711, "indoors": 1712, "computers": 1713, "wings": 1714, "males": 1715, "keeping": 1716, "current": 1717, "toward": 1718, "wants": 1719, "females": 1720, "snacks": 1721, "hats": 1722, "x": 1723, "villain": 1724, "pick": 1725, "puppet": 1726, "dramatic": 1727, "structure": 1728, "showcased": 1729, "depicting": 1730, "flowing": 1731, "bubbles": 1732, "exploration": 1733, "wife": 1734, "microwave": 1735, "muppet": 1736, "ghost": 1737, "pc": 1738, "properly": 1739, "network": 1740, "united": 1741, "gentleman": 1742, "shoulder": 1743, "finish": 1744, "spins": 1745, "breaks": 1746, "remove": 1747, "machinery": 1748, "gaming": 1749, "energy": 1750, "beer": 1751, "dessert": 1752, "solving": 1753, "raps": 1754, "brushes": 1755, "chopped": 1756, "repairing": 1757, "plaid": 1758, "stirred": 1759, "attempts": 1760, "classic": 1761, "anchors": 1762, "john": 1763, "cancer": 1764, "virtual": 1765, "broken": 1766, "stretching": 1767, "director": 1768, "sides": 1769, "until": 1770, "mixture": 1771, "such": 1772, "raw": 1773, "punching": 1774, "silly": 1775, "everyone": 1776, "function": 1777, "perfect": 1778, "4": 1779, "wash": 1780, "circles": 1781, "crafts": 1782, "married": 1783, "listen": 1784, "providing": 1785, "arts": 1786, "runners": 1787, "pencil": 1788, "hills": 1789, "cam": 1790, "non": 1791, "fatality": 1792, "create": 1793, "rocky": 1794, "musician": 1795, "cigarette": 1796, "doctors": 1797, "recorded": 1798, "decorating": 1799, "self": 1800, "graph": 1801, "surf": 1802, "patrick": 1803, "vessel": 1804, "headphones": 1805, "operating": 1806, "dry": 1807, "dr": 1808, "cookies": 1809, "following": 1810, "most": 1811, "solve": 1812, "jet": 1813, "materials": 1814, "sliced": 1815, "among": 1816, "else": 1817, "fingers": 1818, "weapons": 1819, "magic": 1820, "&": 1821, "service": 1822, "battling": 1823, "string": 1824, "served": 1825, "performers": 1826, "search": 1827, "platform": 1828, "beat": 1829, "hiking": 1830, "carefully": 1831, "effects": 1832, "eyeliner": 1833, "subject": 1834, "scrolling": 1835, "chocolate": 1836, "turning": 1837, "stunts": 1838, "last": 1839, "violent": 1840, "build": 1841, "leonardo": 1842, "hugs": 1843, "lime": 1844, "roads": 1845, "cool": 1846, "britney": 1847, "killed": 1848, "death": 1849, "surfboard": 1850, "beating": 1851, "pole": 1852, "broadcast": 1853, "bugs": 1854, "mr": 1855, "yoga": 1856, "belt": 1857, "shapes": 1858, "third": 1859, "painted": 1860, "removes": 1861, "instruction": 1862, "kill": 1863, "lighting": 1864, "watermelon": 1865, "boats": 1866, "cellphone": 1867, "ending": 1868, "backpack": 1869, "theme": 1870, "monsters": 1871, "binoculars": 1872, "hiding": 1873, "exercising": 1874, "clipping": 1875, "activity": 1876, "bouncing": 1877, "fell": 1878, "containing": 1879, "sink": 1880, "saving": 1881, "ufc": 1882, "celebrate": 1883, "comedic": 1884, "celebrates": 1885, "steel": 1886, "awards": 1887, "fresh": 1888, "educational": 1889, "noises": 1890, "jones": 1891, "bride": 1892, "hoop": 1893, "read": 1894, "closed": 1895, "techniques": 1896, "bomb": 1897, "justin": 1898, "fastly": 1899, "rubbing": 1900, "should": 1901, "sunset": 1902, "free": 1903, "crew": 1904, "tray": 1905, "main": 1906, "lips": 1907, "evil": 1908, "chased": 1909, "curly": 1910, "loves": 1911, "athletic": 1912, "wolf": 1913, "shakes": 1914, "forward": 1915, "came": 1916, "competitive": 1917, "bathing": 1918, "reader": 1919, "trip": 1920, "automobile": 1921, "carries": 1922, "discusing": 1923, "crossing": 1924, "apples": 1925, "lotion": 1926, "dummy": 1927, "collection": 1928, "gymnast": 1929, "octopus": 1930, "re": 1931, "audio": 1932, "morning": 1933, "happening": 1934, "single": 1935, "jersey": 1936, "tops": 1937, "heat": 1938, "lined": 1939, "album": 1940, "phil": 1941, "topics": 1942, "electric": 1943, "viewer": 1944, "lap": 1945, "message": 1946, "expensive": 1947, "diving": 1948, "santa": 1949, "etc": 1950, "substance": 1951, "pig": 1952, "shelf": 1953, "results": 1954, "second": 1955, "alone": 1956, "referee": 1957, "exploring": 1958, "skirt": 1959, "chops": 1960, "shouting": 1961, "lay": 1962, "beans": 1963, "chases": 1964, "resort": 1965, "camp": 1966, "muppets": 1967, "yells": 1968, "carriage": 1969, "study": 1970, "rally": 1971, "tell": 1972, "brand": 1973, "drawn": 1974, "brushing": 1975, "womans": 1976, ")": 1977, "blouse": 1978, "worker": 1979, "acts": 1980, "edge": 1981, "kick": 1982, "version": 1983, "pro": 1984, "enemy": 1985, "microphones": 1986, "kite": 1987, "mechanic": 1988, "rubik": 1989, "bubble": 1990, "swings": 1991, "jackson": 1992, "farm": 1993, "boiled": 1994, "politician": 1995, "historical": 1996, "order": 1997, "younger": 1998, "heated": 1999, "rabbit": 2000, "burning": 2001, "blood": 2002, "save": 2003, "bottles": 2004, "situation": 2005, "blows": 2006, "participating": 2007, "screams": 2008, "countries": 2009, "bags": 2010, "finishing": 2011, "trolley": 2012, "leading": 2013, "suv": 2014, "engineering": 2015, "swing": 2016, "romantic": 2017, "crashing": 2018, "shirtless": 2019, "noise": 2020, "hug": 2021, "terror": 2022, "whales": 2023, "cold": 2024, "gta": 2025, "campaign": 2026, "numbers": 2027, "drunk": 2028, "comic": 2029, "humorous": 2030, "wolves": 2031, "tattoo": 2032, "robots": 2033, "radio": 2034, "bathtub": 2035, "gordon": 2036, "doors": 2037, "pretending": 2038, "shaped": 2039, "title": 2040, "vest": 2041, "likes": 2042, "lamb": 2043, "wide": 2044, "generated": 2045, "know": 2046, "gas": 2047, "pitcher": 2048, "spectators": 2049, "wok": 2050, "sandwich": 2051, "university": 2052, "facing": 2053, "advice": 2054, "polo": 2055, "violin": 2056, "shania": 2057, "twain": 2058, "bug": 2059, "nba": 2060, "robin": 2061, "counting": 2062, "xbox": 2063, "cards": 2064, "european": 2065, "insidious": 2066, "edward": 2067, "pie": 2068, "customer": 2069, "chatting": 2070, "reel": 2071, "crime": 2072, "smile": 2073, "joke": 2074, "contains": 2075, "bunny": 2076, "attacked": 2077, "entertaining": 2078, "learn": 2079, "serious": 2080, "far": 2081, "progress": 2082, "season": 2083, "winner": 2084, "thick": 2085, "spatula": 2086, "tables": 2087, "flipping": 2088, "replay": 2089, "position": 2090, "weights": 2091, "toilet": 2092, "flows": 2093, "whale": 2094, "photoshop": 2095, "slides": 2096, "pigs": 2097, "pit": 2098, "plates": 2099, "walls": 2100, "curry": 2101, "created": 2102, "india": 2103, "fallon": 2104, "explaning": 2105, "soap": 2106, "puzzle": 2107, "cafe": 2108, "gathering": 2109, "matter": 2110, "robert": 2111, "windows": 2112, "china": 2113, "feed": 2114, "festival": 2115, "beef": 2116, "keep": 2117, "starring": 2118, "asked": 2119, "designs": 2120, "surrounding": 2121, "nurse": 2122, "receiving": 2123, "umbrella": 2124, "rushing": 2125, "weapon": 2126, "conducting": 2127, "performer": 2128, "ginger": 2129, "attached": 2130, "kinds": 2131, "national": 2132, "simple": 2133, "teeth": 2134, "chat": 2135, "bane": 2136, "ballet": 2137, "drugs": 2138, "opinion": 2139, "marching": 2140, "spraying": 2141, "heroes": 2142, "formula": 2143, "pipe": 2144, "paddle": 2145, "exciting": 2146, "turtle": 2147, "location": 2148, "feeds": 2149, "god": 2150, "age": 2151, "korean": 2152, "backwards": 2153, "helps": 2154, "dj": 2155, "career": 2156, "bit": 2157, "complex": 2158, "seasoning": 2159, "cheetah": 2160, "dicaprio": 2161, "seats": 2162, "rider": 2163, "importance": 2164, "demonstrate": 2165, "professor": 2166, "names": 2167, "quality": 2168, "photograph": 2169, "commentates": 2170, "hurt": 2171, "bears": 2172, "cries": 2173, "leader": 2174, "stock": 2175, "upset": 2176, "medicine": 2177, "improvement": 2178, "engaged": 2179, "africa": 2180, "pumpkin": 2181, "easy": 2182, "futuristic": 2183, "doesn": 2184, "exterior": 2185, "mens": 2186, "handle": 2187, "addressing": 2188, "recent": 2189, "could": 2190, "ancient": 2191, "sleep": 2192, "silent": 2193, "bake": 2194, "rose": 2195, "tires": 2196, "demonstrated": 2197, "any": 2198, "boards": 2199, "wooded": 2200, "spot": 2201, "lifts": 2202, "swims": 2203, "ingredient": 2204, "humans": 2205, "state": 2206, "pull": 2207, "thinking": 2208, "airport": 2209, "provides": 2210, "comment": 2211, "storm": 2212, "may": 2213, "wire": 2214, "wind": 2215, "shower": 2216, "explosion": 2217, "stops": 2218, "acura": 2219, "parade": 2220, "jail": 2221, "strategy": 2222, "fighter": 2223, "wet": 2224, "jesus": 2225, "blindfolded": 2226, "ambulance": 2227, "selfies": 2228, "split": 2229, "elevator": 2230, "fridge": 2231, "scissors": 2232, "religious": 2233, "sketch": 2234, "barack": 2235, "hosts": 2236, "lawn": 2237, "nights": 2238, "don": 2239, "smaller": 2240, "whip": 2241, "ski": 2242, "crawling": 2243, "fries": 2244, "suddenly": 2245, "rapper": 2246, "inner": 2247, "chest": 2248, "zoo": 2249, "fabric": 2250, "wwe": 2251, "ceremony": 2252, "stares": 2253, "scenic": 2254, "shark": 2255, "record": 2256, "wig": 2257, "leave": 2258, "protest": 2259, "protesting": 2260, "paris": 2261, "six": 2262, "fires": 2263, "collecting": 2264, "universe": 2265, "answer": 2266, "dinosaurs": 2267, "turtles": 2268, "smith": 2269, "queen": 2270, "candidate": 2271, "george": 2272, "almost": 2273, "bean": 2274, "england": 2275, "doubles": 2276, "halloween": 2277, "kit": 2278, "streaming": 2279, "politicians": 2280, "bras": 2281, "relationship": 2282, "chase": 2283, "packing": 2284, "escape": 2285, "curved": 2286, "passenger": 2287, "told": 2288, "towel": 2289, "straight": 2290, "commentators": 2291, "auditorium": 2292, "sung": 2293, "experiences": 2294, "production": 2295, "sweet": 2296, "playground": 2297, "wires": 2298, "every": 2299, "upon": 2300, "audition": 2301, "teddy": 2302, "web": 2303, "beautifully": 2304, "furniture": 2305, "exotic": 2306, "lobster": 2307, "serves": 2308, "method": 2309, "now": 2310, "churros": 2311, "hilary": 2312, "farmer": 2313, "nose": 2314, "palm": 2315, "crab": 2316, "storage": 2317, "california": 2318, "victoria": 2319, "brad": 2320, "james": 2321, "pitt": 2322, "prawns": 2323, "desktop": 2324, "research": 2325, "teach": 2326, "started": 2327, "finds": 2328, "riders": 2329, "say": 2330, "interact": 2331, "plan": 2332, "larger": 2333, "punches": 2334, "downey": 2335, "v": 2336, "preforming": 2337, "electronics": 2338, "chips": 2339, "officers": 2340, "workout": 2341, "husband": 2342, "personal": 2343, "papers": 2344, "promoting": 2345, "trains": 2346, "isis": 2347, "maleficent": 2348, "fruits": 2349, "load": 2350, "civic": 2351, "later": 2352, "rate": 2353, "llama": 2354, "designed": 2355, "challenge": 2356, "glue": 2357, "addiction": 2358, "digging": 2359, "bloopers": 2360, "passes": 2361, "dunking": 2362, "shaving": 2363, "bowling": 2364, "bringing": 2365, "dolphin": 2366, "flame": 2367, "healthy": 2368, "born": 2369, "pad": 2370, "instructor": 2371, "shuttle": 2372, "shoe": 2373, "racers": 2374, "manner": 2375, "underwear": 2376, "coins": 2377, "treatment": 2378, "laboratory": 2379, "convention": 2380, "approaches": 2381, "ceiling": 2382, "winter": 2383, "roller": 2384, "involved": 2385, "airplanes": 2386, "skis": 2387, "amount": 2388, "functions": 2389, "support": 2390, "vines": 2391, "pile": 2392, "whole": 2393, "puppets": 2394, "jr": 2395, "voiceover": 2396, "dna": 2397, "nick": 2398, "pots": 2399, "concept": 2400, "leaving": 2401, "choir": 2402, "master": 2403, "promotional": 2404, "backyard": 2405, "again": 2406, "aerial": 2407, "tomatoes": 2408, "europe": 2409, "morgan": 2410, "shadow": 2411, "lacrosse": 2412, "sonic": 2413, "ramsey": 2414, "directions": 2415, "checks": 2416, "share": 2417, "happily": 2418, "programme": 2419, "calling": 2420, "member": 2421, "unique": 2422, "martial": 2423, "effect": 2424, "handling": 2425, "alcohol": 2426, "individuals": 2427, "gestures": 2428, "went": 2429, "scientific": 2430, "plans": 2431, "mechanical": 2432, "below": 2433, "houses": 2434, "themed": 2435, "fighters": 2436, "ponies": 2437, "flashes": 2438, "narration": 2439, "size": 2440, "poster": 2441, "closing": 2442, "goals": 2443, "feud": 2444, "classical": 2445, "rear": 2446, "muddy": 2447, "womens": 2448, "captured": 2449, "ears": 2450, "follow": 2451, "honey": 2452, "panning": 2453, "golfer": 2454, "areas": 2455, "branch": 2456, "security": 2457, "brick": 2458, "wrap": 2459, "north": 2460, "marriage": 2461, "nfl": 2462, "hurdles": 2463, "answers": 2464, "saw": 2465, "specific": 2466, "touches": 2467, "atoms": 2468, "peeling": 2469, "cub": 2470, "elephants": 2471, "phones": 2472, "racetrack": 2473, "depicted": 2474, "murder": 2475, "evidence": 2476, "sheets": 2477, "think": 2478, "console": 2479, "passionately": 2480, "leads": 2481, "buying": 2482, "enter": 2483, "application": 2484, "reaches": 2485, "said": 2486, "headed": 2487, "jackets": 2488, "machines": 2489, "becomes": 2490, "industry": 2491, "subtitles": 2492, "sick": 2493, "lush": 2494, "animations": 2495, "superheroes": 2496, "growing": 2497, "rubber": 2498, "techno": 2499, "rush": 2500, "jogging": 2501, "warehouse": 2502, "blazer": 2503, "difference": 2504, "double": 2505, "pant": 2506, "castle": 2507, "facts": 2508, "drawings": 2509, "1": 2510, "facebook": 2511, "rubs": 2512, "olive": 2513, "keeps": 2514, "rubix": 2515, "editing": 2516, "london": 2517, "crocodile": 2518, "scream": 2519, "prison": 2520, "range": 2521, "wrecking": 2522, "cnbc": 2523, "jogger": 2524, "antelope": 2525, "judged": 2526, "boil": 2527, "presents": 2528, "chopsticks": 2529, "feelings": 2530, "settings": 2531, "minnie": 2532, "instructing": 2533, "travels": 2534, "strong": 2535, "years": 2536, "seriously": 2537, "push": 2538, "soft": 2539, "tasty": 2540, "required": 2541, "figurines": 2542, "shiny": 2543, "processor": 2544, "extra": 2545, "dirty": 2546, "lead": 2547, "pulled": 2548, "surfer": 2549, "filling": 2550, "noodle": 2551, "drug": 2552, "rotating": 2553, "aircraft": 2554, "row": 2555, "norton": 2556, "rihanna": 2557, "lingerie": 2558, "itunes": 2559, "titanic": 2560, "terminator": 2561, "appearing": 2562, "finishes": 2563, "listing": 2564, "shades": 2565, "awesome": 2566, "televised": 2567, "mustache": 2568, "better": 2569, "witch": 2570, "helmets": 2571, "packed": 2572, "viewing": 2573, "adventure": 2574, "mad": 2575, "available": 2576, "glitter": 2577, "bodies": 2578, "buy": 2579, "fairy": 2580, "drivers": 2581, "applause": 2582, "circuit": 2583, "onstage": 2584, "seven": 2585, "file": 2586, "showcase": 2587, "sales": 2588, "though": 2589, "skier": 2590, "chili": 2591, "baked": 2592, "crackers": 2593, "subway": 2594, "applied": 2595, "practices": 2596, "newborn": 2597, "opinions": 2598, "thin": 2599, "sandy": 2600, "biker": 2601, "physical": 2602, "germany": 2603, "deer": 2604, "banana": 2605, "blanket": 2606, "minion": 2607, "stew": 2608, "joking": 2609, "choice": 2610, "society": 2611, "lincoln": 2612, "vote": 2613, "haunted": 2614, "steve": 2615, "final": 2616, "chop": 2617, "telephone": 2618, "extract": 2619, "drying": 2620, "visual": 2621, "dash": 2622, "runner": 2623, "needs": 2624, "local": 2625, "hearing": 2626, "bloomberg": 2627, "enjoys": 2628, "employee": 2629, "lakes": 2630, "begin": 2631, "drifting": 2632, "incredible": 2633, "instructs": 2634, "butterfly": 2635, "lecturing": 2636, "smell": 2637, "gown": 2638, "inhome": 2639, "heroin": 2640, "presenter": 2641, "stunt": 2642, "tackled": 2643, "traditional": 2644, "sale": 2645, "celebration": 2646, "creation": 2647, "girlfriend": 2648, "coloring": 2649, "finally": 2650, "ends": 2651, "argue": 2652, "dropping": 2653, "beast": 2654, "typing": 2655, "ups": 2656, "quick": 2657, "east": 2658, "sharp": 2659, "mice": 2660, "aeroplane": 2661, "donuts": 2662, "finding": 2663, "within": 2664, "designer": 2665, "terrorism": 2666, "soda": 2667, "republican": 2668, "spring": 2669, "adele": 2670, "wii": 2671, "amateur": 2672, "baskets": 2673, "tropical": 2674, "steak": 2675, "burger": 2676, "gymnasium": 2677, "whiskey": 2678, "canyon": 2679, "tug": 2680, "perspective": 2681, "culture": 2682, "cloths": 2683, "offers": 2684, "proper": 2685, "tech": 2686, "miss": 2687, "incident": 2688, "patch": 2689, "6": 2690, "follows": 2691, "crown": 2692, "reaching": 2693, "glowing": 2694, "decorated": 2695, "promo": 2696, "comical": 2697, "selling": 2698, "boyfriend": 2699, "length": 2700, "trap": 2701, "walkthrough": 2702, "prey": 2703, "wilderness": 2704, "contents": 2705, "receives": 2706, "walked": 2707, "kreuger": 2708, "electrical": 2709, "took": 2710, "kimmel": 2711, "pain": 2712, "llamas": 2713, "accidents": 2714, "bull": 2715, "opposite": 2716, "meals": 2717, "cookery": 2718, "compared": 2719, "zombie": 2720, "rough": 2721, "terrorist": 2722, "lists": 2723, "terrain": 2724, "philippines": 2725, "expressing": 2726, "packaging": 2727, "tune": 2728, "miniature": 2729, "rest": 2730, "hunger": 2731, "scottish": 2732, "sat": 2733, "skill": 2734, "remote": 2735, "waters": 2736, "williams": 2737, "cereal": 2738, "bounces": 2739, "rat": 2740, "ideas": 2741, "syria": 2742, "nervous": 2743, "newscast": 2744, "advertised": 2745, "attention": 2746, "romance": 2747, "vacation": 2748, "jordan": 2749, "crowds": 2750, "photographer": 2751, "bars": 2752, "google": 2753, "burgers": 2754, "tackle": 2755, "brings": 2756, "direction": 2757, "fit": 2758, "stomach": 2759, "sewing": 2760, "humpty": 2761, "audi": 2762, "gate": 2763, "interacts": 2764, "skyline": 2765, "abc": 2766, "feeling": 2767, "thru": 2768, "dryer": 2769, "relaxing": 2770, "spectacles": 2771, "beats": 2772, "sitcom": 2773, "deck": 2774, "lies": 2775, "cubes": 2776, "drama": 2777, "detailed": 2778, "content": 2779, "quite": 2780, "experiments": 2781, "laid": 2782, "excitedly": 2783, "attire": 2784, "coin": 2785, "nine": 2786, "vintage": 2787, "nt": 2788, "matters": 2789, "raising": 2790, "dropped": 2791, "combing": 2792, "quarterback": 2793, "alpacas": 2794, "session": 2795, "infant": 2796, "knocked": 2797, "drum": 2798, "pressure": 2799, "sedan": 2800, "soul": 2801, "jam": 2802, "jewish": 2803, "umbrellas": 2804, "tourists": 2805, "introduced": 2806, "measuring": 2807, "chilli": 2808, "washes": 2809, "dangerous": 2810, "hanks": 2811, "halo": 2812, "narrated": 2813, "finished": 2814, "surprised": 2815, "spreading": 2816, "bay": 2817, "mission": 2818, "al": 2819, "spaghetti": 2820, "cable": 2821, "corn": 2822, "chicago": 2823, "chip": 2824, "beam": 2825, "foul": 2826, "gameshow": 2827, "wonderful": 2828, "herbs": 2829, "hunt": 2830, "feel": 2831, "download": 2832, "stretches": 2833, "basil": 2834, "complaining": 2835, "bikinis": 2836, "international": 2837, "cheek": 2838, "setup": 2839, "webcam": 2840, "straw": 2841, "gymnastic": 2842, "happens": 2843, "battery": 2844, "families": 2845, "ear": 2846, "hurdle": 2847, "opened": 2848, "climb": 2849, "homemade": 2850, "angrily": 2851, "calls": 2852, "olympic": 2853, "heating": 2854, "revealing": 2855, "unboxing": 2856, "opera": 2857, "somewhere": 2858, "happened": 2859, "haircut": 2860, "appeared": 2861, "sliding": 2862, "ran": 2863, "cows": 2864, "lava": 2865, "freddie": 2866, "valley": 2867, "today": 2868, "ahead": 2869, "slams": 2870, "muscle": 2871, "trunk": 2872, "causes": 2873, "industrial": 2874, "reach": 2875, "stylish": 2876, "gave": 2877, "brightly": 2878, "floats": 2879, "neighborhood": 2880, "collared": 2881, "expert": 2882, "someones": 2883, "listed": 2884, "flow": 2885, "wheeled": 2886, "newspaper": 2887, "paints": 2888, "leaning": 2889, "cop": 2890, "pikachu": 2891, "challenging": 2892, "adjusting": 2893, "transmission": 2894, "fallen": 2895, "messages": 2896, "article": 2897, "speeding": 2898, "surgery": 2899, "reactions": 2900, "tattoos": 2901, "knight": 2902, "focus": 2903, "contact": 2904, "similar": 2905, "explodes": 2906, "chart": 2907, "ducks": 2908, "released": 2909, "opponents": 2910, "pistol": 2911, "breast": 2912, "snakes": 2913, "widow": 2914, "donkey": 2915, "australian": 2916, "holes": 2917, "posters": 2918, "pushed": 2919, "russian": 2920, "count": 2921, "raining": 2922, "strike": 2923, "luxury": 2924, "conan": 2925, "rings": 2926, "avril": 2927, "panda": 2928, "angelina": 2929, "climate": 2930, "sam": 2931, "log": 2932, "toddler": 2933, "notes": 2934, "cuddling": 2935, "comparing": 2936, "galaxy": 2937, "smartphone": 2938, "hangs": 2939, "confused": 2940, "pillow": 2941, "conducted": 2942, "brake": 2943, "guitarist": 2944, "directly": 2945, "electricity": 2946, "turned": 2947, "vision": 2948, "mermaid": 2949, "leaders": 2950, "wait": 2951, "landing": 2952, "kills": 2953, "hunting": 2954, "planes": 2955, "claps": 2956, "trouble": 2957, "suite": 2958, "faster": 2959, "concrete": 2960, "channels": 2961, "artists": 2962, "unusual": 2963, "tape": 2964, "hoodie": 2965, "violence": 2966, "major": 2967, "hindi": 2968, "buttons": 2969, "seem": 2970, "lives": 2971, "flames": 2972, "championship": 2973, "paw": 2974, "unison": 2975, "rises": 2976, "meets": 2977, "joy": 2978, "newscasters": 2979, "issue": 2980, "exploding": 2981, "funeral": 2982, "coast": 2983, "tourist": 2984, "sell": 2985, "itself": 2986, "whether": 2987, "obstacle": 2988, "artificial": 2989, "growth": 2990, "blind": 2991, "beds": 2992, "starfish": 2993, "mean": 2994, "groom": 2995, "spin": 2996, "spinach": 2997, "mark": 2998, "rubio": 2999, "alphabet": 3000, "force": 3001, "ships": 3002, "messing": 3003, "mug": 3004, "stripes": 3005, "cabbage": 3006, "reilly": 3007, "includes": 3008, "bikers": 3009, "cups": 3010, "skull": 3011, "dumpty": 3012, "coaster": 3013, "boss": 3014, "afraid": 3015, "grip": 3016, "gaga": 3017, "fashionable": 3018, "launch": 3019, "relay": 3020, "zooms": 3021, "missing": 3022, "operation": 3023, "trainer": 3024, "ugly": 3025, "practising": 3026, "styling": 3027, "reviewed": 3028, "chemical": 3029, "slope": 3030, "waits": 3031, "days": 3032, "injury": 3033, "lands": 3034, "government": 3035, "staircase": 3036, "messy": 3037, "choices": 3038, "detail": 3039, "technical": 3040, "mood": 3041, "sumo": 3042, "closes": 3043, "causing": 3044, "approach": 3045, "c": 3046, "explaing": 3047, "success": 3048, "muscular": 3049, "perry": 3050, "paved": 3051, "licking": 3052, "cbs": 3053, "vlog": 3054, "violet": 3055, "minister": 3056, "journalist": 3057, "emotional": 3058, "attempt": 3059, "naked": 3060, "aspects": 3061, "scored": 3062, "tying": 3063, "clap": 3064, "flute": 3065, "strip": 3066, "washed": 3067, "deal": 3068, "museum": 3069, "everything": 3070, "factor": 3071, "informational": 3072, "mind": 3073, "protesters": 3074, "hardwood": 3075, "dungeon": 3076, "ferret": 3077, "beige": 3078, "candle": 3079, "pluto": 3080, "carried": 3081, "carry": 3082, "repeatedly": 3083, "vedio": 3084, "ferrari": 3085, "hang": 3086, "announces": 3087, "zebra": 3088, "general": 3089, "birth": 3090, "cages": 3091, "situations": 3092, "seth": 3093, "upbeat": 3094, "10": 3095, "snail": 3096, "korea": 3097, "freeman": 3098, "porsche": 3099, "splashing": 3100, "urban": 3101, "smashing": 3102, "safe": 3103, "broth": 3104, "homeless": 3105, "cursive": 3106, "movements": 3107, "minaj": 3108, "portion": 3109, "angel": 3110, "icing": 3111, "espn": 3112, "epic": 3113, "leaf": 3114, "maggie": 3115, "satellite": 3116, "eagle": 3117, "pope": 3118, "bacon": 3119, "automotive": 3120, "co": 3121, "sweatshirt": 3122, "argument": 3123, "capture": 3124, "semi": 3125, "met": 3126, "forming": 3127, "ten": 3128, "dying": 3129, "calm": 3130, "backstage": 3131, "creates": 3132, "americans": 3133, "flavor": 3134, "reaction": 3135, "autographs": 3136, "daily": 3137, "manufacturing": 3138, "congress": 3139, "backup": 3140, "participants": 3141, "lifted": 3142, "possibly": 3143, "alley": 3144, "guests": 3145, "nicely": 3146, "circular": 3147, "destroying": 3148, "cooker": 3149, "writes": 3150, "prime": 3151, "economy": 3152, "solo": 3153, "drummer": 3154, "steam": 3155, "grabbing": 3156, "packet": 3157, "swimsuit": 3158, "introduce": 3159, "swimmers": 3160, "muslim": 3161, "worm": 3162, "crawls": 3163, "coversation": 3164, "procedure": 3165, "examining": 3166, "listens": 3167, "fuse": 3168, "ballerina": 3169, "scooping": 3170, "repairs": 3171, "fear": 3172, "headset": 3173, "knock": 3174, "itsy": 3175, "wakes": 3176, "native": 3177, "leans": 3178, "cabinets": 3179, "ryding": 3180, "tee": 3181, "judging": 3182, "goat": 3183, "sunscreen": 3184, "backs": 3185, "sleeved": 3186, "files": 3187, "roman": 3188, "usa": 3189, "pointed": 3190, "engaging": 3191, "boots": 3192, "sister": 3193, "shine": 3194, "trash": 3195, "fork": 3196, "species": 3197, "marco": 3198, "scroll": 3199, "matt": 3200, "butterflies": 3201, "misses": 3202, "jetsons": 3203, "recorder": 3204, "plot": 3205, "zone": 3206, "diamond": 3207, "jolie": 3208, "pointer": 3209, "cooler": 3210, "tornado": 3211, "due": 3212, "greece": 3213, "coke": 3214, "multi": 3215, "birthday": 3216, "abraham": 3217, "conversing": 3218, "scrolls": 3219, "handing": 3220, "gift": 3221, "socks": 3222, "resting": 3223, "fields": 3224, "grow": 3225, "fi": 3226, "complete": 3227, "junior": 3228, "brace": 3229, "pickup": 3230, "price": 3231, "separate": 3232, "spaceship": 3233, "early": 3234, "attending": 3235, "patio": 3236, "unknown": 3237, "already": 3238, "foil": 3239, "qualities": 3240, "texts": 3241, "worked": 3242, "surprise": 3243, "stats": 3244, "armed": 3245, "ribbon": 3246, "heard": 3247, "massive": 3248, "dives": 3249, "struggles": 3250, "shield": 3251, "oriental": 3252, "moment": 3253, "ask": 3254, "overhead": 3255, "post": 3256, "potential": 3257, "youth": 3258, "pops": 3259, "thanks": 3260, "arrows": 3261, "lettering": 3262, "passed": 3263, "racecar": 3264, "veg": 3265, "extremely": 3266, "lyric": 3267, "raises": 3268, "thanking": 3269, "info": 3270, "islands": 3271, "carpenter": 3272, "rod": 3273, "bitsy": 3274, "fuel": 3275, "oragami": 3276, "announcing": 3277, "karate": 3278, "broke": 3279, "pools": 3280, "blinking": 3281, "mold": 3282, "diffrent": 3283, "doh": 3284, "cooling": 3285, "coverage": 3286, "wax": 3287, "summer": 3288, "rooms": 3289, "d": 3290, "feels": 3291, "mystery": 3292, "2015": 3293, "fairies": 3294, "eastern": 3295, "pocket": 3296, "ladder": 3297, "blog": 3298, "highlighted": 3299, "private": 3300, "nipple": 3301, "firetruck": 3302, "click": 3303, "leno": 3304, "fingernails": 3305, "loudly": 3306, "mass": 3307, "teenager": 3308, "cash": 3309, "foam": 3310, "steep": 3311, "lavigne": 3312, "railway": 3313, "bell": 3314, "alibaba": 3315, "cd": 3316, "backflips": 3317, "japan": 3318, "rhino": 3319, "survival": 3320, "creek": 3321, "duet": 3322, "ferrets": 3323, "rats": 3324, "hurricane": 3325, "colonial": 3326, "dried": 3327, "sized": 3328, "accompanied": 3329, "overview": 3330, "warm": 3331, "dashboard": 3332, "timberlake": 3333, "sang": 3334, "bring": 3335, "delivers": 3336, "drew": 3337, "landscapes": 3338, "temperature": 3339, "medium": 3340, "spoof": 3341, "pattern": 3342, "zooming": 3343, "salon": 3344, "chemistry": 3345, "wrong": 3346, "tent": 3347, "icy": 3348, "confronting": 3349, "motorbikes": 3350, "higher": 3351, "financial": 3352, "underneath": 3353, "amusement": 3354, "shares": 3355, "ones": 3356, "cgi": 3357, "broccoli": 3358, "cookie": 3359, "dashing": 3360, "selection": 3361, "hairstyle": 3362, "checked": 3363, "designing": 3364, "slice": 3365, "analyst": 3366, "visit": 3367, "powers": 3368, "greens": 3369, "veggies": 3370, "thoughts": 3371, "chalkboard": 3372, "graphs": 3373, "offer": 3374, "cherokee": 3375, "handles": 3376, "shops": 3377, "wanting": 3378, "temple": 3379, "stories": 3380, "customers": 3381, "visible": 3382, "collage": 3383, "date": 3384, "patrol": 3385, "crashed": 3386, "pub": 3387, "taping": 3388, "stool": 3389, "peace": 3390, "spread": 3391, "peter": 3392, "voting": 3393, "alligator": 3394, "shallow": 3395, "swords": 3396, "gently": 3397, "blooper": 3398, "kneeling": 3399, "cameras": 3400, "drain": 3401, "kitchenthere": 3402, "hike": 3403, "volleying": 3404, "creepy": 3405, "motorcyclist": 3406, "lapse": 3407, "dig": 3408, "brother": 3409, "chinchilla": 3410, "blending": 3411, "liner": 3412, "positive": 3413, "animate": 3414, "squirrel": 3415, "necklace": 3416, "easter": 3417, "owls": 3418, "sleeveless": 3419, "jay": 3420, "spicy": 3421, "outer": 3422, "cry": 3423, "waitress": 3424, "100": 3425, "quotes": 3426, "trophy": 3427, "pump": 3428, "note": 3429, "tractor": 3430, "saved": 3431, "drone": 3432, "rodent": 3433, "biotechnology": 3434, "possible": 3435, "johnny": 3436, "frightened": 3437, "connected": 3438, "smashes": 3439, "watched": 3440, "cabinet": 3441, "roaming": 3442, "stretch": 3443, "impressive": 3444, "cheeks": 3445, "assembly": 3446, "key": 3447, "dumplings": 3448, "typical": 3449, "wish": 3450, "chemicals": 3451, "wheelchair": 3452, "siting": 3453, "generation": 3454, "containers": 3455, "shake": 3456, "looked": 3457, "buffalo": 3458, "cloudy": 3459, "visiting": 3460, "dancer": 3461, "tight": 3462, "gatsby": 3463, "greco": 3464, "those": 3465, "stopped": 3466, "fantasy": 3467, "dribbling": 3468, "bin": 3469, "protective": 3470, "thinks": 3471, "trade": 3472, "gentle": 3473, "heroine": 3474, "steering": 3475, "weaving": 3476, "league": 3477, "dealership": 3478, "idea": 3479, "happen": 3480, "surfs": 3481, "insects": 3482, "gone": 3483, "praying": 3484, "accessories": 3485, "ultron": 3486, "braids": 3487, "arrested": 3488, "recipes": 3489, "nest": 3490, "smelling": 3491, "marker": 3492, "builds": 3493, "coloured": 3494, "cardboard": 3495, "stanky": 3496, "knees": 3497, "evening": 3498, "plus": 3499, "masala": 3500, "arcade": 3501, "cakes": 3502, "lie": 3503, "basic": 3504, "frames": 3505, "shocked": 3506, "godzilla": 3507, "tanks": 3508, "fired": 3509, "pregnant": 3510, "apartment": 3511, "shift": 3512, "teaser": 3513, "even": 3514, "hairs": 3515, "bite": 3516, "anti": 3517, "neatly": 3518, "cruz": 3519, "located": 3520, "goods": 3521, "bolt": 3522, "preform": 3523, "index": 3524, "jetta": 3525, "crosses": 3526, "damon": 3527, "marvel": 3528, "2014": 3529, "fifa": 3530, "spoons": 3531, "confederate": 3532, "ken": 3533, "crisis": 3534, "throughout": 3535, "candles": 3536, "damage": 3537, "former": 3538, "paul": 3539, "warrior": 3540, "outline": 3541, "cleans": 3542, "submarine": 3543, "torch": 3544, "fifth": 3545, "pastry": 3546, "umpire": 3547, "beets": 3548, "fur": 3549, "oxygen": 3550, "tofu": 3551, "fixes": 3552, "via": 3553, "oval": 3554, "fails": 3555, "armor": 3556, "heels": 3557, "greenery": 3558, "led": 3559, "cops": 3560, "confidence": 3561, "stopping": 3562, "8": 3563, "injuries": 3564, "greeting": 3565, "recap": 3566, "trek": 3567, "accomplishments": 3568, "movement": 3569, "roast": 3570, "snack": 3571, "changed": 3572, "statistics": 3573, "partner": 3574, "taught": 3575, "ray": 3576, "fine": 3577, "staff": 3578, "katy": 3579, "sailing": 3580, "cracking": 3581, "balance": 3582, "stepping": 3583, "march": 3584, "known": 3585, "differences": 3586, "atmosphere": 3587, "operate": 3588, "teachers": 3589, "amongst": 3590, "float": 3591, "forehead": 3592, "struggling": 3593, "supermarket": 3594, "picked": 3595, "wraps": 3596, "bank": 3597, "loose": 3598, "tale": 3599, "gang": 3600, "viewed": 3601, "puddle": 3602, "tooth": 3603, "sync": 3604, "memories": 3605, "response": 3606, "coats": 3607, "jean": 3608, "fitting": 3609, "elections": 3610, "mexico": 3611, "stephen": 3612, "encouraging": 3613, "join": 3614, "enormous": 3615, "spots": 3616, "massage": 3617, "examines": 3618, "brief": 3619, "gangs": 3620, "sprays": 3621, "sprinkles": 3622, "ash": 3623, "photoes": 3624, "abilities": 3625, "volkswagon": 3626, "f1": 3627, "statues": 3628, "wiping": 3629, "upside": 3630, "pour": 3631, "grapple": 3632, "acoustic": 3633, "present": 3634, "dialogue": 3635, "mascara": 3636, "maher": 3637, "springs": 3638, "strips": 3639, "ties": 3640, "robe": 3641, "lovely": 3642, "doorway": 3643, "mojo": 3644, "carey": 3645, "squeezing": 3646, "disease": 3647, "cells": 3648, "devices": 3649, "madden": 3650, "revolver": 3651, "carton": 3652, "switch": 3653, "analysis": 3654, "peppers": 3655, "frosting": 3656, "rolled": 3657, "railroad": 3658, "pin": 3659, "snowman": 3660, "donut": 3661, "balloons": 3662, "arranged": 3663, "fluffy": 3664, "virus": 3665, "revolution": 3666, "law": 3667, "barking": 3668, "lightning": 3669, "wrist": 3670, "excitement": 3671, "hamsters": 3672, "decorations": 3673, "guards": 3674, "logs": 3675, "mascot": 3676, "driveway": 3677, "mutant": 3678, "pumpkins": 3679, "strainer": 3680, "farting": 3681, "jaws": 3682, "prawn": 3683, "personality": 3684, "beatles": 3685, "active": 3686, "stages": 3687, "troop": 3688, "goalie": 3689, "speakers": 3690, "dragonball": 3691, "link": 3692, "dots": 3693, "provided": 3694, "e": 3695, "cause": 3696, "pads": 3697, "brakes": 3698, "conditions": 3699, "personnel": 3700, "treat": 3701, "derby": 3702, "porch": 3703, "trapped": 3704, "ability": 3705, "development": 3706, "scientists": 3707, "pretends": 3708, "shoulders": 3709, "rescue": 3710, "bullet": 3711, "showcases": 3712, "walkway": 3713, "loading": 3714, "fashions": 3715, "participate": 3716, "straps": 3717, "muscles": 3718, "battlefield": 3719, "scoops": 3720, "ed": 3721, "elvis": 3722, "pitch": 3723, "seating": 3724, "waking": 3725, "wildlife": 3726, "advertises": 3727, "disaster": 3728, "engineers": 3729, "sample": 3730, "sticker": 3731, "spending": 3732, "wwf": 3733, "cupboard": 3734, "balding": 3735, "cost": 3736, "sight": 3737, "caster": 3738, "motorcyclists": 3739, "olympics": 3740, "rapidly": 3741, "r": 3742, "drag": 3743, "windy": 3744, "once": 3745, "stills": 3746, "competitors": 3747, "pond": 3748, "dock": 3749, "annoying": 3750, "pretend": 3751, "palace": 3752, "pressing": 3753, "express": 3754, "hello": 3755, "rectangular": 3756, "gentlemen": 3757, "died": 3758, "laminate": 3759, "fps": 3760, "chicks": 3761, "rise": 3762, "choreographed": 3763, "showroom": 3764, "arranging": 3765, "edit": 3766, "threw": 3767, "per": 3768, "lovers": 3769, "jobs": 3770, "axe": 3771, "sniffing": 3772, "avatar": 3773, "booth": 3774, "automatic": 3775, "helicopters": 3776, "heading": 3777, "aid": 3778, "height": 3779, "tough": 3780, "crabs": 3781, "navigating": 3782, "expressions": 3783, "aisle": 3784, "racer": 3785, "macfarlane": 3786, "handsome": 3787, "sharks": 3788, "islamic": 3789, "candidates": 3790, "ingrediants": 3791, "kart": 3792, "em": 3793, "ads": 3794, "ceo": 3795, "original": 3796, "u": 3797, "missed": 3798, "sandwiches": 3799, "controls": 3800, "newsroom": 3801, "boxer": 3802, "aiming": 3803, "gmc": 3804, "advertisment": 3805, "chanting": 3806, "eyeglasses": 3807, "symbol": 3808, "awkward": 3809, "khan": 3810, "insides": 3811, "actresses": 3812, "chain": 3813, "mounted": 3814, "peppa": 3815, "snap": 3816, "drawers": 3817, "mongoose": 3818, "lettuce": 3819, "bones": 3820, "talents": 3821, "screw": 3822, "extensions": 3823, "graffiti": 3824, "condom": 3825, "texas": 3826, "curling": 3827, "drought": 3828, "volcano": 3829, "countryside": 3830, "either": 3831, "horrible": 3832, "usage": 3833, "choose": 3834, "nestea": 3835, "linux": 3836, "jesse": 3837, "bought": 3838, "paintings": 3839, "comparison": 3840, "loss": 3841, "simulation": 3842, "interestingly": 3843, "wipes": 3844, "auditions": 3845, "condition": 3846, "hide": 3847, "advertise": 3848, "crushing": 3849, "m": 3850, "cleaned": 3851, "custom": 3852, "helped": 3853, "period": 3854, "zoom": 3855, "flashlight": 3856, "receipe": 3857, "powerful": 3858, "thought": 3859, "probably": 3860, "skateboard": 3861, "lessons": 3862, "luggage": 3863, "eaten": 3864, "cafeteria": 3865, "sexual": 3866, "lower": 3867, "confronts": 3868, "gorgeous": 3869, "spice": 3870, "flood": 3871, "workshop": 3872, "supplies": 3873, "l": 3874, "engage": 3875, "global": 3876, "balloon": 3877, "bio": 3878, "continues": 3879, "grandmother": 3880, "presses": 3881, "boxers": 3882, "expresses": 3883, "trim": 3884, "elegant": 3885, "crossed": 3886, "masked": 3887, "australia": 3888, "applauding": 3889, "explore": 3890, "icons": 3891, "successful": 3892, "destination": 3893, "bone": 3894, "eyelashes": 3895, "curtain": 3896, "zoomed": 3897, "gesturing": 3898, "unwrapping": 3899, "grown": 3900, "leone": 3901, "examine": 3902, "robotic": 3903, "playfully": 3904, "mcdonalds": 3905, "cruise": 3906, "superheros": 3907, "communicate": 3908, "mock": 3909, "fully": 3910, "section": 3911, "task": 3912, "maintenance": 3913, "solar": 3914, "seeds": 3915, "rifle": 3916, "nearly": 3917, "microsoft": 3918, "capturing": 3919, "stones": 3920, "magnet": 3921, "disk": 3922, "agriculture": 3923, "squares": 3924, "9": 3925, "amatuer": 3926, "screenshot": 3927, "asia": 3928, "solution": 3929, "trekking": 3930, "browsing": 3931, "dragging": 3932, "nazi": 3933, "mature": 3934, "guard": 3935, "eggplant": 3936, "installation": 3937, "gay": 3938, "starbucks": 3939, "curtains": 3940, "forms": 3941, "devil": 3942, "numerous": 3943, "modelling": 3944, "become": 3945, "tip": 3946, "mega": 3947, "sparrow": 3948, "dumpster": 3949, "drop": 3950, "barn": 3951, "lasers": 3952, "trailers": 3953, "fed": 3954, "19": 3955, "heavily": 3956, "romancing": 3957, "earthquake": 3958, "unit": 3959, "letting": 3960, "eight": 3961, "sportscar": 3962, "assembles": 3963, "fair": 3964, "appearance": 3965, "transformer": 3966, "meatballs": 3967, "spooning": 3968, "msnbc": 3969, "nerd": 3970, "litter": 3971, "hedgehog": 3972, "wrapped": 3973, "mud": 3974, "paranormal": 3975, "release": 3976, "odd": 3977, "sailor": 3978, "valve": 3979, "trips": 3980, "churro": 3981, "footloose": 3982, "tourism": 3983, "benefit": 3984, "attributes": 3985, "melting": 3986, "vodka": 3987, "putin": 3988, "classes": 3989, "pancakes": 3990, "eyebrow": 3991, "beneath": 3992, "rome": 3993, "refrigerator": 3994, "draining": 3995, "dolphins": 3996, "surfers": 3997, "yosemite": 3998, "waterfalls": 3999, "larry": 4000, "nanotechnology": 4001, "priest": 4002, "acne": 4003, "snowflakes": 4004, "seasonings": 4005, "lets": 4006, "rays": 4007, "rendering": 4008, "code": 4009, "overweight": 4010, "teammates": 4011, "grassland": 4012, "fault": 4013, "sci": 4014, "mistakes": 4015, "complains": 4016, "mermaids": 4017, "corridor": 4018, "sleeve": 4019, "user": 4020, "aliens": 4021, "projects": 4022, "feedback": 4023, "completed": 4024, "7": 4025, "systems": 4026, "fellow": 4027, "components": 4028, "hillside": 4029, "rainy": 4030, "never": 4031, "loved": 4032, "supporting": 4033, "80": 4034, "operates": 4035, "able": 4036, "advanced": 4037, "steal": 4038, "surround": 4039, "enacted": 4040, "plain": 4041, "smells": 4042, "elder": 4043, "balancing": 4044, "transportation": 4045, "atop": 4046, "saucepan": 4047, "skinned": 4048, "crafting": 4049, "pram": 4050, "wresting": 4051, "medieval": 4052, "approaching": 4053, "shade": 4054, "nuclear": 4055, "destroys": 4056, "breasts": 4057, "scale": 4058, "maps": 4059, "floors": 4060, "vice": 4061, "adjusts": 4062, "programm": 4063, "harvey": 4064, "fitness": 4065, "versus": 4066, "backdrop": 4067, "becoming": 4068, "starch": 4069, "asparagus": 4070, "bruce": 4071, "dive": 4072, "instead": 4073, "journey": 4074, "moved": 4075, "railing": 4076, "keys": 4077, "congratulating": 4078, "pier": 4079, "bites": 4080, "tablets": 4081, "soaking": 4082, "reveals": 4083, "flys": 4084, "stable": 4085, "frustrated": 4086, "campus": 4087, "paying": 4088, "escaping": 4089, "jonas": 4090, "brought": 4091, "psychology": 4092, "admiring": 4093, "fantastic": 4094, "matching": 4095, "checkered": 4096, "integra": 4097, "70": 4098, "hour": 4099, "update": 4100, "prehistoric": 4101, "programs": 4102, "whistle": 4103, "entrance": 4104, "sending": 4105, "emotions": 4106, "loses": 4107, "rights": 4108, "mustard": 4109, "hd": 4110, "suspended": 4111, "assortment": 4112, "officials": 4113, "roles": 4114, "tawa": 4115, "windshield": 4116, "isle": 4117, "killer": 4118, "strength": 4119, "advantages": 4120, "understand": 4121, "fireplace": 4122, "harry": 4123, "outtakes": 4124, "wacky": 4125, "movieclips": 4126, "layer": 4127, "mariah": 4128, "difficult": 4129, "lens": 4130, "2016": 4131, "backpacks": 4132, "sparkly": 4133, "am": 4134, "tiles": 4135, "removed": 4136, "bare": 4137, "habitat": 4138, "interest": 4139, "must": 4140, "pus": 4141, "stating": 4142, "embracing": 4143, "always": 4144, "mushroom": 4145, "briefly": 4146, "paparazzi": 4147, "bump": 4148, "dump": 4149, "auditioning": 4150, "piggy": 4151, "raised": 4152, "harmony": 4153, "magnifying": 4154, "cupcakes": 4155, "threatens": 4156, "saves": 4157, "seminar": 4158, "christ": 4159, "ali": 4160, "gladiator": 4161, "friday": 4162, "furry": 4163, "blush": 4164, "mosquito": 4165, "barber": 4166, "farts": 4167, "johnnie": 4168, "mushrooms": 4169, "hiccups": 4170, "towels": 4171, "curves": 4172, "simmering": 4173, "crib": 4174, "argues": 4175, "paced": 4176, "access": 4177, "offering": 4178, "promote": 4179, "addresses": 4180, "samsung": 4181, "syncing": 4182, "farming": 4183, "cases": 4184, "knee": 4185, "losing": 4186, "bending": 4187, "rocking": 4188, "lover": 4189, "converse": 4190, "minutes": 4191, "involves": 4192, "dimensional": 4193, "styled": 4194, "blown": 4195, "useful": 4196, "rows": 4197, "wheeler": 4198, "patties": 4199, "might": 4200, "grilled": 4201, "powerpoint": 4202, "ordering": 4203, "orders": 4204, "casual": 4205, "positions": 4206, "boils": 4207, "enclosed": 4208, "hispanic": 4209, "decision": 4210, "coaches": 4211, "removal": 4212, "gadget": 4213, "france": 4214, "shelves": 4215, "terrific": 4216, "theatre": 4217, "rug": 4218, "vegetation": 4219, "produced": 4220, "subscription": 4221, "cheerleaders": 4222, "hairstyles": 4223, "pale": 4224, "conduct": 4225, "asleep": 4226, "filter": 4227, "yell": 4228, "blocky": 4229, "tin": 4230, "scrambled": 4231, "jim": 4232, "companies": 4233, "pages": 4234, "chalk": 4235, "kabadi": 4236, "magnificent": 4237, "studying": 4238, "revs": 4239, "retail": 4240, "homes": 4241, "burst": 4242, "mash": 4243, "%": 4244, "charts": 4245, "backing": 4246, "winners": 4247, "skies": 4248, "flavored": 4249, "western": 4250, "smash": 4251, "monitors": 4252, "dipping": 4253, "windmill": 4254, "photography": 4255, "worried": 4256, "purpose": 4257, "bartender": 4258, "eyeshadow": 4259, "sleeves": 4260, "ina": 4261, "papaya": 4262, "ducklings": 4263, "cadillac": 4264, "cameron": 4265, "90": 4266, "planets": 4267, "warriors": 4268, "tactics": 4269, "khaki": 4270, "rural": 4271, "scotland": 4272, "seemed": 4273, "rubiks": 4274, "clad": 4275, "cracks": 4276, "instrumental": 4277, "seconds": 4278, "horn": 4279, "coriander": 4280, "loving": 4281, "methods": 4282, "concerning": 4283, "trends": 4284, "wavy": 4285, "physics": 4286, "casting": 4287, "stay": 4288, "casino": 4289, "browns": 4290, "embrace": 4291, "monologue": 4292, "strapped": 4293, "bands": 4294, "napkin": 4295, "tried": 4296, "prisoners": 4297, "kneels": 4298, "announcement": 4299, "reverse": 4300, "micheal": 4301, "week": 4302, "shock": 4303, "gossip": 4304, "depp": 4305, "opposing": 4306, "magical": 4307, "sheep": 4308, "launching": 4309, "fountain": 4310, "bush": 4311, "teal": 4312, "unseen": 4313, "drawer": 4314, "rabbits": 4315, "examined": 4316, "biggest": 4317, "watchmojo": 4318, "restaurants": 4319, "transparent": 4320, "brien": 4321, "ma": 4322, "fill": 4323, "nicki": 4324, "tastes": 4325, "splashes": 4326, "collar": 4327, "printed": 4328, "hear": 4329, "wedges": 4330, "timer": 4331, "rising": 4332, "spectacular": 4333, "interaction": 4334, "knowledge": 4335, "shifting": 4336, "fashioned": 4337, "grapes": 4338, "explode": 4339, "punk": 4340, "terrorists": 4341, "handheld": 4342, "uv": 4343, "cosmetic": 4344, "jets": 4345, "rainforest": 4346, "fictional": 4347, "logos": 4348, "thrones": 4349, "kicked": 4350, "collision": 4351, "blackberry": 4352, "buzz": 4353, "flooded": 4354, "reception": 4355, "adventures": 4356, "luigi": 4357, "nepal": 4358, "lmfao": 4359, "fills": 4360, "eclipse": 4361, "ants": 4362, "2000": 4363, "ufo": 4364, "moore": 4365, "reggae": 4366, "pay": 4367, "marley": 4368, "9/11": 4369, "pendulum": 4370, "beads": 4371, "jars": 4372, "switches": 4373, "fondant": 4374, "bmw": 4375, "parsley": 4376, "cola": 4377, "limbo": 4378, "smurf": 4379, "nelly": 4380, "smooth": 4381, "bats": 4382, "simultaneously": 4383, "lantern": 4384, "greets": 4385, "rack": 4386, "singles": 4387, "origin": 4388, "charging": 4389, "grazing": 4390, "cabin": 4391, "bleachers": 4392, "tile": 4393, "danger": 4394, "guide": 4395, "welding": 4396, "warming": 4397, "isn": 4398, "carved": 4399, "carving": 4400, "complicated": 4401, "economic": 4402, "print": 4403, "dust": 4404, "cameraman": 4405, "apps": 4406, "youths": 4407, "eventually": 4408, "slightly": 4409, "searches": 4410, "duty": 4411, "diaper": 4412, "slamming": 4413, "max": 4414, "excellent": 4415, "spongbob": 4416, "grooming": 4417, "worn": 4418, "mexican": 4419, "engineer": 4420, "articles": 4421, "sends": 4422, "maker": 4423, "sour": 4424, "static": 4425, "minced": 4426, "crushed": 4427, "currently": 4428, "hell": 4429, "streams": 4430, "steven": 4431, "goggles": 4432, "spain": 4433, "camel": 4434, "egypt": 4435, "hysterically": 4436, "thread": 4437, "religion": 4438, "mama": 4439, "difficulties": 4440, "exits": 4441, "ever": 4442, "cables": 4443, "focused": 4444, "titles": 4445, "serial": 4446, "vw": 4447, "signal": 4448, "rail": 4449, "stainless": 4450, "rivers": 4451, "threatening": 4452, "brittany": 4453, "latin": 4454, "patients": 4455, "tears": 4456, "delivering": 4457, "employees": 4458, "broadcasting": 4459, "encourages": 4460, "oysters": 4461, "navigate": 4462, "pavement": 4463, "vinyl": 4464, "courtyard": 4465, "gangster": 4466, "counts": 4467, "consisting": 4468, "massaging": 4469, "shouts": 4470, "bombing": 4471, "diner": 4472, "th": 4473, "braid": 4474, "calculus": 4475, "compact": 4476, "roaring": 4477, "grain": 4478, "mechanics": 4479, "pitching": 4480, "abandoned": 4481, "expression": 4482, "bryant": 4483, "rubicks": 4484, "benz": 4485, "cher": 4486, "fist": 4487, "occurs": 4488, "upper": 4489, "artwork": 4490, "vehical": 4491, "alex": 4492, "jockey": 4493, "thier": 4494, "minions": 4495, "telecast": 4496, "hardly": 4497, "tamil": 4498, "spoken": 4499, "shredded": 4500, "onscreen": 4501, "policy": 4502, "businesses": 4503, "burns": 4504, "agent": 4505, "vigorously": 4506, "cort": 4507, "recently": 4508, "thriller": 4509, "darkness": 4510, "orbit": 4511, "grilling": 4512, "micro": 4513, "ledge": 4514, "proud": 4515, "escaped": 4516, "curve": 4517, "pbs": 4518, "commerical": 4519, "programming": 4520, "replace": 4521, "failed": 4522, "illustrates": 4523, "vampire": 4524, "closet": 4525, "apparently": 4526, "goats": 4527, "schools": 4528, "knives": 4529, "twitter": 4530, "leash": 4531, "nay": 4532, "tubes": 4533, "correct": 4534, "triangle": 4535, "write": 4536, "captions": 4537, "corners": 4538, "com": 4539, "true": 4540, "installs": 4541, "brothers": 4542, "hopping": 4543, "sticking": 4544, "rules": 4545, "degeneres": 4546, "edges": 4547, "tails": 4548, "kong": 4549, "prices": 4550, "yourself": 4551, "cape": 4552, "soy": 4553, "sprinkler": 4554, "whilst": 4555, "cone": 4556, "angle": 4557, "escapes": 4558, "provide": 4559, "decides": 4560, "handled": 4561, "suicide": 4562, "professionally": 4563, "texans": 4564, "batting": 4565, "russia": 4566, "doug": 4567, "utensils": 4568, "border": 4569, "platforms": 4570, "confront": 4571, "scorpion": 4572, "sketches": 4573, "learned": 4574, "kangaroo": 4575, "conspiracy": 4576, "howard": 4577, "avocado": 4578, "ramsay": 4579, "neon": 4580, "cucumber": 4581, "skeletons": 4582, "pillars": 4583, "assorted": 4584, "launcher": 4585, "misty": 4586, "wielding": 4587, "pens": 4588, "vacuum": 4589, "sausage": 4590, "elf": 4591, "prussia": 4592, "paintball": 4593, "busters": 4594, "pharmacy": 4595, "warcraft": 4596, "bombs": 4597, "wandering": 4598, "jelly": 4599, "transformers": 4600, "aorta": 4601, "pea": 4602, "immigration": 4603, "fart": 4604, "raft": 4605, "ebola": 4606, "shines": 4607, "convince": 4608, "adorable": 4609, "vietnam": 4610, "spirit": 4611, "rests": 4612, "emotionally": 4613, "infomercial": 4614, "awkwardly": 4615, "twirls": 4616, "startled": 4617, "sim": 4618, "prize": 4619, "barrymore": 4620, "mentions": 4621, "completing": 4622, "poor": 4623, "scaring": 4624, "energetic": 4625, "melon": 4626, "scrubs": 4627, "youtuber": 4628, "skiers": 4629, "chats": 4630, "colours": 4631, "glimpses": 4632, "countertop": 4633, "additional": 4634, "blast": 4635, "moped": 4636, "stovetop": 4637, "bloody": 4638, "chief": 4639, "didn": 4640, "pipes": 4641, "crispy": 4642, "leadership": 4643, "purchasing": 4644, "dubbed": 4645, "solider": 4646, "divers": 4647, "mod": 4648, "debating": 4649, "kylie": 4650, "ans": 4651, "de": 4652, "dumps": 4653, "believe": 4654, "alpaca": 4655, "dodging": 4656, "caption": 4657, "pitches": 4658, "wake": 4659, "uncooked": 4660, "marinate": 4661, "et": 4662, "status": 4663, "tested": 4664, "interested": 4665, "vocalist": 4666, "historic": 4667, "sleeps": 4668, "popping": 4669, "properties": 4670, "blackboard": 4671, "everywhere": 4672, "fuels": 4673, "naruto": 4674, "groceries": 4675, "strollers": 4676, "worth": 4677, "promotion": 4678, "verity": 4679, "sportscaster": 4680, "ca": 4681, "pulpit": 4682, "aerospace": 4683, "sections": 4684, "butt": 4685, "rockets": 4686, "structures": 4687, "roasting": 4688, "cosmetics": 4689, "skirts": 4690, "carbon": 4691, "mess": 4692, "profile": 4693, "practical": 4694, "twins": 4695, "owners": 4696, "outro": 4697, "proceeds": 4698, "gates": 4699, "avoid": 4700, "shell": 4701, "arranges": 4702, "sauces": 4703, "levels": 4704, "buffet": 4705, "experts": 4706, "chickens": 4707, "hampster": 4708, "smokes": 4709, "handbag": 4710, "zip": 4711, "mary": 4712, "synchronized": 4713, "bounce": 4714, "applauded": 4715, "hides": 4716, "revolving": 4717, "mirrors": 4718, "lee": 4719, "infowars": 4720, "according": 4721, "dangers": 4722, "recepie": 4723, "bigger": 4724, "solid": 4725, "sticky": 4726, "hay": 4727, "farmers": 4728, "sideways": 4729, "atv": 4730, "loop": 4731, "cannon": 4732, "mocking": 4733, "reacts": 4734, "ranting": 4735, "dummies": 4736, "attend": 4737, "destruction": 4738, "spy": 4739, "beaker": 4740, "crayons": 4741, "mathematics": 4742, "excerpt": 4743, "trivia": 4744, "decide": 4745, "stupid": 4746, "motorcyle": 4747, "rhyme": 4748, "princesses": 4749, "audiance": 4750, "tone": 4751, "murry": 4752, "wardrobe": 4753, "barbies": 4754, "shaves": 4755, "enclosure": 4756, "diced": 4757, "scuba": 4758, "example": 4759, "producing": 4760, "links": 4761, "fisher": 4762, "hamburgers": 4763, "versions": 4764, "diet": 4765, "chin": 4766, "paddles": 4767, "clippers": 4768, "bowel": 4769, "conductor": 4770, "directing": 4771, "strolling": 4772, "nerdy": 4773, "squeezes": 4774, "mid": 4775, "beiber": 4776, "subscribing": 4777, "explayning": 4778, "navigation": 4779, "mode": 4780, "hugged": 4781, "enthusiastically": 4782, "masks": 4783, "cycling": 4784, "closer": 4785, "genetic": 4786, "extreme": 4787, "mashed": 4788, "childs": 4789, "gamestop": 4790, "martian": 4791, "hindu": 4792, "nbc": 4793, "gears": 4794, "tackles": 4795, "affects": 4796, "winding": 4797, "loaf": 4798, "controlling": 4799, "shampoo": 4800, "happiness": 4801, "comically": 4802, "bionic": 4803, "panoramic": 4804, "accidentally": 4805, "anger": 4806, "dollars": 4807, "hours": 4808, "krabs": 4809, "explosions": 4810, "sportsman": 4811, "ramen": 4812, "elsa": 4813, "burrito": 4814, "foo": 4815, "superb": 4816, "mental": 4817, "emergency": 4818, "angels": 4819, "diary": 4820, "panels": 4821, "panorama": 4822, "twister": 4823, "lonely": 4824, "towed": 4825, "root": 4826, "coconut": 4827, "illustration": 4828, "pollution": 4829, "ms": 4830, "struck": 4831, "lorry": 4832, "worst": 4833, "reagan": 4834, "g": 4835, "grinder": 4836, "rover": 4837, "trophies": 4838, "mercedes": 4839, "spreads": 4840, "norway": 4841, "strikes": 4842, "tesla": 4843, "gospel": 4844, "rabbi": 4845, "jeremy": 4846, "chimpanzee": 4847, "beginner": 4848, "simpsons": 4849, "titans": 4850, "scandinavia": 4851, "posts": 4852, "mattress": 4853, "celery": 4854, "sushi": 4855, "dentist": 4856, "trumpet": 4857, "investing": 4858, "skydiving": 4859, "teasers": 4860, "kitty": 4861, "notebook": 4862, "terrible": 4863, "20": 4864, "attaches": 4865, "previews": 4866, "mtv": 4867, "mute": 4868, "purchased": 4869, "reveal": 4870, "un": 4871, "rectangle": 4872, "lane": 4873, "witnesses": 4874, "resolution": 4875, "glow": 4876, "sailors": 4877, "roberts": 4878, "interface": 4879, "olden": 4880, "cylinder": 4881, "vet": 4882, "spectacle": 4883, "columns": 4884, "replacing": 4885, "vegetarian": 4886, "legged": 4887, "safari": 4888, "sentences": 4889, "troops": 4890, "jenner": 4891, "downhill": 4892, "ballroom": 4893, "excercise": 4894, "seasoned": 4895, "shut": 4896, "detailing": 4897, "cinema": 4898, "punch": 4899, "tonight": 4900, "cliffs": 4901, "pride": 4902, "pace": 4903, "curb": 4904, "policeman": 4905, "skyrim": 4906, "flooding": 4907, "stranger": 4908, "lively": 4909, "anything": 4910, "waste": 4911, "stationary": 4912, "actually": 4913, "planning": 4914, "messes": 4915, "means": 4916, "pokes": 4917, "praises": 4918, "internal": 4919, "dusty": 4920, "hilly": 4921, "vanilla": 4922, "chance": 4923, "wanders": 4924, "focusing": 4925, "perfume": 4926, "diesel": 4927, "bun": 4928, "exiting": 4929, "meter": 4930, "technician": 4931, "suspect": 4932, "silk": 4933, "selected": 4934, "rhymes": 4935, "cartons": 4936, "enough": 4937, "maroon": 4938, "navy": 4939, "official": 4940, "insect": 4941, "tired": 4942, "hidden": 4943, "pathway": 4944, "austin": 4945, "billy": 4946, "junkies": 4947, "lecturer": 4948, "portrayed": 4949, "diagrams": 4950, "tuxedo": 4951, "bound": 4952, "surroundings": 4953, "steals": 4954, "returns": 4955, "whiteboard": 4956, "lectures": 4957, "shadows": 4958, "intex": 4959, "services": 4960, "sorts": 4961, "return": 4962, "italian": 4963, "stiring": 4964, "borat": 4965, "languages": 4966, "android": 4967, "doughnuts": 4968, "affected": 4969, "witness": 4970, "beaten": 4971, "grows": 4972, "reflect": 4973, "compiled": 4974, "navigates": 4975, "hash": 4976, "kung": 4977, "fu": 4978, "dealing": 4979, "wreck": 4980, "destroyed": 4981, "critique": 4982, "ropes": 4983, "ironman": 4984, "tony": 4985, "stark": 4986, "tosses": 4987, "legal": 4988, "loki": 4989, "spoke": 4990, "dribbles": 4991, "soil": 4992, "entered": 4993, "atta": 4994, "studies": 4995, "pros": 4996, "banjo": 4997, "dating": 4998, "hatchback": 4999, "horseback": 5000, "ruby": 5001, "cement": 5002, "moscow": 5003, "otehr": 5004, "headlights": 5005, "na": 5006, "pork": 5007, "projector": 5008, "pics": 5009, "garbage": 5010, "shave": 5011, "speedometer": 5012, "obstacles": 5013, "fixed": 5014, "rich": 5015, "remix": 5016, "sprouts": 5017, "boston": 5018, "dripping": 5019, "protect": 5020, "goodbye": 5021, "r&b": 5022, "toss": 5023, "highlighting": 5024, "seemingly": 5025, "ve": 5026, "symphony": 5027, "sentence": 5028, "shout": 5029, "dot": 5030, "relations": 5031, "grammar": 5032, "cities": 5033, "fade": 5034, "less": 5035, "improve": 5036, "symbols": 5037, "bees": 5038, "mostly": 5039, "lounge": 5040, "meadow": 5041, "compares": 5042, "trio": 5043, "ryan": 5044, "pups": 5045, "struts": 5046, "selecting": 5047, "rooftop": 5048, "received": 5049, "organization": 5050, "disasters": 5051, "pill": 5052, "freaks": 5053, "drove": 5054, "seductively": 5055, "rushes": 5056, "pup": 5057, "bolts": 5058, "basement": 5059, "marketing": 5060, "footballs": 5061, "arresting": 5062, "questioning": 5063, "colourful": 5064, "impress": 5065, "debt": 5066, "ark": 5067, "emotion": 5068, "format": 5069, "standup": 5070, "hitman": 5071, "breathtaking": 5072, "oatmeal": 5073, "shoreline": 5074, "balck": 5075, "jurassic": 5076, "dave": 5077, "foundation": 5078, "defeats": 5079, "candies": 5080, "bagel": 5081, "meant": 5082, "secssion": 5083, "panties": 5084, "rugrats": 5085, "interactive": 5086, "poles": 5087, "worms": 5088, "storyboard": 5089, "blogger": 5090, "walmart": 5091, "hitch": 5092, "raccoons": 5093, "shingles": 5094, "grinding": 5095, "criminal": 5096, "aggressive": 5097, "chuck": 5098, "joining": 5099, "cinnamon": 5100, "renovation": 5101, "awareness": 5102, "sled": 5103, "bubbling": 5104, "alcoholic": 5105, "completely": 5106, "bomber": 5107, "carp": 5108, "senator": 5109, "n": 5110, "backgrounds": 5111, "scissor": 5112, "ll": 5113, "supposed": 5114, "blindfold": 5115, "gangnam": 5116, "jennifer": 5117, "cyclist": 5118, "goblin": 5119, "portal": 5120, "danced": 5121, "ghostbusters": 5122, "pageant": 5123, "cranberry": 5124, "disco": 5125, "flint": 5126, "crawl": 5127, "gohan": 5128, "cornstarch": 5129, "splatoon": 5130, "charity": 5131, "hawaii": 5132, "community": 5133, "clock": 5134, "yolk": 5135, "rpg": 5136, "tongue": 5137, "coca": 5138, "intestines": 5139, "albino": 5140, "lock": 5141, "sock": 5142, "transplants": 5143, "allowing": 5144, "seafood": 5145, "leak": 5146, "needing": 5147, "squatting": 5148, "discovered": 5149, "memorial": 5150, "alongside": 5151, "banner": 5152, "shining": 5153, "trunks": 5154, "stood": 5155, "flirt": 5156, "closely": 5157, "sizes": 5158, "dan": 5159, "wildly": 5160, "destroy": 5161, "micky": 5162, "expectations": 5163, "andy": 5164, "closeup": 5165, "slapping": 5166, "carnival": 5167, "trendy": 5168, "crow": 5169, "anatomy": 5170, "sizzling": 5171, "cuisine": 5172, "spell": 5173, "skate": 5174, "modified": 5175, "dinning": 5176, "clearing": 5177, "mates": 5178, "ago": 5179, "shaggy": 5180, "30": 5181, "longer": 5182, "rotor": 5183, "parachutes": 5184, "hikers": 5185, "shed": 5186, "terms": 5187, "champion": 5188, "normal": 5189, "inspects": 5190, "ultimate": 5191, "underground": 5192, "houston": 5193, "traveled": 5194, "amounts": 5195, "overs": 5196, "tends": 5197, "assist": 5198, "visited": 5199, "daddy": 5200, "flexes": 5201, "motions": 5202, "minute": 5203, "burner": 5204, "chorus": 5205, "maths": 5206, "popsicle": 5207, "nothing": 5208, "continue": 5209, "hooded": 5210, "estate": 5211, "treasure": 5212, "dodge": 5213, "knot": 5214, "carrier": 5215, "fued": 5216, "examples": 5217, "loaded": 5218, "gluten": 5219, "loosing": 5220, "competitively": 5221, "gummy": 5222, "scares": 5223, "connecting": 5224, "specifications": 5225, "directors": 5226, "centered": 5227, "meowing": 5228, "engines": 5229, "eyelash": 5230, "tongs": 5231, "lose": 5232, "ii": 5233, "measures": 5234, "avenger": 5235, "tests": 5236, "valleys": 5237, "accepting": 5238, "rating": 5239, "belly": 5240, "teasing": 5241, "combining": 5242, "playingg": 5243, "cursing": 5244, "strategies": 5245, "invites": 5246, "settlers": 5247, "exchange": 5248, "rink": 5249, "taps": 5250, "mcdonald": 5251, "aggressively": 5252, "badmitton": 5253, "remedies": 5254, "hears": 5255, "roadside": 5256, "shit": 5257, "admirals": 5258, "galley": 5259, "include": 5260, "balances": 5261, "professionals": 5262, "torn": 5263, "soon": 5264, "backseat": 5265, "maintain": 5266, "villains": 5267, "sorting": 5268, "receiver": 5269, "returning": 5270, "lobby": 5271, "circus": 5272, "solved": 5273, "tasks": 5274, "unfolding": 5275, "playlist": 5276, "seasons": 5277, "lamp": 5278, "cg": 5279, "thank": 5280, "hawking": 5281, "civil": 5282, "caring": 5283, "360": 5284, "tapping": 5285, "israel": 5286, "digestive": 5287, "pinned": 5288, "blank": 5289, "forces": 5290, "jumpsuit": 5291, "transport": 5292, "roofs": 5293, "contraption": 5294, "tossing": 5295, "crystals": 5296, "tackling": 5297, "donations": 5298, "pilot": 5299, "inspirational": 5300, "dexter": 5301, "impressions": 5302, "dimly": 5303, "wrapping": 5304, "procession": 5305, "stabbing": 5306, "skinny": 5307, "freaking": 5308, "therapy": 5309, "supercar": 5310, "tide": 5311, "translates": 5312, "euro": 5313, "pictured": 5314, "sirens": 5315, "fades": 5316, "peels": 5317, "intimate": 5318, "receipee": 5319, "iran": 5320, "negative": 5321, "berry": 5322, "chess": 5323, "kilt": 5324, "1997": 5325, "attaching": 5326, "joint": 5327, "revenge": 5328, "harbor": 5329, "jumped": 5330, "earrings": 5331, "author": 5332, "editor": 5333, "rather": 5334, "broad": 5335, "slip": 5336, "batteries": 5337, "packs": 5338, "assemble": 5339, "tap": 5340, "cycles": 5341, "taxi": 5342, "label": 5343, "formation": 5344, "stripped": 5345, "menus": 5346, "warfare": 5347, "compound": 5348, "seashore": 5349, "offered": 5350, "sean": 5351, "surfboards": 5352, "gps": 5353, "bored": 5354, "fingernail": 5355, "wound": 5356, "carl": 5357, "hardware": 5358, "die": 5359, "nurses": 5360, "battles": 5361, "mole": 5362, "torso": 5363, "curvy": 5364, "reflects": 5365, "ketchup": 5366, "hose": 5367, "conducts": 5368, "reviewer": 5369, "moss": 5370, "wholesalers": 5371, "sawing": 5372, "tracing": 5373, "pound": 5374, "drills": 5375, "nuts": 5376, "captures": 5377, "cursor": 5378, "biology": 5379, "bet": 5380, "idol": 5381, "tuck": 5382, "grading": 5383, "creator": 5384, "adjust": 5385, "cocktails": 5386, "combined": 5387, "tvs": 5388, "easily": 5389, "prince": 5390, "hack": 5391, "hitter": 5392, "youngsters": 5393, "damaged": 5394, "till": 5395, "evolution": 5396, "july": 5397, "enthusiastic": 5398, "goldfish": 5399, "ipad": 5400, "doughnut": 5401, "freeway": 5402, "calories": 5403, "somone": 5404, "gnocchi": 5405, "outs": 5406, "nikki": 5407, "san": 5408, "ridden": 5409, "canoe": 5410, "bananas": 5411, "hi": 5412, "sanitary": 5413, "napkins": 5414, "saturday": 5415, "mount": 5416, "carves": 5417, "joker": 5418, "dashcam": 5419, "yemen": 5420, "nursery": 5421, "sinking": 5422, "tides": 5423, "phineas": 5424, "ferb": 5425, "fort": 5426, "observes": 5427, "parks": 5428, "nintendo": 5429, "fishermen": 5430, "wool": 5431, "wagon": 5432, "dvd": 5433, "soapy": 5434, "vomit": 5435, "belle": 5436, "simulator": 5437, "computerized": 5438, "funk": 5439, "cloud": 5440, "biking": 5441, "experiencing": 5442, "scout": 5443, "museums": 5444, "husky": 5445, "upright": 5446, "competitor": 5447, "burn": 5448, "stolen": 5449, "tokyo": 5450, "elmer": 5451, "fudd": 5452, "cherry": 5453, "crust": 5454, "woody": 5455, "flynt": 5456, "pornography": 5457, "clash": 5458, "clans": 5459, "wizard": 5460, "silencer": 5461, "missiles": 5462, "chevrolet": 5463, "simpson": 5464, "kobe": 5465, "backstreet": 5466, "racoons": 5467, "imperial": 5468, "tarp": 5469, "widget": 5470, "blessing": 5471, "stalks": 5472, "saucer": 5473, "lemons": 5474, "threat": 5475, "stall": 5476, "bradford": 5477, "sciences": 5478, "rapture": 5479, "flakes": 5480, "clones": 5481, "nokia": 5482, "obelisk": 5483, "tomb": 5484, "rc": 5485, "tracy": 5486, "vows": 5487, "businessman": 5488, "thermometer": 5489, "esat": 5490, "glider": 5491, "mane": 5492, "chilled": 5493, "sally": 5494, "spiders": 5495, "kale": 5496, "lightyear": 5497, "salmon": 5498, "meryl": 5499, "sperm": 5500, "rand": 5501, "snowmobile": 5502, "puree": 5503, "principle": 5504, "terrified": 5505, "explores": 5506, "welcome": 5507, "coaching": 5508, "coral": 5509, "multicolored": 5510, "beverage": 5511, "avoiding": 5512, "cotton": 5513, "scattered": 5514, "shared": 5515, "geese": 5516, "dramatically": 5517, "lining": 5518, "tunes": 5519, "goofy": 5520, "chewing": 5521, "shaping": 5522, "greet": 5523, "kissed": 5524, "illustrated": 5525, "figurine": 5526, "hairspray": 5527, "experiement": 5528, "midst": 5529, "betting": 5530, "composed": 5531, "hunters": 5532, "boarding": 5533, "announcers": 5534, "fluid": 5535, "muffler": 5536, "affect": 5537, "contain": 5538, "rappers": 5539, "observed": 5540, "ton": 5541, "elevated": 5542, "sprinting": 5543, "playful": 5544, "photographing": 5545, "cutscene": 5546, "ate": 5547, "processes": 5548, "disabled": 5549, "egyptian": 5550, "dragons": 5551, "gigantic": 5552, "visits": 5553, "ben": 5554, "portable": 5555, "forensic": 5556, "hips": 5557, "converted": 5558, "condiments": 5559, "cans": 5560, "shaved": 5561, "garb": 5562, "leaps": 5563, "resturant": 5564, "barcelona": 5565, "executive": 5566, "ea": 5567, "mma": 5568, "creative": 5569, "penne": 5570, "courage": 5571, "swiming": 5572, "applauds": 5573, "bulb": 5574, "rainbow": 5575, "gelatin": 5576, "vertical": 5577, "smashed": 5578, "arrives": 5579, "videotaping": 5580, "department": 5581, "violently": 5582, "chosen": 5583, "experimenting": 5584, "forests": 5585, "horizon": 5586, "podiums": 5587, "tyga": 5588, "skins": 5589, "seizures": 5590, "batmanton": 5591, "tab": 5592, "scooters": 5593, "streamer": 5594, "empire": 5595, "fiction": 5596, "imitating": 5597, "juices": 5598, "annoyed": 5599, "sniffs": 5600, "mclobster": 5601, "rapid": 5602, "impossible": 5603, "recommended": 5604, "twisting": 5605, "address": 5606, "constructed": 5607, "behavior": 5608, "addicts": 5609, "newly": 5610, "wiring": 5611, "selections": 5612, "couldn": 5613, "catcher": 5614, "mashing": 5615, "ward": 5616, "surgeon": 5617, "rubic": 5618, "handgun": 5619, "layout": 5620, "golfing": 5621, "competes": 5622, "scantily": 5623, "sounding": 5624, "actual": 5625, "saree": 5626, "disappears": 5627, "villian": 5628, "reddish": 5629, "bushes": 5630, "ominous": 5631, "holiday": 5632, "genie": 5633, "wit": 5634, "gary": 5635, "curls": 5636, "gerbil": 5637, "inserting": 5638, "lighted": 5639, "formulas": 5640, "conflict": 5641, "accused": 5642, "harvesting": 5643, "crop": 5644, "2d": 5645, "waist": 5646, "crystal": 5647, "highly": 5648, "casts": 5649, "photoshoot": 5650, "multiplayer": 5651, "folder": 5652, "pitched": 5653, "encounter": 5654, "strokes": 5655, "wishing": 5656, "downs": 5657, "rio": 5658, "tossed": 5659, "oddly": 5660, "theory": 5661, "autograph": 5662, "discovering": 5663, "feild": 5664, "powered": 5665, "blimp": 5666, "venue": 5667, "sneaks": 5668, "denim": 5669, "flees": 5670, "poorly": 5671, "whisking": 5672, "rotated": 5673, "degree": 5674, "rams": 5675, "lack": 5676, "beings": 5677, "scrambling": 5678, "prisoner": 5679, "respective": 5680, "veil": 5681, "stabbed": 5682, "pins": 5683, "medication": 5684, "flirts": 5685, "uneven": 5686, "assembled": 5687, "televison": 5688, "indians": 5689, "flights": 5690, "supply": 5691, "sent": 5692, "screeen": 5693, "rhianna": 5694, "promotes": 5695, "satirical": 5696, "aside": 5697, "diver": 5698, "stealing": 5699, "antique": 5700, "lace": 5701, "spit": 5702, "rains": 5703, "cord": 5704, "gifts": 5705, "hundreds": 5706, "rotates": 5707, "interiors": 5708, "ikea": 5709, "slicer": 5710, "catchy": 5711, "slapped": 5712, "doritos": 5713, "nude": 5714, "4th": 5715, "repaired": 5716, "reflecting": 5717, "felt": 5718, "scare": 5719, "phd": 5720, "horizontal": 5721, "framed": 5722, "controlled": 5723, "marinating": 5724, "camping": 5725, "replays": 5726, "bycycle": 5727, "communicating": 5728, "parent": 5729, "reflection": 5730, "unfolds": 5731, "conversations": 5732, "falcons": 5733, "rub": 5734, "rude": 5735, "rehearsing": 5736, "chose": 5737, "undressing": 5738, "platter": 5739, "values": 5740, "cauliflower": 5741, "crush": 5742, "claymation": 5743, "decoration": 5744, "protests": 5745, "bookcase": 5746, "hiv": 5747, "organs": 5748, "sucking": 5749, "appliance": 5750, "peers": 5751, "thumbs": 5752, "phenomenon": 5753, "descriptive": 5754, "gowns": 5755, "register": 5756, "laws": 5757, "theories": 5758, "drilling": 5759, "transfers": 5760, "richard": 5761, "manual": 5762, "users": 5763, "consequences": 5764, "correctly": 5765, "burnt": 5766, "freddies": 5767, "praise": 5768, "indiana": 5769, "hundred": 5770, "tweets": 5771, "pinocchio": 5772, "visuals": 5773, "overtaking": 5774, "tyres": 5775, "stickers": 5776, "stored": 5777, "titled": 5778, "successfully": 5779, "blends": 5780, "rips": 5781, "uk": 5782, "bricks": 5783, "branches": 5784, "sunsets": 5785, "beautician": 5786, "headlines": 5787, "processing": 5788, "raven": 5789, "dangling": 5790, "dumping": 5791, "filipino": 5792, "sped": 5793, "completes": 5794, "subjects": 5795, "watts": 5796, "total": 5797, "turquoise": 5798, "tyre": 5799, "cuddles": 5800, "arriving": 5801, "observing": 5802, "basketballs": 5803, "fro": 5804, "screencast": 5805, "ware": 5806, "sarah": 5807, "brittney": 5808, "bridges": 5809, "laughter": 5810, "hippo": 5811, "metro": 5812, "rims": 5813, "nation": 5814, "britain": 5815, "forklift": 5816, "miso": 5817, "stair": 5818, "paths": 5819, "safely": 5820, "brow": 5821, "screwdriver": 5822, "profession": 5823, "galloway": 5824, "leeks": 5825, "imagery": 5826, "depiction": 5827, "firework": 5828, "warns": 5829, "peak": 5830, "maybe": 5831, "overlooking": 5832, "cockpit": 5833, "phrases": 5834, "accross": 5835, "taped": 5836, "convertible": 5837, "secretary": 5838, "ronald": 5839, "throat": 5840, "slaps": 5841, "ditch": 5842, "unibrow": 5843, "2012": 5844, "weaves": 5845, "stores": 5846, "calmly": 5847, "sparkle": 5848, "effective": 5849, "technologies": 5850, "folk": 5851, "wanted": 5852, "kiddie": 5853, "garner": 5854, "reallity": 5855, "phillies": 5856, "melody": 5857, "mats": 5858, "decades": 5859, "academy": 5860, "minnesota": 5861, "blocking": 5862, "poking": 5863, "ymca": 5864, "unpacking": 5865, "suited": 5866, "softball": 5867, "digs": 5868, "illegal": 5869, "raod": 5870, "chimp": 5871, "earths": 5872, "discovery": 5873, "produce": 5874, "bart": 5875, "dodgeball": 5876, "penis": 5877, "skilled": 5878, "whisk": 5879, "integrated": 5880, "juicing": 5881, "molecules": 5882, "account": 5883, "tuna": 5884, "flexible": 5885, "bono": 5886, "central": 5887, "dental": 5888, "stitching": 5889, "drains": 5890, "hitler": 5891, "collider": 5892, "opportunity": 5893, "pedialyte": 5894, "raider": 5895, "riley": 5896, "streep": 5897, "ireland": 5898, "cinderella": 5899, "benghazi": 5900, "palying": 5901, "regular": 5902, "connect": 5903, "helpful": 5904, "pedestrians": 5905, "salesman": 5906, "grips": 5907, "floral": 5908, "informative": 5909, "select": 5910, "sunday": 5911, "advertisements": 5912, "bizarre": 5913, "elaborate": 5914, "fuzzy": 5915, "comfort": 5916, "polka": 5917, "alpha": 5918, "hearts": 5919, "sketching": 5920, "oprah": 5921, "pre": 5922, "warning": 5923, "mysterious": 5924, "frantically": 5925, "petals": 5926, "twirling": 5927, "measured": 5928, "capital": 5929, "wounded": 5930, "solutions": 5931, "vitamins": 5932, "jockeys": 5933, "jake": 5934, "scrolled": 5935, "posted": 5936, "frightening": 5937, "darkened": 5938, "skates": 5939, "113th": 5940, "honor": 5941, "aluminum": 5942, "mansion": 5943, "martin": 5944, "funnel": 5945, "gadgets": 5946, "parachute": 5947, "burned": 5948, "balcony": 5949, "garment": 5950, "mothers": 5951, "increase": 5952, "conditioning": 5953, "personalities": 5954, "caprio": 5955, "animatedly": 5956, "biting": 5957, "congratulated": 5958, "outfield": 5959, "gallery": 5960, "slender": 5961, "joins": 5962, "amidst": 5963, "facility": 5964, "fairly": 5965, "zumba": 5966, "nap": 5967, "filters": 5968, "rants": 5969, "knows": 5970, "seater": 5971, "philosophy": 5972, "patterns": 5973, "swirling": 5974, "spielberg": 5975, "agitated": 5976, "deals": 5977, "penalty": 5978, "thailand": 5979, "script": 5980, "horns": 5981, "strawberries": 5982, "targets": 5983, "necessary": 5984, "decorate": 5985, "divided": 5986, "server": 5987, "licks": 5988, "companion": 5989, "revving": 5990, "billboard": 5991, "blocked": 5992, "scanning": 5993, "picturesque": 5994, "edited": 5995, "guiding": 5996, "tow": 5997, "blob": 5998, "comics": 5999, "strangely": 6000, "junk": 6001, "yummy": 6002, "facilities": 6003, "canadian": 6004, "faucet": 6005, "dalmatian": 6006, "explanations": 6007, "logic": 6008, "notice": 6009, "developed": 6010, "zach": 6011, "kingdom": 6012, "scorpions": 6013, "alive": 6014, "casually": 6015, "bicycles": 6016, "thai": 6017, "marble": 6018, "chilly": 6019, "cowboys": 6020, "guitarists": 6021, "intensely": 6022, "emerges": 6023, "ninjas": 6024, "voiced": 6025, "rick": 6026, "cutter": 6027, "surrounds": 6028, "buggy": 6029, "assistant": 6030, "carpentry": 6031, "remodeling": 6032, "installed": 6033, "healthcare": 6034, "riffle": 6035, "grounds": 6036, "neat": 6037, "clown": 6038, "bits": 6039, "jamming": 6040, "mistake": 6041, "prep": 6042, "foliage": 6043, "picnic": 6044, "athletics": 6045, "aladdin": 6046, "thousand": 6047, "mention": 6048, "4k": 6049, "syrian": 6050, "w": 6051, "sportscenter": 6052, "kneel": 6053, "caucasian": 6054, "wingspan": 6055, "accompanies": 6056, "treats": 6057, "varying": 6058, "christie": 6059, "portuguese": 6060, "hurts": 6061, "bleeding": 6062, "parrots": 6063, "mannequins": 6064, "scanned": 6065, "hostage": 6066, "frustration": 6067, "statement": 6068, "trucking": 6069, "dense": 6070, "shrimps": 6071, "sauteed": 6072, "skater": 6073, "someine": 6074, "applications": 6075, "rare": 6076, "newest": 6077, "blasting": 6078, "duel": 6079, "sure": 6080, "aloud": 6081, "spelling": 6082, "barbecue": 6083, "smoothie": 6084, "rooster": 6085, "crate": 6086, "chooses": 6087, "peach": 6088, "recovery": 6089, "beauties": 6090, "galloping": 6091, "jerseys": 6092, "rugged": 6093, "upscale": 6094, "exhibition": 6095, "region": 6096, "flown": 6097, "partially": 6098, "universal": 6099, "swearing": 6100, "masters": 6101, "flesh": 6102, "ariel": 6103, "impact": 6104, "leap": 6105, "strutting": 6106, "bullets": 6107, "charge": 6108, "blade": 6109, "pairs": 6110, "collects": 6111, "parties": 6112, "cilantro": 6113, "dispenser": 6114, "common": 6115, "tweet": 6116, "#": 6117, "grumpy": 6118, "headband": 6119, "ribbons": 6120, "arabic": 6121, "checkers": 6122, "century": 6123, "separated": 6124, "earthquakes": 6125, "bios": 6126, "mate": 6127, "videotaped": 6128, "fitted": 6129, "teh": 6130, "rifles": 6131, "arrangements": 6132, "prior": 6133, "ink": 6134, "surfaces": 6135, "thumb": 6136, "besides": 6137, "bieber": 6138, "tmz": 6139, "entire": 6140, "reasons": 6141, "accents": 6142, "revealed": 6143, "majestic": 6144, "reenacting": 6145, "claims": 6146, "drags": 6147, "writer": 6148, "fact": 6149, "lil": 6150, "kim": 6151, "suggests": 6152, "photographers": 6153, "poem": 6154, "bee": 6155, "$": 6156, "thunderbird": 6157, "disciplinary": 6158, "result": 6159, "value": 6160, "equation": 6161, "formed": 6162, "nations": 6163, "plated": 6164, "sportscasters": 6165, "pov": 6166, "merkel": 6167, "marinade": 6168, "switching": 6169, "organizing": 6170, "mesh": 6171, "overalls": 6172, "courts": 6173, "planned": 6174, "geography": 6175, "advantage": 6176, "samurai": 6177, "array": 6178, "stuffs": 6179, "advances": 6180, "county": 6181, "alot": 6182, "bends": 6183, "promoted": 6184, "lioness": 6185, "futbol": 6186, "iggy": 6187, "frisk": 6188, "duties": 6189, "cones": 6190, "scolds": 6191, "stare": 6192, "boobs": 6193, "align": 6194, "spirits": 6195, "gazelle": 6196, "dawson": 6197, "risk": 6198, "union": 6199, "aquariums": 6200, "tampoline": 6201, "b": 6202, "bills": 6203, "foosball": 6204, "webpage": 6205, "dragged": 6206, "canopy": 6207, "instagram": 6208, "raiders": 6209, "wise": 6210, "occuring": 6211, "splash": 6212, "grid": 6213, "suitcase": 6214, "greeted": 6215, "onlookers": 6216, "sinks": 6217, "comfortable": 6218, "tupperware": 6219, "flattening": 6220, "especially": 6221, "marinated": 6222, "jazz": 6223, "viral": 6224, "liquor": 6225, "celebrations": 6226, "passengers": 6227, "plains": 6228, "drowning": 6229, "mincraft": 6230, "collapses": 6231, "succession": 6232, "soilder": 6233, "depict": 6234, "relax": 6235, "dutch": 6236, "markets": 6237, "conveyor": 6238, "cookware": 6239, "aman": 6240, "strongly": 6241, "housing": 6242, "talented": 6243, "attractions": 6244, "moustache": 6245, "softly": 6246, "afro": 6247, "reggie": 6248, "underway": 6249, "caused": 6250, "chipmunks": 6251, "layup": 6252, "quit": 6253, "buses": 6254, "skimpy": 6255, "biscuit": 6256, "prank": 6257, "hbo": 6258, "dream": 6259, "asphalt": 6260, "bye": 6261, "raccoon": 6262, "intersection": 6263, "beyond": 6264, "snowden": 6265, "otter": 6266, "ornate": 6267, "diferent": 6268, "definition": 6269, "dispose": 6270, "taco": 6271, "requirements": 6272, "slopes": 6273, "journalists": 6274, "dosa": 6275, "peaceful": 6276, "peel": 6277, "glove": 6278, "parliament": 6279, "dribble": 6280, "lawyer": 6281, "launched": 6282, "industries": 6283, "kermit": 6284, "cia": 6285, "oblong": 6286, "ruins": 6287, "monument": 6288, "canal": 6289, "saxophone": 6290, "trapping": 6291, "traps": 6292, "whom": 6293, "continuing": 6294, "functionality": 6295, "tripod": 6296, "grenade": 6297, "diglett": 6298, "beech": 6299, "royal": 6300, "representation": 6301, "festivals": 6302, "ghosts": 6303, "reily": 6304, "sail": 6305, "mac": 6306, "previous": 6307, "avatars": 6308, "crude": 6309, "deers": 6310, "tricycle": 6311, "blend": 6312, "transitions": 6313, "sells": 6314, "mountaintop": 6315, "spitting": 6316, "soaked": 6317, "therapies": 6318, "cure": 6319, "nachos": 6320, "supermoon": 6321, "stockings": 6322, "poodle": 6323, "wrote": 6324, "ant": 6325, "monk": 6326, "charges": 6327, "plaque": 6328, "category": 6329, "biotech": 6330, "roots": 6331, "usc": 6332, "epilepsy": 6333, "zucchini": 6334, "gluing": 6335, "300": 6336, "glory": 6337, "motherboard": 6338, "brave": 6339, "gladiators": 6340, "forrest": 6341, "extends": 6342, "2011": 6343, "miners": 6344, "profit": 6345, "clarkson": 6346, "avideo": 6347, "thief": 6348, "ridiculous": 6349, "mccain": 6350, "nano": 6351, "artistic": 6352, "buffy": 6353, "poachers": 6354, "rangers": 6355, "vases": 6356, "ignition": 6357, "primer": 6358, "squeeze": 6359, "lopez": 6360, "stairwell": 6361, "tentacles": 6362, "scooped": 6363, "barrel": 6364, "tnt": 6365, "fog": 6366, "bumps": 6367, "stems": 6368, "screeching": 6369, "wonders": 6370, "hashbrowns": 6371, "javelin": 6372, "separating": 6373, "lifter": 6374, "zealand": 6375, "muffin": 6376, "fringe": 6377, "nutella": 6378, "smoker": 6379, "beams": 6380, "lightening": 6381, "awning": 6382, "swallowing": 6383, "wagging": 6384, "mail": 6385, "fossil": 6386, "uphill": 6387, "sauteing": 6388, "shards": 6389, "addams": 6390, "option": 6391, "academic": 6392, "claude": 6393, "bowser": 6394, "plating": 6395, "shredders": 6396, "utopian": 6397, "confetti": 6398, "keytar": 6399, "palin": 6400, "pyramid": 6401, "newsweek": 6402, "immigrants": 6403, "equality": 6404, "compartment": 6405, "liberty": 6406, "excavator": 6407, "quantum": 6408, "lighthouse": 6409, "usain": 6410, "calamari": 6411, "binford": 6412, "starry": 6413, "speedly": 6414, "leaks": 6415, "transition": 6416, "brows": 6417, "scitentist": 6418, "vlogging": 6419, "wishes": 6420, "stretched": 6421, "tenis": 6422, "fourth": 6423, "congratulate": 6424, "medal": 6425, "goku": 6426, "graze": 6427, "consoling": 6428, "launches": 6429, "telugu": 6430, "brazil": 6431, "panicking": 6432, "peron": 6433, "backward": 6434, "inch": 6435, "bonfire": 6436, "goofing": 6437, "clicking": 6438, "perfectly": 6439, "daisy": 6440, "partners": 6441, "dick": 6442, "stylist": 6443, "subscriptions": 6444, "bookshelf": 6445, "icon": 6446, "sketing": 6447, "enjoyed": 6448, "furiously": 6449, "scatting": 6450, "supported": 6451, "everybody": 6452, "prevent": 6453, "spooky": 6454, "trailor": 6455, "predator": 6456, "asians": 6457, "vast": 6458, "simply": 6459, "marathon": 6460, "molding": 6461, "compartments": 6462, "bent": 6463, "simulating": 6464, "chunks": 6465, "keeper": 6466, "claws": 6467, "edition": 6468, "wrecks": 6469, "bulldozer": 6470, "measurements": 6471, "translating": 6472, "outfielder": 6473, "possibility": 6474, "flexing": 6475, "loudspeaker": 6476, "expanse": 6477, "residential": 6478, "trained": 6479, "hairy": 6480, "wordgirl": 6481, "spotted": 6482, "samples": 6483, "investigating": 6484, "glittery": 6485, "generations": 6486, "chewbacca": 6487, "slammed": 6488, "doom": 6489, "magician": 6490, "piling": 6491, "vegas": 6492, "tablespoon": 6493, "wheelies": 6494, "martha": 6495, "quote": 6496, "stocks": 6497, "trouser": 6498, "district": 6499, "marketplace": 6500, "staying": 6501, "constructing": 6502, "vip": 6503, "senior": 6504, "cheap": 6505, "recipie": 6506, "nim": 6507, "gain": 6508, "popcorn": 6509, "wherein": 6510, "charcter": 6511, "column": 6512, "performances": 6513, "orb": 6514, "differently": 6515, "complexion": 6516, "cords": 6517, "mountainous": 6518, "barren": 6519, "aired": 6520, "baggy": 6521, "trend": 6522, "counters": 6523, "began": 6524, "utensil": 6525, "separates": 6526, "cigarettes": 6527, "scenarios": 6528, "medals": 6529, "sometimes": 6530, "voicing": 6531, "downtown": 6532, "handed": 6533, "skipping": 6534, "utility": 6535, "laundry": 6536, "speach": 6537, "rewards": 6538, "heights": 6539, "fuses": 6540, "teases": 6541, "tense": 6542, "flicker": 6543, "suburban": 6544, "dallas": 6545, "hesitant": 6546, "k": 6547, "adjacent": 6548, "scoop": 6549, "reported": 6550, "protestors": 6551, "vitamin": 6552, "liquids": 6553, "freshly": 6554, "tiling": 6555, "vidoe": 6556, "construct": 6557, "centre": 6558, "lighter": 6559, "overcoat": 6560, "posed": 6561, "descriptions": 6562, "paws": 6563, "cannons": 6564, "pats": 6565, "sequences": 6566, "snaps": 6567, "grains": 6568, "stunned": 6569, "thors": 6570, "braking": 6571, "spend": 6572, "dome": 6573, "invisible": 6574, "colouring": 6575, "60": 6576, "sweets": 6577, "pastries": 6578, "locked": 6579, "pasture": 6580, "garam": 6581, "wolfs": 6582, "silhouette": 6583, "pod": 6584, "horned": 6585, "curled": 6586, "origins": 6587, "handicapped": 6588, "hey": 6589, "moderator": 6590, "resembles": 6591, "haul": 6592, "wing": 6593, "span": 6594, "11": 6595, "abuse": 6596, "document": 6597, "funky": 6598, "voter": 6599, "committee": 6600, "flee": 6601, "indy": 6602, "attraction": 6603, "simulated": 6604, "bunk": 6605, "spilling": 6606, "campfire": 6607, "characteristics": 6608, "stripe": 6609, "troubles": 6610, "cartoonish": 6611, "alphabets": 6612, "placement": 6613, "andrew": 6614, "spectator": 6615, "wwii": 6616, "triple": 6617, "occur": 6618, "beater": 6619, "greenish": 6620, "nicholson": 6621, "flirting": 6622, "raiding": 6623, "affecting": 6624, "tragedy": 6625, "huddle": 6626, "touched": 6627, "goose": 6628, "anna": 6629, "remembering": 6630, "ford": 6631, "replaces": 6632, "gon": 6633, "lashes": 6634, "cctv": 6635, "drifts": 6636, "seek": 6637, "footballers": 6638, "celebrated": 6639, "transforms": 6640, "elements": 6641, "birdie": 6642, "oldies": 6643, "rendition": 6644, "teaspoon": 6645, "premiere": 6646, "debates": 6647, "guacamole": 6648, "movieclipscom": 6649, "toddlers": 6650, "liked": 6651, "p": 6652, "i&p": 6653, "accessory": 6654, "usb": 6655, "romantically": 6656, "halle": 6657, "sofas": 6658, "everyday": 6659, "crack": 6660, "inspiring": 6661, "dresser": 6662, "h": 6663, "chrome": 6664, "feats": 6665, "ranking": 6666, "goth": 6667, "nods": 6668, "clashes": 6669, "lengths": 6670, "dipped": 6671, "cyrus": 6672, "sexually": 6673, "incidents": 6674, "jewelry": 6675, "mounts": 6676, "lazer": 6677, "bold": 6678, "included": 6679, "interstellar": 6680, "2013": 6681, "peacefully": 6682, "predictions": 6683, "innovation": 6684, "screws": 6685, "sack": 6686, "meaning": 6687, "graco": 6688, "weighs": 6689, "blurry": 6690, "econ": 6691, "treated": 6692, "championships": 6693, "sprinter": 6694, "mentioned": 6695, "coverings": 6696, "swaying": 6697, "lockers": 6698, "cab": 6699, "budget": 6700, "firewood": 6701, "mathematical": 6702, "flatscreen": 6703, "questioned": 6704, "angela": 6705, "degrees": 6706, "pasting": 6707, "seal": 6708, "merchandise": 6709, "bachata": 6710, "salsa": 6711, "bangs": 6712, "lounging": 6713, "headlight": 6714, "winged": 6715, "friendly": 6716, "environmental": 6717, "pokeman": 6718, "pockets": 6719, "kentucky": 6720, "14": 6721, "christian": 6722, "toothpick": 6723, "geographical": 6724, "lessening": 6725, "feather": 6726, "nutritional": 6727, "consistency": 6728, "tanker": 6729, "faced": 6730, "summary": 6731, "sining": 6732, "pills": 6733, "hooking": 6734, "sadly": 6735, "vader": 6736, "sin": 6737, "criticizes": 6738, "scolding": 6739, "dollhouse": 6740, "patterned": 6741, "protection": 6742, "situated": 6743, "tribal": 6744, "poring": 6745, "aroud": 6746, "goddess": 6747, "sculptures": 6748, "worlds": 6749, "boneless": 6750, "saute": 6751, "oak": 6752, "rust": 6753, "population": 6754, "boob": 6755, "craters": 6756, "era": 6757, "sceen": 6758, "leggo": 6759, "bengal": 6760, "weeping": 6761, "baskeball": 6762, "shifts": 6763, "replies": 6764, "stations": 6765, "aboard": 6766, "captive": 6767, "yound": 6768, "solders": 6769, "organize": 6770, "animatronic": 6771, "disappointed": 6772, "creations": 6773, "elaborately": 6774, "j": 6775, "ipo": 6776, "clearly": 6777, "eyelids": 6778, "gameboy": 6779, "panic": 6780, "recaps": 6781, "ladle": 6782, "export": 6783, "transporting": 6784, "morality": 6785, "aims": 6786, "jessica": 6787, "alba": 6788, "slab": 6789, "wad": 6790, "blended": 6791, "honored": 6792, "gravity": 6793, "lizards": 6794, "ringing": 6795, "ceramic": 6796, "restuarant": 6797, "throne": 6798, "stuffing": 6799, "transferring": 6800, "limbs": 6801, "breeze": 6802, "spiral": 6803, "tress": 6804, "innovative": 6805, "lids": 6806, "aimed": 6807, "villages": 6808, "interstate": 6809, "lanes": 6810, "chipotle": 6811, "combine": 6812, "shotgun": 6813, "genome": 6814, "cracked": 6815, "ripped": 6816, "bakes": 6817, "eyed": 6818, "toes": 6819, "inspecting": 6820, "foriegn": 6821, "foreigners": 6822, "quitting": 6823, "disc": 6824, "combination": 6825, "purchase": 6826, "vanquish": 6827, "activated": 6828, "advancing": 6829, "saws": 6830, "hazy": 6831, "screenshots": 6832, "steaming": 6833, "specialist": 6834, "costumed": 6835, "criminals": 6836, "walter": 6837, "collapsing": 6838, "dion": 6839, "printer": 6840, "careers": 6841, "tortillas": 6842, "crepes": 6843, "sites": 6844, "trades": 6845, "cheered": 6846, "deliver": 6847, "wading": 6848, "west": 6849, "os": 6850, "ferris": 6851, "chubby": 6852, "graphical": 6853, "increasing": 6854, "bumper": 6855, "adjustments": 6856, "remains": 6857, "direct": 6858, "corral": 6859, "operated": 6860, "motivational": 6861, "runnig": 6862, "heaven": 6863, "fowler": 6864, "labeled": 6865, "vladimir": 6866, "bowing": 6867, "12": 6868, "bonus": 6869, "declares": 6870, "voltorb": 6871, "prototype": 6872, "margaret": 6873, "thatcher": 6874, "announced": 6875, "mars": 6876, "legend": 6877, "scarlet": 6878, "ridding": 6879, "imac": 6880, "poetry": 6881, "judgement": 6882, "employment": 6883, "soaring": 6884, "zinc": 6885, "oxide": 6886, "browning": 6887, "parted": 6888, "critic": 6889, "previously": 6890, "releasing": 6891, "exit": 6892, "brooklyn": 6893, "newswoman": 6894, "volks": 6895, "volunteer": 6896, "associated": 6897, "initially": 6898, "uptown": 6899, "ended": 6900, "panting": 6901, "latoya": 6902, "parlor": 6903, "castors": 6904, "nut": 6905, "carson": 6906, "mountainside": 6907, "rushed": 6908, "seas": 6909, "puzzles": 6910, "spear": 6911, "prosthetic": 6912, "guides": 6913, "casters": 6914, "jolene": 6915, "spitzer": 6916, "tulle": 6917, "sony": 6918, "representative": 6919, "reptile": 6920, "combo": 6921, "gump": 6922, "oncoming": 6923, "leia": 6924, "influenced": 6925, "improving": 6926, "leafs": 6927, "baseballs": 6928, "punched": 6929, "convenience": 6930, "automated": 6931, "jon": 6932, "creamy": 6933, "grab": 6934, "smacks": 6935, "medications": 6936, "la": 6937, "playthrough": 6938, "tissue": 6939, "unreal": 6940, "narrators": 6941, "predicted": 6942, "curious": 6943, "bb": 6944, "orca": 6945, "presley": 6946, "decorates": 6947, "911": 6948, "shaft": 6949, "porn": 6950, "wipers": 6951, "patting": 6952, "tuned": 6953, "sautes": 6954, "panics": 6955, "combed": 6956, "beasts": 6957, "looney": 6958, "dusk": 6959, "mechanism": 6960, "bacteria": 6961, "silhouettes": 6962, "beijing": 6963, "jackie": 6964, "chan": 6965, "inspired": 6966, "satellites": 6967, "hydraulic": 6968, "glamorous": 6969, "nacho": 6970, "baguette": 6971, "teammate": 6972, "belts": 6973, "melts": 6974, "melted": 6975, "costs": 6976, "scarves": 6977, "cocktail": 6978, "lemonade": 6979, "pakistan": 6980, "si": 6981, "jetpack": 6982, "silento": 6983, "drained": 6984, "biscoot": 6985, "carwash": 6986, "marijuana": 6987, "saber": 6988, "martymer": 6989, "pleasant": 6990, "adam": 6991, "controller": 6992, "bernie": 6993, "attendant": 6994, "olaf": 6995, "charcoal": 6996, "hadron": 6997, "crane": 6998, "halos": 6999, "communities": 7000, "truffle": 7001, "200000": 7002, "units": 7003, "devry": 7004, "16": 7005, "cannabis": 7006, "chemotherapy": 7007, "catalog": 7008, "drainer": 7009, "insurance": 7010, "precautionary": 7011, "holograms": 7012, "wiz": 7013, "hulu": 7014, "pixels": 7015, "welcoming": 7016, "carell": 7017, "vietnamese": 7018, "dates": 7019, "september": 7020, "mccartney": 7021, "dollar": 7022, "heeled": 7023, "ox": 7024, "sprinters": 7025, "verbally": 7026, "messi": 7027, "mouths": 7028, "palms": 7029, "feminine": 7030, "floppy": 7031, "believing": 7032, "defending": 7033, "knew": 7034, "erase": 7035, "sideline": 7036, "pixelated": 7037, "toasting": 7038, "potted": 7039, "partying": 7040, "passionate": 7041, "failing": 7042, "supplements": 7043, "occasionally": 7044, "ex": 7045, "shoppers": 7046, "browse": 7047, "tutorials": 7048, "goers": 7049, "canine": 7050, "hikes": 7051, "veterinarian": 7052, "patty": 7053, "wilson": 7054, "skying": 7055, "blazers": 7056, "printing": 7057, "vide": 7058, "thermostat": 7059, "roasted": 7060, "aston": 7061, "troopers": 7062, "parachuting": 7063, "thus": 7064, "shoving": 7065, "chinnesse": 7066, "miles": 7067, "univers": 7068, "stack": 7069, "raise": 7070, "retro": 7071, "conditioner": 7072, "comb": 7073, "combs": 7074, "chamber": 7075, "bunnies": 7076, "recklessly": 7077, "caressing": 7078, "plow": 7079, "consumption": 7080, "precision": 7081, "perosn": 7082, "megaphone": 7083, "waterway": 7084, "shores": 7085, "lick": 7086, "extended": 7087, "chevy": 7088, "braided": 7089, "improved": 7090, "gymnasts": 7091, "concentrating": 7092, "aligning": 7093, "analyzing": 7094, "purposes": 7095, "developing": 7096, "monitored": 7097, "stiller": 7098, "thunder": 7099, "layers": 7100, "chant": 7101, "singign": 7102, "apparel": 7103, "snoring": 7104, "modification": 7105, "decisions": 7106, "=": 7107, "airing": 7108, "lever": 7109, "southern": 7110, "specifically": 7111, "stabs": 7112, "int": 7113, "usually": 7114, "harm": 7115, "elegantly": 7116, "soccor": 7117, "grandma": 7118, "grandpa": 7119, "hanger": 7120, "braiding": 7121, "workplace": 7122, "inspect": 7123, "investigate": 7124, "luxurious": 7125, "relaxed": 7126, "screencasting": 7127, "analysts": 7128, "conjuring": 7129, "curses": 7130, "scratch": 7131, "syrup": 7132, "bandana": 7133, "irritated": 7134, "redhead": 7135, "roti": 7136, "impressed": 7137, "roam": 7138, "crumbs": 7139, "enjoyment": 7140, "helmeted": 7141, "obese": 7142, "whit": 7143, "mingling": 7144, "latino": 7145, "infrared": 7146, "technicians": 7147, "vocal": 7148, "descends": 7149, "putty": 7150, "slime": 7151, "gooey": 7152, "meows": 7153, "dc": 7154, "biscuits": 7155, "opponenet": 7156, "commentate": 7157, "canada": 7158, "init": 7159, "earlier": 7160, "perfomance": 7161, "motorized": 7162, "supporters": 7163, "barely": 7164, "kannada": 7165, "theatrical": 7166, "deadly": 7167, "zest": 7168, "terminal": 7169, "tapes": 7170, "mc": 7171, "jog": 7172, "purses": 7173, "anothers": 7174, "likely": 7175, "bumping": 7176, "admires": 7177, "massages": 7178, "spa": 7179, "st": 7180, "correspondent": 7181, "affraid": 7182, "updates": 7183, "cinematic": 7184, "overlaid": 7185, "thomas": 7186, "childhood": 7187, "crossdressing": 7188, "influence": 7189, "straighten": 7190, "aa": 7191, "compass": 7192, "spout": 7193, "detained": 7194, "inbetween": 7195, "eldorado": 7196, "predict": 7197, "remodel": 7198, "ranch": 7199, "petted": 7200, "hairdo": 7201, "warms": 7202, "potatoe": 7203, "connects": 7204, "punt": 7205, "delivered": 7206, "frisbee": 7207, "astonished": 7208, "spec": 7209, "adobe": 7210, "lana": 7211, "del": 7212, "rey": 7213, "provocatively": 7214, "indigenous": 7215, "wonder": 7216, "invention": 7217, "ratings": 7218, "downloading": 7219, "narrate": 7220, "farmland": 7221, "murky": 7222, "confronted": 7223, "cleavage": 7224, "thousands": 7225, "18": 7226, "month": 7227, "afghanistan": 7228, "pavilion": 7229, "sold": 7230, "livecast": 7231, "significant": 7232, "informing": 7233, "beatboxing": 7234, "backround": 7235, "swimsuits": 7236, "crops": 7237, "pixel": 7238, "racquet": 7239, "garments": 7240, "inches": 7241, "interrupted": 7242, "workings": 7243, "fraud": 7244, "oliver": 7245, "repeat": 7246, "repeating": 7247, "maximum": 7248, "teased": 7249, "restaraunt": 7250, "neighbor": 7251, "distressed": 7252, "tending": 7253, "drill": 7254, "spills": 7255, "wipe": 7256, "whips": 7257, "ultra": 7258, "roofing": 7259, "ovation": 7260, "proof": 7261, "often": 7262, "bout": 7263, "trench": 7264, "faith": 7265, "casserole": 7266, "zeppelin": 7267, "sweaters": 7268, "downloaded": 7269, "folders": 7270, "mill": 7271, "flaming": 7272, "flickering": 7273, "humorously": 7274, "chile": 7275, "grills": 7276, "laptops": 7277, "attracting": 7278, "qualifiers": 7279, "dino": 7280, "denver": 7281, "buzzer": 7282, "guided": 7283, "dreaming": 7284, "aroma": 7285, "dicussing": 7286, "mishaps": 7287, "prayer": 7288, "newsreader": 7289, "outlet": 7290, "gallops": 7291, "particulars": 7292, "apologizing": 7293, "dip": 7294, "everytime": 7295, "occurring": 7296, "visions": 7297, "varied": 7298, "guessing": 7299, "finals": 7300, "grappling": 7301, "stem": 7302, "wheelie": 7303, "wheeling": 7304, "cw": 7305, "vault": 7306, "tortured": 7307, "connection": 7308, "jams": 7309, "pic": 7310, "tennise": 7311, "animates": 7312, "gauges": 7313, "fenced": 7314, "accuses": 7315, "chilies": 7316, "fireball": 7317, "metallic": 7318, "mandarin": 7319, "badly": 7320, "rescuing": 7321, "spits": 7322, "begging": 7323, "drips": 7324, "clerk": 7325, "concepts": 7326, "splits": 7327, "noddles": 7328, "unwraps": 7329, "surprises": 7330, "amazed": 7331, "amid": 7332, "sense": 7333, "wheat": 7334, "inserted": 7335, "coating": 7336, "miley": 7337, "handwriting": 7338, "respond": 7339, "sequin": 7340, "labels": 7341, "lamborghinis": 7342, "releases": 7343, "fins": 7344, "autobytel": 7345, "guitor": 7346, "nomination": 7347, "broadcasters": 7348, "creme": 7349, "plush": 7350, "concoction": 7351, "hers": 7352, "strap": 7353, "hover": 7354, "fought": 7355, "rods": 7356, "participant": 7357, "allows": 7358, "swarm": 7359, "port": 7360, "pistols": 7361, "environments": 7362, "mainly": 7363, "steams": 7364, "limbaugh": 7365, "chancellor": 7366, "probe": 7367, "determine": 7368, "outcome": 7369, "dim": 7370, "glows": 7371, "bakery": 7372, "joyfully": 7373, "corporation": 7374, "pry": 7375, "priced": 7376, "drumming": 7377, "replica": 7378, "tarzan": 7379, "astronaut": 7380, "hamburger": 7381, "decent": 7382, "dodges": 7383, "climax": 7384, "dumped": 7385, "dye": 7386, "aerobics": 7387, "finely": 7388, "cons": 7389, "ammunition": 7390, "tents": 7391, "diseases": 7392, "whisks": 7393, "apartments": 7394, "documents": 7395, "modded": 7396, "illustrations": 7397, "highways": 7398, "roadway": 7399, "reminiscing": 7400, "mand": 7401, "darth": 7402, "rearview": 7403, "spooned": 7404, "buys": 7405, "seducing": 7406, "explaying": 7407, "cascades": 7408, "carpeted": 7409, "limit": 7410, "deeply": 7411, "sculpture": 7412, "supernatural": 7413, "informs": 7414, "trolly": 7415, "thrilling": 7416, "hind": 7417, "chew": 7418, "plotting": 7419, "contained": 7420, "quietly": 7421, "arrive": 7422, "suspense": 7423, "scratches": 7424, "truth": 7425, "clinic": 7426, "worry": 7427, "habits": 7428, "basics": 7429, "australians": 7430, "beverages": 7431, "memory": 7432, "unwind": 7433, "taller": 7434, "desks": 7435, "sultry": 7436, "shoves": 7437, "guru": 7438, "saint": 7439, "mashup": 7440, "breath": 7441, "cheif": 7442, "powdered": 7443, "documenting": 7444, "broom": 7445, "dreams": 7446, "theire": 7447, "bmx": 7448, "ariana": 7449, "grande": 7450, "soloist": 7451, "mannequin": 7452, "kitchena": 7453, "hungry": 7454, "inlet": 7455, "nemesis": 7456, "katniss": 7457, "laments": 7458, "educating": 7459, "imaging": 7460, "selects": 7461, "savanna": 7462, "humurous": 7463, "approached": 7464, "gut": 7465, "gasket": 7466, "rode": 7467, "whistles": 7468, "aurora": 7469, "traced": 7470, "prop": 7471, "interviewers": 7472, "stunning": 7473, "unzips": 7474, "seperated": 7475, "cpr": 7476, "exam": 7477, "thee": 7478, "evaluated": 7479, "petrol": 7480, "sprayed": 7481, "ls": 7482, "vinegar": 7483, "parka": 7484, "stout": 7485, "thong": 7486, "panty": 7487, "episodes": 7488, "raging": 7489, "serene": 7490, "boulders": 7491, "genes": 7492, "topped": 7493, "projection": 7494, "pies": 7495, "naming": 7496, "rim": 7497, "demostrating": 7498, "sorry": 7499, "swiftly": 7500, "smiley": 7501, "dangerously": 7502, "hurting": 7503, "decker": 7504, "porcelain": 7505, "koch": 7506, "cupcake": 7507, "dozens": 7508, "dug": 7509, "nickelodeon": 7510, "notices": 7511, "bulls": 7512, "unbelievable": 7513, "newsman": 7514, "nigerian": 7515, "maneuver": 7516, "garnishes": 7517, "particularly": 7518, "agreement": 7519, "digestion": 7520, "wireless": 7521, "entry": 7522, "temperatures": 7523, "furious": 7524, "frizzy": 7525, "perfoming": 7526, "backside": 7527, "imaginary": 7528, "crossdresser": 7529, "quarters": 7530, "unfinished": 7531, "quarrel": 7532, "briefcase": 7533, "gems": 7534, "courtroom": 7535, "sleeper": 7536, "occasions": 7537, "interviewd": 7538, "breathing": 7539, "celine": 7540, "unable": 7541, "organic": 7542, "transported": 7543, "joe": 7544, "lightly": 7545, "ther": 7546, "await": 7547, "atheletes": 7548, "gains": 7549, "perfomed": 7550, "salesmen": 7551, "sinai": 7552, "parading": 7553, "sensational": 7554, "angled": 7555, "lengthwise": 7556, "stencil": 7557, "karaoke": 7558, "rescued": 7559, "floyd": 7560, "amusing": 7561, "ethnic": 7562, "buried": 7563, "secrets": 7564, "reason": 7565, "stumbles": 7566, "sanding": 7567, "sands": 7568, "monuments": 7569, "plying": 7570, "responsibility": 7571, "kurtz": 7572, "azuz": 7573, "adventurous": 7574, "progressing": 7575, "correspondents": 7576, "ballerinas": 7577, "consists": 7578, "korn": 7579, "gravel": 7580, "sari": 7581, "wade": 7582, "spacecraft": 7583, "sprint": 7584, "volcanic": 7585, "fisherman": 7586, "strut": 7587, "panned": 7588, "cpu": 7589, "rig": 7590, "responds": 7591, "joined": 7592, "racial": 7593, "racism": 7594, "wolks": 7595, "household": 7596, "chews": 7597, "gum": 7598, "ponytail": 7599, "garnishing": 7600, "seatbelt": 7601, "exposing": 7602, "scrubbing": 7603, "gazing": 7604, "brawl": 7605, "paychex": 7606, "difficulty": 7607, "lean": 7608, "spanning": 7609, "cdc": 7610, "transfer": 7611, "blouses": 7612, "caps": 7613, "peek": 7614, "lunar": 7615, "vendor": 7616, "religions": 7617, "roses": 7618, "ricky": 7619, "headline": 7620, "nominee": 7621, "commando": 7622, "efficiency": 7623, "heros": 7624, "dudes": 7625, "hank": 7626, "stress": 7627, "mo": 7628, "property": 7629, "1000": 7630, "beatboxes": 7631, "trousers": 7632, "sudden": 7633, "collides": 7634, "oracle": 7635, "foe": 7636, "slipping": 7637, "bowtie": 7638, "trampled": 7639, "wrestles": 7640, "interrupting": 7641, "barr": 7642, "upwards": 7643, "browser": 7644, "highschool": 7645, "electro": 7646, "grate": 7647, "cuting": 7648, "couches": 7649, "playdoh": 7650, "pianist": 7651, "arrangement": 7652, "ludacris": 7653, "arnold": 7654, "carve": 7655, "luke": 7656, "practiced": 7657, "nineteen": 7658, "electrons": 7659, "pundits": 7660, "referendum": 7661, "paces": 7662, "scences": 7663, "attach": 7664, "balea": 7665, "fleeing": 7666, "experienced": 7667, "shovel": 7668, "lifeguard": 7669, "buddha": 7670, "merry": 7671, "barriers": 7672, "presumably": 7673, "caricatures": 7674, "slayer": 7675, "beagle": 7676, "lived": 7677, "polaris": 7678, "ranger": 7679, "defends": 7680, "cutout": 7681, "focuses": 7682, "109": 7683, "animators": 7684, "crumb": 7685, "stomps": 7686, "slate": 7687, "balm": 7688, "mist": 7689, "flashback": 7690, "orcas": 7691, "aquatic": 7692, "cigar": 7693, "sullivan": 7694, "syncs": 7695, "sprinkling": 7696, "fathers": 7697, "strings": 7698, "baltic": 7699, "quiet": 7700, "presence": 7701, "greensleeves": 7702, "twice": 7703, "forum": 7704, "jello": 7705, "whites": 7706, "traces": 7707, "carcass": 7708, "nasty": 7709, "colliding": 7710, "microwaving": 7711, "wiffleball": 7712, "straightening": 7713, "chi": 7714, "sylvester": 7715, "clothed": 7716, "disappear": 7717, "collect": 7718, "trampolines": 7719, "exfoliating": 7720, "gorilla": 7721, "casket": 7722, "loreal": 7723, "accelerates": 7724, "nowhere": 7725, "attitude": 7726, "mikey": 7727, "sucked": 7728, "bazar": 7729, "barar": 7730, "teenaged": 7731, "21": 7732, "dynamite": 7733, "hotter": 7734, "bursts": 7735, "unzipping": 7736, "showdown": 7737, "nascar": 7738, "suites": 7739, "scars": 7740, "anakin": 7741, "julia": 7742, "pushups": 7743, "milford": 7744, "recites": 7745, "equipments": 7746, "coughing": 7747, "propaganda": 7748, "81": 7749, "trails": 7750, "poopourri": 7751, "granny": 7752, "psp": 7753, "undressed": 7754, "reward": 7755, "dopamine": 7756, "hops": 7757, "ballons": 7758, "producer": 7759, "depression": 7760, "trainers": 7761, "attempted": 7762, "grade": 7763, "btl": 7764, "sartorialist": 7765, "builders": 7766, "flattened": 7767, "linkin": 7768, "moaning": 7769, "cardinals": 7770, "hammering": 7771, "shortening": 7772, "damme": 7773, "carolyn": 7774, "nerds": 7775, "freeze": 7776, "sandler": 7777, "measure": 7778, "truffles": 7779, "finance": 7780, "cardio": 7781, "seahorse": 7782, "scoville": 7783, "lasagna": 7784, "deadbolt": 7785, "florida": 7786, "hoverboard": 7787, "rental": 7788, "bridget": 7789, "anvil": 7790, "essentials": 7791, "yawning": 7792, "cousteau": 7793, "locking": 7794, "bond": 7795, "shia": 7796, "mortar": 7797, "morocco": 7798, "gogo": 7799, "spinner": 7800, "chimney": 7801, "communism": 7802, "socialism": 7803, "molten": 7804, "corgi": 7805, "dalmatians": 7806, "bugatti": 7807, "travolta": 7808, "uteruses": 7809, "atari": 7810, "projectors": 7811, "genetics": 7812, "belgium": 7813, "spearow": 7814, "ironing": 7815, "kuhar": 7816, "gecko": 7817, "silently": 7818, "guying": 7819, "combustion": 7820, "tester": 7821, "bouncy": 7822, "acted": 7823, "portraying": 7824, "plugged": 7825, "institute": 7826, "fron": 7827, "welcomes": 7828, "falon": 7829, "backhand": 7830, "unlock": 7831, "forever": 7832, "praising": 7833, "heel": 7834, "glittering": 7835, "transforming": 7836, "heats": 7837, "inn": 7838, "curl": 7839, "illustrate": 7840, "boxed": 7841, "compare": 7842, "introductions": 7843, "attracted": 7844, "anxiously": 7845, "pinatas": 7846, "cleared": 7847, "whacks": 7848, "earns": 7849, "entertain": 7850, "investigation": 7851, "sidelines": 7852, "continuously": 7853, "restraunt": 7854, "crunchy": 7855, "abused": 7856, "gory": 7857, "appropriate": 7858, "squirting": 7859, "bot": 7860, "customized": 7861, "goup": 7862, "bandages": 7863, "leo": 7864, "hairstyling": 7865, "attended": 7866, "blasted": 7867, "powders": 7868, "f": 7869, "sking": 7870, "snowboarding": 7871, "fabrics": 7872, "favourite": 7873, "chines": 7874, "risks": 7875, "joints": 7876, "partial": 7877, "choreography": 7878, "hoses": 7879, "coasters": 7880, "framing": 7881, "discribing": 7882, "thug": 7883, "vocals": 7884, "sheaf": 7885, "overlook": 7886, "telescope": 7887, "moutain": 7888, "accompanying": 7889, "inflatable": 7890, "swimmer": 7891, "cluster": 7892, "meadows": 7893, "moulding": 7894, "3-d": 7895, "enlisted": 7896, "programe": 7897, "considers": 7898, "intercourse": 7899, "fatalities": 7900, "plenty": 7901, "forcing": 7902, "toned": 7903, "deciding": 7904, "encouraged": 7905, "embraces": 7906, "incoming": 7907, "micron": 7908, "accuracy": 7909, "filed": 7910, "marks": 7911, "anchoring": 7912, "tedx": 7913, "superior": 7914, "pajamas": 7915, "affection": 7916, "accompany": 7917, "coding": 7918, "studied": 7919, "anxiety": 7920, "havoc": 7921, "swept": 7922, "commander": 7923, "suspension": 7924, "unboxes": 7925, "clutter": 7926, "cardigan": 7927, "naps": 7928, "innovations": 7929, "membrane": 7930, "genre": 7931, "las": 7932, "sriracha": 7933, "hung": 7934, "medley": 7935, "pantry": 7936, "victorian": 7937, "parasol": 7938, "umberella": 7939, "twinkling": 7940, "sparkling": 7941, "scramble": 7942, "cramer": 7943, "servicemen": 7944, "precipitable": 7945, "inc": 7946, "attends": 7947, "pyramids": 7948, "strands": 7949, "jig": 7950, "banks": 7951, "jokingly": 7952, "shy": 7953, "workouts": 7954, "sid": 7955, "associate": 7956, "chillies": 7957, "lily": 7958, "gauge": 7959, "headpiece": 7960, "prizes": 7961, "mining": 7962, "scence": 7963, "amphitheater": 7964, "hype": 7965, "skypark": 7966, "maching": 7967, "lucy": 7968, "strawberry": 7969, "blurred": 7970, "plugs": 7971, "forcefully": 7972, "eyeballs": 7973, "toothpaste": 7974, "siblings": 7975, "montoge": 7976, "deserted": 7977, "caged": 7978, "bulldog": 7979, "confrontation": 7980, "podcast": 7981, "noticed": 7982, "receding": 7983, "yacht": 7984, "false": 7985, "flipped": 7986, "sear": 7987, "gap": 7988, "closeups": 7989, "butts": 7990, "splattered": 7991, "anthropomorphic": 7992, "petri": 7993, "fives": 7994, "scan": 7995, "ensemble": 7996, "blew": 7997, "champions": 7998, "ayo": 7999, "goo": 8000, "gambling": 8001, "convulses": 8002, "lama": 8003, "cracker": 8004, "soaks": 8005, "farage": 8006, "ethic": 8007, "y": 8008, "snippets": 8009, "endcap": 8010, "randomly": 8011, "pina": 8012, "separately": 8013, "skaters": 8014, "topping": 8015, "mockingjay": 8016, "fred": 8017, "stereo": 8018, "illustrating": 8019, "profiled": 8020, "patrons": 8021, "skincare": 8022, "massaged": 8023, "moisturizer": 8024, "pimples": 8025, "ravine": 8026, "least": 8027, "restaurent": 8028, "roasts": 8029, "coastal": 8030, "flyover": 8031, "recounts": 8032, "twists": 8033, "brutality": 8034, "sidewalks": 8035, "ny": 8036, "racecars": 8037, "snuggling": 8038, "chariot": 8039, "equations": 8040, "stethoscope": 8041, "fluids": 8042, "automobiles": 8043, "holly": 8044, "voted": 8045, "roughly": 8046, "achieved": 8047, "classics": 8048, "achievements": 8049, "yogurt": 8050, "scope": 8051, "shooted": 8052, "hail": 8053, "delivery": 8054, "soars": 8055, "boulder": 8056, "bagpipes": 8057, "parakeet": 8058, "clause": 8059, "possibilities": 8060, "explored": 8061, "mustang": 8062, "firearm": 8063, "stoller": 8064, "somewhat": 8065, "gender": 8066, "somthing": 8067, "littlest": 8068, "lightbulb": 8069, "measurement": 8070, "ios": 8071, "foggy": 8072, "nightclub": 8073, "sprinkled": 8074, "turmeric": 8075, "germs": 8076, "citizens": 8077, "resources": 8078, "python": 8079, "suns": 8080, "repeated": 8081, "brownish": 8082, "gamers": 8083, "simon": 8084, "magnetic": 8085, "circumstances": 8086, "hawkins": 8087, "manly": 8088, "chilling": 8089, "thrill": 8090, "banging": 8091, "stroke": 8092, "danielle": 8093, "foamy": 8094, "invited": 8095, "request": 8096, "transitioning": 8097, "benjamin": 8098, "clipboard": 8099, "suffering": 8100, "crusty": 8101, "reflex": 8102, "rimmed": 8103, "signals": 8104, "render": 8105, "affairs": 8106, "barrett": 8107, "cultivation": 8108, "worldwide": 8109, "crouches": 8110, "toilets": 8111, "desserts": 8112, "barrier": 8113, "overtake": 8114, "deceased": 8115, "emt": 8116, "apparatus": 8117, "yelled": 8118, "negotiating": 8119, "rape": 8120, "tyson": 8121, "dozen": 8122, "knowing": 8123, "fury": 8124, "skateboarding": 8125, "laps": 8126, "pingpong": 8127, "catwoman": 8128, "1990": 8129, "unicorn": 8130, "collegiate": 8131, "souse": 8132, "floored": 8133, "rectangles": 8134, "texture": 8135, "seance": 8136, "pray": 8137, "slanted": 8138, "spells": 8139, "steaks": 8140, "combines": 8141, "passion": 8142, "dashes": 8143, "kazooie": 8144, "steamer": 8145, "saran": 8146, "topless": 8147, "reenactment": 8148, "dumpling": 8149, "tinted": 8150, "spokesperson": 8151, "actively": 8152, "equestrian": 8153, "upload": 8154, "fame": 8155, "responding": 8156, "physiology": 8157, "typically": 8158, "climbed": 8159, "woolen": 8160, "dips": 8161, "giggles": 8162, "broadcaster": 8163, "caves": 8164, "corresponding": 8165, "crimes": 8166, "hopeless": 8167, "hte": 8168, "backetball": 8169, "makeshift": 8170, "outcomes": 8171, "command": 8172, "rudolph": 8173, "reindeer": 8174, "1st": 8175, "achieve": 8176, "chap": 8177, "kardashian": 8178, "bridal": 8179, "camels": 8180, "manta": 8181, "sting": 8182, "reef": 8183, "countless": 8184, "stance": 8185, "yogi": 8186, "tons": 8187, "chiles": 8188, "dispenses": 8189, "snippet": 8190, "knights": 8191, "woan": 8192, "recreate": 8193, "brussel": 8194, "refuses": 8195, "produces": 8196, "pepsi": 8197, "slowing": 8198, "intimately": 8199, "leapfrog": 8200, "cutlet": 8201, "vegan": 8202, "shaky": 8203, "catapulting": 8204, "sledgehammer": 8205, "dries": 8206, "whose": 8207, "tensed": 8208, "graveler": 8209, "someting": 8210, "tag": 8211, "pounds": 8212, "encourage": 8213, "sealant": 8214, "puncture": 8215, "charged": 8216, "dubai": 8217, "automatically": 8218, "bearing": 8219, "27": 8220, "tt": 8221, "skits": 8222, "dbz": 8223, "observation": 8224, "themes": 8225, "puffer": 8226, "entice": 8227, "odds": 8228, "reactors": 8229, "atlantic": 8230, "ritchie": 8231, "grammer": 8232, "spects": 8233, "ottoman": 8234, "preperation": 8235, "unfold": 8236, "praised": 8237, "muted": 8238, "yum": 8239, "approval": 8240, "vaginas": 8241, "amc": 8242, "contract": 8243, "makers": 8244, "tshirts": 8245, "awe": 8246, "kidding": 8247, "blessed": 8248, "tradition": 8249, "handguns": 8250, "stormtrooper": 8251, "backdrops": 8252, "hooks": 8253, "cares": 8254, "goodies": 8255, "capped": 8256, "parkway": 8257, "upgrades": 8258, "frank": 8259, "drift": 8260, "bown": 8261, "seam": 8262, "conchords": 8263, "illuminated": 8264, "ruffled": 8265, "tribute": 8266, "ramb": 8267, "stocking": 8268, "boogie": 8269, "gliding": 8270, "landed": 8271, "dial": 8272, "christianity": 8273, "spiked": 8274, "trialler": 8275, "agony": 8276, "tanner": 8277, "refugees": 8278, "charleston": 8279, "rob": 8280, "unfortunate": 8281, "baseboard": 8282, "rooftops": 8283, "playdough": 8284, "hook": 8285, "bass": 8286, "bait": 8287, "chord": 8288, "fermented": 8289, "tumbling": 8290, "scrambles": 8291, "alarm": 8292, "winds": 8293, "pressed": 8294, "chested": 8295, "flamboyantly": 8296, "component": 8297, "disgusted": 8298, "habitats": 8299, "manchester": 8300, "towns": 8301, "jetson": 8302, "emphatically": 8303, "gothic": 8304, "bouquet": 8305, "directs": 8306, "seduce": 8307, "humor": 8308, "filtered": 8309, "russell": 8310, "microscope": 8311, "constantly": 8312, "woodworking": 8313, "hokey": 8314, "bumped": 8315, "flavors": 8316, "sunshine": 8317, "selfy": 8318, "exposed": 8319, "crater": 8320, "gathers": 8321, "tribe": 8322, "operations": 8323, "swiping": 8324, "graduation": 8325, "recounting": 8326, "quaterback": 8327, "sacked": 8328, "presenters": 8329, "improvements": 8330, "excaping": 8331, "trespassing": 8332, "nursing": 8333, "testdrive": 8334, "currency": 8335, "bookcases": 8336, "eu": 8337, "respect": 8338, "owning": 8339, "upstairs": 8340, "harajuku": 8341, "collected": 8342, "kate": 8343, "spikes": 8344, "slim": 8345, "unwinding": 8346, "loyal": 8347, "eminem": 8348, "paid": 8349, "rocker": 8350, "freak": 8351, "diapers": 8352, "crouching": 8353, "supplying": 8354, "ornament": 8355, "arch": 8356, "advises": 8357, "troupe": 8358, "predicting": 8359, "sculpting": 8360, "filing": 8361, "tightly": 8362, "drip": 8363, "goop": 8364, "mascra": 8365, "eyelid": 8366, "desperate": 8367, "meats": 8368, "alert": 8369, "packages": 8370, "striking": 8371, "lacross": 8372, "starred": 8373, "ign": 8374, "dicusses": 8375, "inspiration": 8376, "seaside": 8377, "lord": 8378, "whisky": 8379, "rendered": 8380, "calvin": 8381, "presidents": 8382, "spiky": 8383, "wonton": 8384, "enquiring": 8385, "sunrise": 8386, "simulates": 8387, "lowered": 8388, "zero": 8389, "shadowy": 8390, "peice": 8391, "thermal": 8392, "millions": 8393, "gop": 8394, "tegu": 8395, "roger": 8396, "mopeds": 8397, "chocolates": 8398, "skip": 8399, "avoids": 8400, "slight": 8401, "diversity": 8402, "rehearsed": 8403, "institution": 8404, "critics": 8405, "twangy": 8406, "chains": 8407, "telecasting": 8408, "gardens": 8409, "midcourt": 8410, "pooh": 8411, "smartwatch": 8412, "pedestal": 8413, "dual": 8414, "segments": 8415, "cambodia": 8416, "destinations": 8417, "weave": 8418, "trucker": 8419, "dashed": 8420, "maneuvering": 8421, "organisation": 8422, "celebs": 8423, "rapids": 8424, "brook": 8425, "organisms": 8426, "discoveries": 8427, "addition": 8428, "advance": 8429, "rhyming": 8430, "shenanigans": 8431, "attendance": 8432, "guilty": 8433, "auction": 8434, "vocalists": 8435, "teaspoons": 8436, "oath": 8437, "herds": 8438, "bend": 8439, "riffles": 8440, "oceans": 8441, "trial": 8442, "epcot": 8443, "sneaking": 8444, "maintains": 8445, "wayne": 8446, "forced": 8447, "bounds": 8448, "perch": 8449, "raced": 8450, "exchanging": 8451, "disks": 8452, "errors": 8453, "sweat": 8454, "hoping": 8455, "ladles": 8456, "ladling": 8457, "rotate": 8458, "kerala": 8459, "televisions": 8460, "inviting": 8461, "coelom": 8462, "tract": 8463, "harsh": 8464, "choosing": 8465, "parallel": 8466, "founding": 8467, "association": 8468, "glaciers": 8469, "strategic": 8470, "websites": 8471, "eric": 8472, "inspection": 8473, "rec": 8474, "patton": 8475, "wb": 8476, "bros": 8477, "involve": 8478, "debris": 8479, "silverware": 8480, "chai": 8481, "dirtbike": 8482, "gourmet": 8483, "menstrual": 8484, "tampon": 8485, "salads": 8486, "tacos": 8487, "confidently": 8488, "ingredents": 8489, "armored": 8490, "pawn": 8491, "trading": 8492, "consumer": 8493, "roux": 8494, "oman": 8495, "gloved": 8496, "colorfully": 8497, "plywood": 8498, "critiquing": 8499, "skips": 8500, "communication": 8501, "claus": 8502, "attorney": 8503, "grasping": 8504, "tortilla": 8505, "mayweather": 8506, "gotham": 8507, "flew": 8508, "unrest": 8509, "oregon": 8510, "intoxicated": 8511, "waiter": 8512, "15": 8513, "pallet": 8514, "geometric": 8515, "ordered": 8516, "carpets": 8517, "polishing": 8518, "greek": 8519, "carpeting": 8520, "markings": 8521, "dabs": 8522, "clippings": 8523, "computor": 8524, "daytime": 8525, "architecture": 8526, "sayings": 8527, "deserts": 8528, "buisness": 8529, "exactly": 8530, "preparations": 8531, "unloading": 8532, "prepping": 8533, "barks": 8534, "bordered": 8535, "cock": 8536, "snuggles": 8537, "raid": 8538, "wades": 8539, "bolton": 8540, "identify": 8541, "krab": 8542, "emptying": 8543, "figuring": 8544, "unicorns": 8545, "cubs": 8546, "freely": 8547, "wields": 8548, "perhaps": 8549, "astronauts": 8550, "whining": 8551, "nets": 8552, "offensive": 8553, "stoping": 8554, "preakness": 8555, "stakes": 8556, "commands": 8557, "strumming": 8558, "disagree": 8559, "hiphop": 8560, "skarlet": 8561, "kidnapped": 8562, "chick": 8563, "vieo": 8564, "reconstruction": 8565, "cranberries": 8566, "robes": 8567, "oufits": 8568, "sauteeing": 8569, "boring": 8570, "lovable": 8571, "hate": 8572, "dunked": 8573, "client": 8574, "sprints": 8575, "analyzed": 8576, "gesture": 8577, "reputed": 8578, "algorithms": 8579, "glases": 8580, "zz": 8581, "dubstep": 8582, "cures": 8583, "physically": 8584, "traits": 8585, "rallying": 8586, "fasion": 8587, "parodies": 8588, "phase": 8589, "wiggles": 8590, "replayed": 8591, "failures": 8592, "brands": 8593, "1806": 8594, "interrogated": 8595, "gulf": 8596, "sipping": 8597, "soundtrack": 8598, "feline": 8599, "token": 8600, "comercial": 8601, "nodding": 8602, "receive": 8603, "tatoo": 8604, "tattooed": 8605, "hedge": 8606, "introductory": 8607, "genius": 8608, "hotels": 8609, "virtualbox": 8610, "labor": 8611, "macaroni": 8612, "freud": 8613, "relevant": 8614, "squidbert": 8615, "summit": 8616, "3rd": 8617, "wih": 8618, "reply": 8619, "stairway": 8620, "formations": 8621, "awarded": 8622, "kansas": 8623, "singularity": 8624, "stools": 8625, "chaos": 8626, "pug": 8627, "zerg": 8628, "expertly": 8629, "eliot": 8630, "clutch": 8631, "circling": 8632, "desired": 8633, "redneck": 8634, "rodents": 8635, "impersonates": 8636, "pga": 8637, "fatty": 8638, "effort": 8639, "struggle": 8640, "ordinary": 8641, "dam": 8642, "representing": 8643, "federal": 8644, "marshmallows": 8645, "cubic": 8646, "billiards": 8647, "slashing": 8648, "phases": 8649, "harrelson": 8650, "interrupts": 8651, "landmarks": 8652, "ww2": 8653, "flanked": 8654, "cascada": 8655, "dads": 8656, "daughters": 8657, "quarry": 8658, "mound": 8659, "allowed": 8660, "josh": 8661, "bullies": 8662, "gearbox": 8663, "tracking": 8664, "relays": 8665, "stearing": 8666, "tax": 8667, "forty": 8668, "buzzfeed": 8669, "scape": 8670, "plugging": 8671, "whipped": 8672, "vase": 8673, "authority": 8674, "watermark": 8675, "remember": 8676, "bodysuit": 8677, "overly": 8678, "dente": 8679, "send": 8680, "capacity": 8681, "carolina": 8682, "peaks": 8683, "ape": 8684, "exhaust": 8685, "threats": 8686, "bitten": 8687, "tailor": 8688, "diameter": 8689, "sway": 8690, "principles": 8691, "animator": 8692, "survivor": 8693, "million": 8694, "garry": 8695, "exhaustion": 8696, "capsicum": 8697, "gril": 8698, "dove": 8699, "alienware": 8700, "methadone": 8701, "nyx": 8702, "hilarious": 8703, "kimono": 8704, "eddie": 8705, "mammal": 8706, "extinct": 8707, "kreugar": 8708, "potatos": 8709, "discovers": 8710, "snapping": 8711, "gross": 8712, "strung": 8713, "sights": 8714, "fumble": 8715, "ill": 8716, "compliments": 8717, "leagues": 8718, "beyonce": 8719, "threatened": 8720, "footbal": 8721, "hunter": 8722, "housed": 8723, "patrolling": 8724, "sisters": 8725, "comforts": 8726, "blesses": 8727, "demonstrations": 8728, "selfs": 8729, "applaud": 8730, "wan": 8731, "vedios": 8732, "molds": 8733, "pitchers": 8734, "mustached": 8735, "mischief": 8736, "individually": 8737, "recommend": 8738, "bruno": 8739, "bundle": 8740, "neal": 8741, "shaq": 8742, "chauffeur": 8743, "hoisting": 8744, "priests": 8745, "ritual": 8746, "intervied": 8747, "bark": 8748, "mfe": 8749, "magnum": 8750, "showering": 8751, "mourning": 8752, "giveaway": 8753, "oreal": 8754, "planted": 8755, "aprons": 8756, "salty": 8757, "watering": 8758, "boar": 8759, "patricia": 8760, "oregano": 8761, "technological": 8762, "rackets": 8763, "develop": 8764, "portrait": 8765, "cutscenes": 8766, "pouch": 8767, "harassing": 8768, "huddled": 8769, "hut": 8770, "lincolns": 8771, "atlas": 8772, "tucked": 8773, "dividing": 8774, "200": 8775, "overlay": 8776, "amenities": 8777, "emu": 8778, "youngster": 8779, "underwire": 8780, "arizona": 8781, "morty": 8782, "weed": 8783, "clubhouse": 8784, "suggesting": 8785, "tend": 8786, "pennsylvania": 8787, "eliphant": 8788, "immersed": 8789, "cc": 8790, "relatives": 8791, "competion": 8792, "baseket": 8793, "tedtalks": 8794, "herring": 8795, "stranded": 8796, "weightlifter": 8797, "gorger": 8798, "knief": 8799, "adams": 8800, "malcolm": 8801, "ncaa": 8802, "consumers": 8803, "activating": 8804, "accurate": 8805, "aphrodisiac": 8806, "proffesional": 8807, "moms": 8808, "floods": 8809, "newcasters": 8810, "emptied": 8811, "gained": 8812, "stacked": 8813, "chineese": 8814, "starcraft": 8815, "overgrown": 8816, "tugboat": 8817, "farms": 8818, "sniper": 8819, "adolf": 8820, "cern": 8821, "interactions": 8822, "cutouts": 8823, "radicalism": 8824, "radical": 8825, "essay": 8826, "essays": 8827, "lately": 8828, "savings": 8829, "accounts": 8830, "evolve": 8831, "regions": 8832, "flexibility": 8833, "fits": 8834, "buds": 8835, "droplets": 8836, "gangam": 8837, "neurons": 8838, "collins": 8839, "cloves": 8840, "grant": 8841, "voleyball": 8842, "generating": 8843, "brooke": 8844, "lara": 8845, "croft": 8846, "defeating": 8847, "quentin": 8848, "enabling": 8849, "persecution": 8850, "reefs": 8851, "disassembled": 8852, "sweating": 8853, "stalls": 8854, "doorman": 8855, "millitant": 8856, "sins": 8857, "cosplaying": 8858, "conscious": 8859, "skydivers": 8860, "loans": 8861, "beaches": 8862, "foreground": 8863, "pixar": 8864, "carols": 8865, "silicon": 8866, "producers": 8867, "everglades": 8868, "orangutan": 8869, "hydrogen": 8870, "huddling": 8871, "meg": 8872, "flicks": 8873, "pollen": 8874, "wordings": 8875, "slingshot": 8876, "yawns": 8877, "cytochrome": 8878, "pharmacist": 8879, "3/4": 8880, "huts": 8881, "hammocks": 8882, "bongos": 8883, "asteroids": 8884, "iguana": 8885, "smurfs": 8886, "melissa": 8887, "offended": 8888, "sexiest": 8889, "blacks": 8890, "aux": 8891, "transistor": 8892, "duchess": 8893, "extremist": 8894, "50s": 8895, "faints": 8896, "steamy": 8897, "keyland": 8898, "slack": 8899, "murphy": 8900, "sunlight": 8901, "baymax": 8902, "crayon": 8903, "gremlins": 8904, "wi": 8905, "adblock": 8906, "squash": 8907, "waterproof": 8908, "frankfurt": 8909, "allies": 8910, "wellness": 8911, "packets": 8912, "spoonfuls": 8913, "notepad": 8914, "wo": 8915, "clark": 8916, "kent": 8917, "abe": 8918, "appleseed": 8919, "flickers": 8920, "diy": 8921, "conferencing": 8922, "minimum": 8923, "buiding": 8924, "skillful": 8925, "organized": 8926, "accept": 8927, "raquet": 8928, "debut": 8929, "programmes": 8930, "purchases": 8931, "rhett": 8932, "mobiles": 8933, "slippers": 8934, "sporty": 8935, "squidworth": 8936, "canvas": 8937, "natures": 8938, "contour": 8939, "marino": 8940, "endless": 8941, "remover": 8942, "metalwork": 8943, "chunky": 8944, "phill": 8945, "wounds": 8946, "mics": 8947, "severe": 8948, "receipes": 8949, "delicately": 8950, "blooming": 8951, "boutique": 8952, "betty": 8953, "beneficial": 8954, "scanner": 8955, "cheaper": 8956, "bandage": 8957, "participated": 8958, "venues": 8959, "rash": 8960, "les": 8961, "telecasted": 8962, "manufactured": 8963, "factories": 8964, "reliable": 8965, "bases": 8966, "translucent": 8967, "indo": 8968, "pointy": 8969, "vada": 8970, "eatables": 8971, "shinning": 8972, "christine": 8973, "ruining": 8974, "sketting": 8975, "fastest": 8976, "domed": 8977, "uncovering": 8978, "supper": 8979, "attract": 8980, "herd": 8981, "relating": 8982, "thirty": 8983, "prominent": 8984, "kiddish": 8985, "paratroopers": 8986, "quaint": 8987, "memorabilia": 8988, "polymer": 8989, "injection": 8990, "rythm": 8991, "watery": 8992, "humping": 8993, "effectively": 8994, "analyise": 8995, "xray": 8996, "+": 8997, "juliet": 8998, "proceed": 8999, "henchmen": 9000, "labratory": 9001, "tj": 9002, "yates": 9003, "beetles": 9004, "lamas": 9005, "flock": 9006, "barrels": 9007, "vans": 9008, "happend": 9009, "plows": 9010, "tractors": 9011, "plowing": 9012, "discount": 9013, "lauren": 9014, "hid": 9015, "muscled": 9016, "spoungebob": 9017, "savannah": 9018, "tags": 9019, "keepers": 9020, "policemen": 9021, "miami": 9022, "bosh": 9023, "parlour": 9024, "commented": 9025, "discussess": 9026, "antoher": 9027, "tropic": 9028, "density": 9029, "specification": 9030, "exited": 9031, "overjoyed": 9032, "emits": 9033, "unsure": 9034, "deputy": 9035, "rs": 9036, "popularity": 9037, "earn": 9038, "pinch": 9039, "overcook": 9040, "steamed": 9041, "cougar": 9042, "nussbaum": 9043, "humanity": 9044, "vibrant": 9045, "piper": 9046, "speilberg": 9047, "inflated": 9048, "partition": 9049, "biofuels": 9050, "christopher": 9051, "silhouetted": 9052, "survey": 9053, "delph": 9054, "waved": 9055, "concerned": 9056, "protein": 9057, "fiddling": 9058, "fiddles": 9059, "almighty": 9060, "backstroke": 9061, "remake": 9062, "preaching": 9063, "ucas": 9064, "handcuffed": 9065, "rainbows": 9066, "wondering": 9067, "clarinet": 9068, "convoy": 9069, "caravan": 9070, "vertically": 9071, "toothbrush": 9072, "comeback": 9073, "onboard": 9074, "shaken": 9075, "correcting": 9076, "beachgoers": 9077, "verses": 9078, "slyly": 9079, "monoxide": 9080, "aspect": 9081, "communications": 9082, "belong": 9083, "assailants": 9084, "scans": 9085, "hillsides": 9086, "outstretched": 9087, "icehockey": 9088, "tot": 9089, "chewy": 9090, "perfom": 9091, "harasses": 9092, "slot": 9093, "froma": 9094, "labelled": 9095, "rumor": 9096, "comminicating": 9097, "magenta": 9098, "curiously": 9099, "labs": 9100, "relative": 9101, "impending": 9102, "101": 9103, "squishing": 9104, "crowed": 9105, "entourage": 9106, "crews": 9107, "ar": 9108, "milling": 9109, "terrarium": 9110, "spaces": 9111, "euronews": 9112, "mvong": 9113, "locating": 9114, "moses": 9115, "frankenstein": 9116, "glamour": 9117, "contemporary": 9118, "murdered": 9119, "accusing": 9120, "liking": 9121, "posting": 9122, "defeat": 9123, "scorpio": 9124, "platic": 9125, "sponebob": 9126, "foal": 9127, "youg": 9128, "distorted": 9129, "freddys": 9130, "leggings": 9131, "veranda": 9132, "swirl": 9133, "canisters": 9134, "mitten": 9135, "suck": 9136, "addict": 9137, "bryan": 9138, "analyze": 9139, "conclusion": 9140, "vaseline": 9141, "overrun": 9142, "louis": 9143, "civilians": 9144, "hr": 9145, "rioting": 9146, "iceberg": 9147, "henry": 9148, "dislikes": 9149, "puffy": 9150, "crevice": 9151, "outing": 9152, "edm": 9153, "trimming": 9154, "needle": 9155, "decorative": 9156, "skillfully": 9157, "dumb": 9158, "teling": 9159, "arrived": 9160, "treatments": 9161, "duckling": 9162, "phrase": 9163, "naughty": 9164, "el": 9165, "tinkers": 9166, "bassinet": 9167, "di": 9168, "referring": 9169, "summoned": 9170, "crucial": 9171, "straining": 9172, "sprayer": 9173, "utilizing": 9174, "labour": 9175, "infants": 9176, "bagpipe": 9177, "exhausted": 9178, "organizes": 9179, "beak": 9180, "rubric": 9181, "artifacts": 9182, "recoil": 9183, "reloading": 9184, "jose": 9185, "rory": 9186, "mcilroy": 9187, "michale": 9188, "woamn": 9189, "ass": 9190, "baton": 9191, "wendy": 9192, "shelving": 9193, "outlook": 9194, "yet": 9195, "tablewith": 9196, "newsperson": 9197, "missles": 9198, "cascading": 9199, "finale": 9200, "honking": 9201, "efforts": 9202, "exposes": 9203, "finder": 9204, "alladin": 9205, "popped": 9206, "cumin": 9207, "laughed": 9208, "growling": 9209, "buzzing": 9210, "laden": 9211, "beutiful": 9212, "witnessed": 9213, "bora": 9214, "feautres": 9215, "waling": 9216, "grabbed": 9217, "uninterested": 9218, "copper": 9219, "distant": 9220, "devoted": 9221, "suspected": 9222, "pictorial": 9223, "intentions": 9224, "suncreen": 9225, "harvest": 9226, "extending": 9227, "salamouny": 9228, "playstation": 9229, "rubics": 9230, "analyzes": 9231, "percent": 9232, "israeli": 9233, "term": 9234, "plug": 9235, "plumbing": 9236, "plumber": 9237, "seriousely": 9238, "bodily": 9239, "ages": 9240, "governor": 9241, "blasts": 9242, "hovers": 9243, "spectating": 9244, "givng": 9245, "livingroom": 9246, "coaxes": 9247, "offscreen": 9248, "vying": 9249, "follower": 9250, "routines": 9251, "lone": 9252, "populated": 9253, "disbelief": 9254, "victims": 9255, "videoed": 9256, "tiled": 9257, "township": 9258, "ankles": 9259, "invasion": 9260, "pep": 9261, "elves": 9262, "elbow": 9263, "pilots": 9264, "boll": 9265, "gents": 9266, "preferences": 9267, "adolescents": 9268, "90s": 9269, "videotapes": 9270, "zelda": 9271, "realization": 9272, "crap": 9273, "tunnels": 9274, "hillbilly": 9275, "stroking": 9276, "slat": 9277, "whisked": 9278, "clockwise": 9279, "batch": 9280, "tumbles": 9281, "keinan": 9282, "briggs": 9283, "rated": 9284, "cushions": 9285, "cif": 9286, "identified": 9287, "gruesome": 9288, "mutilated": 9289, "positioned": 9290, "appreciating": 9291, "rituals": 9292, "skidding": 9293, "chambers": 9294, "25": 9295, "organ": 9296, "uploading": 9297, "eyeliners": 9298, "applicator": 9299, "consulting": 9300, "suitable": 9301, "boot": 9302, "lovingly": 9303, "evergreen": 9304, "movee": 9305, "scratching": 9306, "showin": 9307, "translation": 9308, "ref": 9309, "chinchillas": 9310, "blinds": 9311, "hannibal": 9312, "f250": 9313, "tat": 9314, "serenades": 9315, "soften": 9316, "fastened": 9317, "failure": 9318, "mcfarlane": 9319, "enhance": 9320, "hectic": 9321, "relates": 9322, "hints": 9323, "vaults": 9324, "poll": 9325, "critiqued": 9326, "nosed": 9327, "busts": 9328, "pudding": 9329, "peopl": 9330, "psychologist": 9331, "temples": 9332, "analytics": 9333, "pertains": 9334, "lebanon": 9335, "textured": 9336, "horde": 9337, "malls": 9338, "saftey": 9339, "carribbean": 9340, "potter": 9341, "outtake": 9342, "ding": 9343, "poke": 9344, "meteor": 9345, "indicator": 9346, "mob": 9347, "aquarim": 9348, "defend": 9349, "assisting": 9350, "juicer": 9351, "spike": 9352, "promise": 9353, "manufacture": 9354, "although": 9355, "nanny": 9356, "begs": 9357, "vomits": 9358, "unhappy": 9359, "corporate": 9360, "bluish": 9361, "scalloped": 9362, "charger": 9363, "plaing": 9364, "unhealthy": 9365, "resorts": 9366, "vegetarians": 9367, "sepia": 9368, "furnitures": 9369, "shotput": 9370, "average": 9371, "motors": 9372, "narator": 9373, "mileage": 9374, "sledge": 9375, "peddle": 9376, "trust": 9377, "cheating": 9378, "fishnet": 9379, "madly": 9380, "moral": 9381, "knifes": 9382, "dimensions": 9383, "marked": 9384, "totally": 9385, "since": 9386, "neighbors": 9387, "lazy": 9388, "michel": 9389, "dreses": 9390, "scraching": 9391, "benches": 9392, "advert": 9393, "plight": 9394, "tarot": 9395, "arrange": 9396, "bonnet": 9397, "64": 9398, "squeezed": 9399, "abnormal": 9400, "krillin": 9401, "geek": 9402, "socialize": 9403, "torque": 9404, "volunteers": 9405, "speedway": 9406, "pufferfish": 9407, "reviws": 9408, "flared": 9409, "relation": 9410, "shin": 9411, "greyish": 9412, "16th": 9413, "poeple": 9414, "gotten": 9415, "differ": 9416, "brags": 9417, "prowess": 9418, "addressed": 9419, "broadcasted": 9420, "charters": 9421, "tabletop": 9422, "mingle": 9423, "transgender": 9424, "excessive": 9425, "uncomfortable": 9426, "downy": 9427, "adjustable": 9428, "ok": 9429, "embraced": 9430, "traditions": 9431, "snapshot": 9432, "windstorm": 9433, "residence": 9434, "approve": 9435, "turkish": 9436, "airlines": 9437, "con": 9438, "brock": 9439, "putt": 9440, "gunner": 9441, "coalition": 9442, "speking": 9443, "artillery": 9444, "supposedly": 9445, "icefields": 9446, "weekly": 9447, "gaining": 9448, "mutton": 9449, "juicy": 9450, "attachment": 9451, "scenario": 9452, "2007": 9453, "09": 9454, "sub": 9455, "cellular": 9456, "pone": 9457, "switched": 9458, "joan": 9459, "grinning": 9460, "sleek": 9461, "delighted": 9462, "informal": 9463, "supermodel": 9464, "bible": 9465, "rounding": 9466, "motivation": 9467, "ole": 9468, "vatican": 9469, "cathedral": 9470, "severely": 9471, "hurry": 9472, "pas": 9473, "polluted": 9474, "canister": 9475, "techinician": 9476, "wering": 9477, "areal": 9478, "establishment": 9479, "breading": 9480, "bandit": 9481, "detective": 9482, "rip": 9483, "consist": 9484, "tammany": 9485, "contractor": 9486, "contractors": 9487, "income": 9488, "leafy": 9489, "lure": 9490, "cholera": 9491, "epidemic": 9492, "yoke": 9493, "thighs": 9494, "foxnews": 9495, "ladys": 9496, "ridged": 9497, "intently": 9498, "references": 9499, "stability": 9500, "r2d2": 9501, "sierra": 9502, "silence": 9503, "hunts": 9504, "80s": 9505, "cashier": 9506, "dipstick": 9507, "flashy": 9508, "dressers": 9509, "homosexuality": 9510, "torwards": 9511, "boosting": 9512, "modest": 9513, "gamplay": 9514, "gameshop": 9515, "mimicking": 9516, "manga": 9517, "hiker": 9518, "parental": 9519, "forgetting": 9520, "documentation": 9521, "openings": 9522, "source": 9523, "terrestrial": 9524, "smokey": 9525, "crisp": 9526, "thickness": 9527, "mayonnaise": 9528, "delicate": 9529, "magnified": 9530, "quest": 9531, "pertaining": 9532, "enacts": 9533, "commentated": 9534, "availability": 9535, "holidays": 9536, "weeps": 9537, "defender": 9538, "readers": 9539, "planks": 9540, "dice": 9541, "arrest": 9542, "mobility": 9543, "deeper": 9544, "solids": 9545, "suggest": 9546, "obsessed": 9547, "yards": 9548, "rails": 9549, "unexpected": 9550, "winslet": 9551, "freezing": 9552, "christians": 9553, "addicted": 9554, "weigh": 9555, "footballer": 9556, "economical": 9557, "knit": 9558, "pensive": 9559, "critter": 9560, "googla": 9561, "observe": 9562, "taunting": 9563, "mongols": 9564, "council": 9565, "oop": 9566, "phoning": 9567, "lifters": 9568, "mosaic": 9569, "unconscious": 9570, "barefoot": 9571, "projected": 9572, "shawl": 9573, "capabilities": 9574, "darker": 9575, "mapping": 9576, "borderlands": 9577, "collapse": 9578, "sails": 9579, "squirts": 9580, "shimmery": 9581, "giamatti": 9582, "cruises": 9583, "stapler": 9584, "despite": 9585, "puting": 9586, "negotiates": 9587, "velocity": 9588, "timing": 9589, "challenges": 9590, "chapathi": 9591, "wasn": 9592, "nato": 9593, "myself": 9594, "lavender": 9595, "coockery": 9596, "collections": 9597, "ilands": 9598, "liquors": 9599, "diamonds": 9600, "prove": 9601, "evenly": 9602, "perched": 9603, "jason": 9604, "seperate": 9605, "imagination": 9606, "lottery": 9607, "survivors": 9608, "carly": 9609, "pelted": 9610, "buttoning": 9611, "leaking": 9612, "siren": 9613, "alleyway": 9614, "suffers": 9615, "beign": 9616, "similarly": 9617, "recessed": 9618, "marking": 9619, "afghan": 9620, "taliban": 9621, "talkig": 9622, "armies": 9623, "hammock": 9624, "baraka": 9625, "pine": 9626, "blossom": 9627, "gasps": 9628, "reaing": 9629, "recover": 9630, "tumor": 9631, "rollers": 9632, "owen": 9633, "thrilled": 9634, "depressed": 9635, "sump": 9636, "piping": 9637, "timed": 9638, "answered": 9639, "winnie": 9640, "nonstick": 9641, "storing": 9642, "outstanding": 9643, "cheerful": 9644, "fierce": 9645, "tours": 9646, "panes": 9647, "22nd": 9648, "hcc": 9649, "sensual": 9650, "venture": 9651, "succeeding": 9652, "fiery": 9653, "burritos": 9654, "shipping": 9655, "whoopie": 9656, "grohl": 9657, "eagerly": 9658, "instruct": 9659, "tearing": 9660, "fir": 9661, "despicable": 9662, "buyers": 9663, "descibing": 9664, "altered": 9665, "streaker": 9666, "muffins": 9667, "geographic": 9668, "performng": 9669, "stealthily": 9670, "locks": 9671, "downstairs": 9672, "sketchy": 9673, "scent": 9674, "cara": 9675, "penguin": 9676, "smock": 9677, "unsuspecting": 9678, "hopes": 9679, "error": 9680, "malala": 9681, "revolutionary": 9682, "rhythm": 9683, "assaulted": 9684, "apprehensive": 9685, "strait": 9686, "satisfaction": 9687, "vido": 9688, "persona": 9689, "muhammad": 9690, "francisco": 9691, "distinct": 9692, "surge": 9693, "polar": 9694, "dork": 9695, "theirs": 9696, "alan": 9697, "regulations": 9698, "slashes": 9699, "swordfighting": 9700, "busses": 9701, "stormy": 9702, "streak": 9703, "scott": 9704, "bower": 9705, "equipement": 9706, "nsa": 9707, "meaningless": 9708, "robbery": 9709, "piles": 9710, "infinity": 9711, "advising": 9712, "realm": 9713, "franchise": 9714, "stern": 9715, "strangles": 9716, "orbies": 9717, "cheesy": 9718, "tavern": 9719, "etouffee": 9720, "keira": 9721, "knightly": 9722, "hilltop": 9723, "commuting": 9724, "mlb": 9725, "badger": 9726, "weasel": 9727, "carpaccio": 9728, "delight": 9729, "extinguishes": 9730, "mouses": 9731, "sneakers": 9732, "coocking": 9733, "waterfront": 9734, "cardamom": 9735, "peppercorns": 9736, "inflation": 9737, "peeping": 9738, "paneling": 9739, "mouthed": 9740, "fighing": 9741, "trys": 9742, "storied": 9743, "represent": 9744, "controversial": 9745, "votes": 9746, "messaging": 9747, "equal": 9748, "traverses": 9749, "exuberantly": 9750, "afar": 9751, "rival": 9752, "turret": 9753, "scales": 9754, "toast": 9755, "ia": 9756, "muphets": 9757, "gonzo": 9758, "revenue": 9759, "revenues": 9760, "groundhog": 9761, "claw": 9762, "textures": 9763, "furnishings": 9764, "mineral": 9765, "onlooking": 9766, "villians": 9767, "artworks": 9768, "taunts": 9769, "uniformed": 9770, "rebels": 9771, "plad": 9772, "speedily": 9773, "contacts": 9774, "collisions": 9775, "immerse": 9776, "gallon": 9777, "overlooks": 9778, "disassembling": 9779, "boating": 9780, "shakespeare": 9781, "paddling": 9782, "pleading": 9783, "panelists": 9784, "agenda": 9785, "byke": 9786, "rickie": 9787, "platformer": 9788, "smuggling": 9789, "cargo": 9790, "portray": 9791, "shuttlecock": 9792, "posture": 9793, "indicates": 9794, "krusty": 9795, "muslims": 9796, "heap": 9797, "ful": 9798, "edits": 9799, "protaganist": 9800, "aerobic": 9801, "whistling": 9802, "functional": 9803, "realizes": 9804, "footbridge": 9805, "cyclists": 9806, "novelty": 9807, "yankees": 9808, "fool": 9809, "assignment": 9810, "arounf": 9811, "res": 9812, "jingle": 9813, "bells": 9814, "previewing": 9815, "stressing": 9816, "requirement": 9817, "ta": 9818, "intel": 9819, "superimposed": 9820, "homerun": 9821, "acquisition": 9822, "pundit": 9823, "intruder": 9824, "independent": 9825, "contemplate": 9826, "disturb": 9827, "ona": 9828, "aroung": 9829, "nighttime": 9830, "backgroud": 9831, "riving": 9832, "peppered": 9833, "dong": 9834, "wallet": 9835, "bluetooth": 9836, "cylindrical": 9837, "racist": 9838, "snowing": 9839, "lanscape": 9840, "bw": 9841, "depth": 9842, "bwcollision": 9843, "boasting": 9844, "extraordinary": 9845, "unfortunately": 9846, "flashcard": 9847, "unsuccessfully": 9848, "antelopes": 9849, "leapord": 9850, "tugging": 9851, "playset": 9852, "expansion": 9853, "agirl": 9854, "demon": 9855, "broadcasts": 9856, "shirted": 9857, "painter": 9858, "grapples": 9859, "civilization": 9860, "undershirt": 9861, "pancake": 9862, "thoroughly": 9863, "seductive": 9864, "rained": 9865, "appreciate": 9866, "immediately": 9867, "engages": 9868, "manager": 9869, "umpires": 9870, "distribution": 9871, "pamphlet": 9872, "voucher": 9873, "rapped": 9874, "cushion": 9875, "wallpaper": 9876, "roading": 9877, "peephole": 9878, "paly": 9879, "demand": 9880, "paiting": 9881, "configuration": 9882, "ricki": 9883, "singalong": 9884, "minerals": 9885, "hoops": 9886, "beginnings": 9887, "basin": 9888, "froth": 9889, "disembarking": 9890, "fooling": 9891, "stampede": 9892, "inserts": 9893, "webbing": 9894, "academics": 9895, "duo": 9896, "easel": 9897, "ideal": 9898, "shrubs": 9899, "whimsical": 9900, "counterstrike": 9901, "daydreaming": 9902, "verbal": 9903, "erupts": 9904, "hex": 9905, "creeping": 9906, "ramming": 9907, "honks": 9908, "pail": 9909, "portugal": 9910, "flappy": 9911, "arial": 9912, "pinning": 9913, "marches": 9914, "grotesque": 9915, "calf": 9916, "keaton": 9917, "rag": 9918, "solemnly": 9919, "newt": 9920, "trots": 9921, "passage": 9922, "canyons": 9923, "alike": 9924, "occurrences": 9925, "sandal": 9926, "preference": 9927, "riots": 9928, "moshing": 9929, "frolicking": 9930, "typed": 9931, "leaked": 9932, "wetsuit": 9933, "pure": 9934, "q&a": 9935, "batons": 9936, "ttpm": 9937, "adrenaline": 9938, "bonding": 9939, "dota": 9940, "mandolin": 9941, "grater": 9942, "bottoms": 9943, "venice": 9944, "cowell": 9945, "bing": 9946, "trooper": 9947, "monitoring": 9948, "impersonating": 9949, "netherlands": 9950, "dyed": 9951, "jokers": 9952, "dubbing": 9953, "tickled": 9954, "gambit": 9955, "xmen": 9956, "russel": 9957, "crowe": 9958, "dora": 9959, "motorcade": 9960, "mindcraft": 9961, "particles": 9962, "quad": 9963, "hardy": 9964, "hologram": 9965, "dope": 9966, "lollipop": 9967, "wookie": 9968, "fluorescent": 9969, "excess": 9970, "chopper": 9971, "hone": 9972, "hump": 9973, "spends": 9974, "mayhem": 9975, "exhibit": 9976, "interests": 9977, "kings": 9978, "rubble": 9979, "shoveling": 9980, "snuggle": 9981, "hartnett": 9982, "lucky": 9983, "commit": 9984, "bins": 9985, "recreation": 9986, "deaths": 9987, "plumbers": 9988, "almond": 9989, "goofs": 9990, "lapel": 9991, "seeking": 9992, "littering": 9993, "encounters": 9994, "promising": 9995, "hissing": 9996, "haven": 9997, "messed": 9998, "spare": 9999, "macaw": 10000, "sequel": 10001, "rhinoceros": 10002, "melodious": 10003, "shrugs": 10004, "confessing": 10005, "thickly": 10006, "specialities": 10007, "visualization": 10008, "recycling": 10009, "voicemail": 10010, "clears": 10011, "dias": 10012, "krillen": 10013, "rage": 10014, "drawling": 10015, "sometime": 10016, "creams": 10017, "camps": 10018, "drier": 10019, "lcd": 10020, "hustler": 10021, "increased": 10022, "brian": 10023, "daylight": 10024, "fail": 10025, "clever": 10026, "jester": 10027, "storms": 10028, "cortoon": 10029, "reels": 10030, "hoodies": 10031, "auditioned": 10032, "apocalypse": 10033, "worse": 10034, "soups": 10035, "promoation": 10036, "knobs": 10037, "oh": 10038, "corpse": 10039, "draped": 10040, "kabobs": 10041, "shootout": 10042, "curd": 10043, "continents": 10044, "continued": 10045, "skiier": 10046, "ghee": 10047, "highest": 10048, "largest": 10049, "kimonos": 10050, "curler": 10051, "lassie": 10052, "poolside": 10053, "sanjeev": 10054, "congressman": 10055, "cutted": 10056, "panthers": 10057, "pak": 10058, "circket": 10059, "pendulums": 10060, "telanovella": 10061, "cheerleader": 10062, "tina": 10063, "barbershop": 10064, "melt": 10065, "jike": 10066, "soemthing": 10067, "manually": 10068, "quarter": 10069, "roster": 10070, "clients": 10071, "grew": 10072, "coastline": 10073, "partitions": 10074, "smoky": 10075, "slips": 10076, "trimmed": 10077, "dies": 10078, "swatting": 10079, "choked": 10080, "requested": 10081, "tactical": 10082, "beliefs": 10083, "yolks": 10084, "grated": 10085, "fundamentals": 10086, "seller": 10087, "helmetcam": 10088, "happenings": 10089, "drys": 10090, "blowdrying": 10091, "kazoo": 10092, "kazoos": 10093, "congregation": 10094, "primates": 10095, "hyenas": 10096, "predators": 10097, "bedding": 10098, "coil": 10099, "decals": 10100, "racks": 10101, "barbell": 10102, "scheming": 10103, "scenary": 10104, "pretzel": 10105, "carcasses": 10106, "ruth": 10107, "bro": 10108, "fists": 10109, "oversized": 10110, "straightener": 10111, "mason": 10112, "teriyaki": 10113, "fermentation": 10114, "sportsmen": 10115, "azalea": 10116, "parodying": 10117, "portraits": 10118, "journal": 10119, "sappy": 10120, "hijacking": 10121, "revenant": 10122, "hiting": 10123, "ing": 10124, "expressionlessly": 10125, "agricultural": 10126, "alvin": 10127, "restroom": 10128, "crossover": 10129, "bravely": 10130, "reserve": 10131, "democrats": 10132, "composing": 10133, "useing": 10134, "mush": 10135, "saiko": 10136, "shaquille": 10137, "resteraunt": 10138, "triangular": 10139, "sumthing": 10140, "management": 10141, "interlocking": 10142, "firm": 10143, "tyra": 10144, "faux": 10145, "bleach": 10146, "contests": 10147, "tins": 10148, "friendship": 10149, "rex": 10150, "dinasour": 10151, "fbi": 10152, "surgical": 10153, "dotted": 10154, "buff": 10155, "turbine": 10156, "pumpking": 10157, "imagining": 10158, "prancing": 10159, "viedo": 10160, "grace": 10161, "consoles": 10162, "halves": 10163, "marbles": 10164, "tucking": 10165, "salient": 10166, "sip": 10167, "advancements": 10168, "intercontinental": 10169, "din": 10170, "vivid": 10171, "electrified": 10172, "thrift": 10173, "beginners": 10174, "synthetic": 10175, "deposits": 10176, "instant": 10177, "sabers": 10178, "hynd": 10179, "footprints": 10180, "breakdancing": 10181, "commercials": 10182, "tabby": 10183, "tigs": 10184, "vendors": 10185, "interviewee": 10186, "newsclip": 10187, "teeing": 10188, "schedule": 10189, "symphonic": 10190, "forested": 10191, "pillows": 10192, "scraping": 10193, "joyful": 10194, "moderators": 10195, "fanning": 10196, "execute": 10197, "batsman": 10198, "cubed": 10199, "herbal": 10200, "rosemary": 10201, "stripping": 10202, "flings": 10203, "dame": 10204, "umizoomi": 10205, "profiles": 10206, "font": 10207, "colt": 10208, "arab": 10209, "accepts": 10210, "emerging": 10211, "strobe": 10212, "concludes": 10213, "trampolin": 10214, "narrowly": 10215, "noticing": 10216, "speedy": 10217, "lancaster": 10218, "recieving": 10219, "manchurian": 10220, "skywalker": 10221, "snout": 10222, "handbags": 10223, "wags": 10224, "hen": 10225, "complimenting": 10226, "nissan": 10227, "liberals": 10228, "nd": 10229, "bi": 10230, "substances": 10231, "pillar": 10232, "stalking": 10233, "emblem": 10234, "passat": 10235, "frequencies": 10236, "fossils": 10237, "mountian": 10238, "fishbowl": 10239, "spencer23": 10240, "fund": 10241, "justice": 10242, "thre": 10243, "stinky": 10244, "diners": 10245, "noisy": 10246, "lightsaber": 10247, "cavern": 10248, "freaked": 10249, "pealing": 10250, "vulgar": 10251, "niki": 10252, "nemo": 10253, "dialect": 10254, "poultry": 10255, "trailhead": 10256, "walker": 10257, "arctic": 10258, "alternative": 10259, "kneads": 10260, "freshener": 10261, "peas": 10262, "crumpets": 10263, "britian": 10264, "threaten": 10265, "munsters": 10266, "jfk": 10267, "toucan": 10268, "dslr": 10269, "beiimaan": 10270, "holographic": 10271, "longingly": 10272, "caressed": 10273, "knob": 10274, "casing": 10275, "chidren": 10276, "directed": 10277, "eleven": 10278, "merits": 10279, "entertained": 10280, "lumpy": 10281, "sacks": 10282, "boom": 10283, "patching": 10284, "goatee": 10285, "decor": 10286, "investigates": 10287, "turorial": 10288, "damn": 10289, "regret": 10290, "campaigns": 10291, "innocent": 10292, "fences": 10293, "literally": 10294, "ours": 10295, "suction": 10296, "trhough": 10297, "stressful": 10298, "defence": 10299, "demolition": 10300, "fictitious": 10301, "overtakes": 10302, "donkeys": 10303, "1920s": 10304, "appliances": 10305, "guarding": 10306, "shang": 10307, "nashville": 10308, "conservative": 10309, "islam": 10310, "dealt": 10311, "fcc": 10312, "reform": 10313, "broadband": 10314, "deflated": 10315, "manufacturers": 10316, "goof": 10317, "assassin": 10318, "creed": 10319, "biological": 10320, "bushy": 10321, "utopia": 10322, "photogenic": 10323, "europeans": 10324, "stitches": 10325, "volvo": 10326, "swallows": 10327, "tamer": 10328, "ultraman": 10329, "swan": 10330, "grind": 10331, "intimidate": 10332, "programmed": 10333, "incline": 10334, "traumatic": 10335, "underpass": 10336, "pumps": 10337, "volga": 10338, "swamiji": 10339, "conspiracies": 10340, "spares": 10341, "mumbai": 10342, "brazilian": 10343, "hendrix": 10344, "dutchsinse": 10345, "distributing": 10346, "equinox": 10347, "diffusing": 10348, "immersion": 10349, "chracter": 10350, "unflavored": 10351, "dehydrated": 10352, "terrifying": 10353, "thames": 10354, "influences": 10355, "valentine": 10356, "agility": 10357, "fascinating": 10358, "recommends": 10359, "fumbles": 10360, "testimony": 10361, "compatibility": 10362, "weilding": 10363, "avaril": 10364, "headlock": 10365, "choke": 10366, "descendants": 10367, "willson": 10368, "wahlberg": 10369, "organizations": 10370, "rickshaw": 10371, "malley": 10372, "dancefloor": 10373, "conveying": 10374, "garnished": 10375, "carebears": 10376, "introducer": 10377, "pipelines": 10378, "infrastructure": 10379, "tailored": 10380, "gomez": 10381, "scuffle": 10382, "taliking": 10383, "concerns": 10384, "acrobatic": 10385, "loader": 10386, "junkyard": 10387, "seasonal": 10388, "millennial": 10389, "investment": 10390, "patrolled": 10391, "hisbah": 10392, "peerson": 10393, "devils": 10394, "impersonator": 10395, "ho": 10396, "towers": 10397, "gunpoint": 10398, "pimple": 10399, "skyscraper": 10400, "flicking": 10401, "nutrition": 10402, "narrative": 10403, "estates": 10404, "markers": 10405, "brocolli": 10406, "altering": 10407, "swamp": 10408, "fleshy": 10409, "manufacturer": 10410, "replaced": 10411, "sheryl": 10412, "entrepreneur": 10413, "trigonometry": 10414, "explaination": 10415, "uso": 10416, "recapped": 10417, "glycemic": 10418, "retiring": 10419, "45": 10420, "someon": 10421, "cavities": 10422, "skulls": 10423, "gran": 10424, "snows": 10425, "pharmacists": 10426, "wiggle": 10427, "sloth": 10428, "autotuned": 10429, "montages": 10430, "overseeing": 10431, "rafts": 10432, "delta": 10433, "kugar": 10434, "grandfather": 10435, "washington": 10436, "servers": 10437, "dabbing": 10438, "etheridge": 10439, "gums": 10440, "plantation": 10441, "nelson": 10442, "grenn": 10443, "involvement": 10444, "scoreboard": 10445, "tilburg": 10446, "youtubers": 10447, "campground": 10448, "troubleshooting": 10449, "tranformers": 10450, "tlaking": 10451, "screened": 10452, "ptsd": 10453, "mosquitos": 10454, "injects": 10455, "marshall": 10456, "diagnosed": 10457, "veteran": 10458, "scallion": 10459, "scallions": 10460, "rodeo": 10461, "legislation": 10462, "natto": 10463, "veyron": 10464, "fyi": 10465, "wordpress": 10466, "wells": 10467, "socket": 10468, "sperms": 10469, "iceland": 10470, "tram": 10471, "khalifa": 10472, "rockclimbing": 10473, "voters": 10474, "prevention": 10475, "stimulates": 10476, "dunes": 10477, "oscar": 10478, "wifi": 10479, "pepperoni": 10480, "cheerleading": 10481, "bagels": 10482, "bib": 10483, "benchazi": 10484, "muscae": 10485, "volitantes": 10486, "thyme": 10487, "lester": 10488, "raichu": 10489, "lugia": 10490, "founder": 10491, "hurdlers": 10492, "induction": 10493, "keyboards": 10494, "horseriding": 10495, "sparks": 10496, "decal": 10497, "sew": 10498, "girk": 10499, "matress": 10500, "famine": 10501, "cr": 10502, "mit": 10503, "sandcastle": 10504, "heath": 10505, "caine": 10506, "textbooks": 10507, "corrected": 10508, "vtec": 10509} \ No newline at end of file diff --git a/applications/TableTennis/ActionRecognition/README.md b/applications/TableTennis/ActionRecognition/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bd60bc8eff0d5640a8404e4b7c3ac074224ef2f0 --- /dev/null +++ b/applications/TableTennis/ActionRecognition/README.md @@ -0,0 +1,98 @@ +# 乒乓球动作识别模型 + + +## 内容 +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型推理](#模型推理) +- [模型优化](#模型优化) +- [模型部署](#模型部署) +- [参考论文](#参考论文) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install imageio +``` + +## 模型简介 +该代码库用于乒乓球动作识别, 基于paddle2.2版本开发,结合PaddleVideo中的VideoSwinTransformer模型,对给定的乒乓球视频进行动作分类。 +主要分为如下几步 + - 图像特征抽取,SwinTransformer3D + - 动作分类,I3DHead + + +## 数据准备 + +TODO + +## 模型训练 +主要代码来自VideoSwin模型:[VideoSwin](../../../docs/zh-CN/model_zoo/recognition/videoswin.md) + +1. 使用VideoSwin在K400上的预训练模型基础上进行finetune,因此首先下载K400的预训练模型并放置到`data`目录下 + ```bash + wget -P data/ https://videotag.bj.bcebos.com/PaddleVideo-release2.2/VideoSwin_k400.pdparams + ``` + +2. 使用`TableTennis/ActionRecognition/configs/videoswin_tabletennis.yaml`配置文件进行训练 + 训练启动命令如下: + ```bash + # 单卡 + python3.7 -u main.py --amp --validate -c applications/TableTennis/ActionRecognition/configs/videoswin_tabletennis.yaml + # 多卡 + python3.7 -u -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin_tabletennis main.py --amp --validate -c applications/TableTennis/ActionRecognition/configs/videoswin_tabletennis.yaml + ``` + +## 模型评估 + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin_tabletennis main.py --test -c configs/recognition/video_swin_transformer/videoswin_tabletennis.yaml -w "output/VideoSwin_TableTennis/VideoSwin_TableTennis_best.pdparams" +``` + +## 模型推理 + +我们提供了一个在乒乓球数据集上训练好的模型以及一个乒乓球样例的视频pkl文件,以供测试 +``` +wget -P data/ https://videotag.bj.bcebos.com/PaddleVideo-release2.2/VideoSwin_tennis.pdparams # 下载乒乓球数据集上训练好的模型 +wget -P data/ https://videotag.bj.bcebos.com/Data/example_tennis.pkl # 下载乒乓球样例输入视频pkl文件 +``` + +### 导出推理模型 +``` +python3.7 tools/export_model.py -c applications/TableTennis/ActionRecognition/configs/videoswin_tabletennis.yaml \ + -p output/VideoSwin_TableTennis/VideoSwin_TableTennis_best.pdparams \ + -o inference/VideoSwin_TableTennis +``` +上述命令会根据传入的`.pdparams`模型,在`inference/VideoSwin_TableTennis`文件夹下生成推理模型,主要包括3个文件:`VideoSwin_TableTennis.pdiparams`、`VideoSwin_TableTennis.pdmodel`、`VideoSwin_TableTennis.info` + +### 使用推理模型 +测试文件使用`.pkl`文件,其包含了已抽取的用于预测的乒乓球视频帧。 +运行预测代码 +```bash +python3.7 tools/predict.py --input_file data/example_tennis_7.pkl \ + --config applications/TableTennis/ActionRecognition/configs/videoswin_tabletennis.yaml \ + --model_file inference/VideoSwin_TableTennis/VideoSwin_TableTennis.pdmodel \ + --params_file inference/VideoSwin_TableTennis/VideoSwin_TableTennis.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` +执行以上命令会产出一个原视频叠加预测结果文本(Top1类别+概率)的gif图片,保存在本目录的results文件夹下,gif文件名与输入的pkl文件名相同。 +效果如下图: + +![example_7.gif](results/example_tennis_7.gif) + + +## 模型优化 +在实际使用场景中可根据视频内容尝试优化策略 +- 可根据动作持续时间的长短,调整采样的段数num_seg和段内采样的帧数seg_len +- 可以根据数据集大小调整模型训练的超参数,包括权重衰减、DropOut概率、学习率、更换优化器等,以获得更优的结果。 +- 本代码的backbone部分可以作为视频特征提取模块,代替其它的动作识别backbone,以获得表征能力更强的视频特征,以提升整体任务的精度。 + + +## 模型部署 +TODO + + +## 参考论文 + +- [Video Swin Transformer](https://arxiv.org/pdf/2106.13230.pdf), Ze Liu, Jia Ning, Yue Cao, Yixuan Wei diff --git a/applications/TableTennis/ActionRecognition/configs/videoswin_tabletennis.yaml b/applications/TableTennis/ActionRecognition/configs/videoswin_tabletennis.yaml new file mode 100644 index 0000000000000000000000000000000000000000..92ee29b730ae0fb8928c990f974b5d82fc9944a0 --- /dev/null +++ b/applications/TableTennis/ActionRecognition/configs/videoswin_tabletennis.yaml @@ -0,0 +1,170 @@ +MODEL: #MODEL field + framework: "RecognizerTransformer" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "SwinTransformer3D" #Mandatory, The name of backbone. + pretrained: "data/SwinTransformer_k400.pdparams" #Optional, pretrained model path. + patch_size: [2, 4, 4] + embed_dim: 128 + depths: [2, 2, 18, 2] + num_heads: [4, 8, 16, 32] + window_size: [8, 7, 7] + mlp_ratio: 4. + qkv_bias: True + qk_scale: None + drop_rate: 0.0 + attn_drop_rate: 0.0 + drop_path_rate: 0.2 + patch_norm: True + head: + name: "I3DHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 8 #Optional, the number of classes to be classified. + in_channels: 1024 #input channel of the extracted feature. + spatial_type: 'avg' + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + runtime_cfg: # configuration used when the model is train or test. + test: # test config + num_seg: 32 + avg_type: 'prob' # 'score' or 'prob' + +DATASET: #DATASET field + batch_size: 1 #Mandatory, bacth size + num_workers: 2 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 1 + train: + format: "FrameDataset_Sport" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "train.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset_Sport" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset_Sport" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + +PIPELINE: #PIPELINE field TODO..... + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "SamplerPkl" + num_seg: 1 + seg_len: 32 + valid_mode: False + backend: 'cv2' + transform: #Mandotary, image transform operator. + - Scale: + short_size: 256 + fixed_ratio: False + keep_ratio: True + backend: 'cv2' + do_round: True + - RandomResizedCrop: + backend: 'cv2' + - Scale: + short_size: 224 + fixed_ratio: False + keep_ratio: False + backend: 'cv2' + do_round: True + - RandomFlip: + - Normalization: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + tensor_shape: [3, 1, 1, 1] + inplace: True + - Image2Array: + data_format: 'cthw' + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "SamplerPkl" + num_seg: 1 + seg_len: 32 + valid_mode: True + backend: 'cv2' + transform: #Mandotary, image transform operator. + - Scale: + short_size: 256 + fixed_ratio: False + keep_ratio: True + backend: 'cv2' + do_round: True + - CenterCrop: + target_size: 224 + do_round: False + backend: 'cv2' + - Normalization: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + tensor_shape: [3, 1, 1, 1] + inplace: True + - Image2Array: + data_format: 'cthw' + test: + decode: + name: "FrameDecoder" + sample: + name: "SamplerPkl" + num_seg: 1 + seg_len: 32 + valid_mode: True + backend: 'cv2' + transform: #Mandotary, image transform operator. + - Scale: + short_size: 256 + fixed_ratio: False + keep_ratio: True + backend: 'cv2' + do_round: True + - UniformCrop: + target_size: 224 + backend: 'cv2' + - Normalization: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + tensor_shape: [3, 1, 1, 1] + inplace: True + - Image2Array: + data_format: 'cthw' +OPTIMIZER: + name: 'AdamW' + beta1: 0.9 + beta2: 0.999 + no_weight_decay_name: 'norm relative_position_bias_table' + learning_rate: + name: 'CustomWarmupCosineStepDecay' + iter_step: True + warmup_iters: 2 + warmup_ratio: 0.1 + min_lr: 0 + base_lr: 1e-5 + max_epoch: 20 + weight_decay: 0.05 + +METRIC: + name: 'CenterCropMetric' + +GRADIENT_ACCUMULATION: + global_batch_size: 64 + +MIX: + name: "Mixup" + alpha: 0.2 + +INFERENCE: + name: 'VideoSwin_TableTennis_Inference_helper' + num_seg: 1 + seg_len: 32 + short_size: 224 + target_size: 224 + +model_name: "VideoSwin_TableTennis" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 5 +epochs: 20 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/applications/TableTennis/ActionRecognition/results/example_tennis_7.gif b/applications/TableTennis/ActionRecognition/results/example_tennis_7.gif new file mode 100644 index 0000000000000000000000000000000000000000..df98ad562dcc2aea19d3083d12b8a0b7f886585c Binary files /dev/null and b/applications/TableTennis/ActionRecognition/results/example_tennis_7.gif differ diff --git a/applications/TableTennis/configs/bmn_tabletennis.yaml b/applications/TableTennis/configs/bmn_tabletennis.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c629e28be466b78882335f9c73446add8c479158 --- /dev/null +++ b/applications/TableTennis/configs/bmn_tabletennis.yaml @@ -0,0 +1,99 @@ +MODEL: #MODEL field + framework: "BMNLocalizer" + backbone: + name: "BMN" + feat_dim: 2048 + tscale: 200 + dscale: 200 + prop_boundary_ratio: 0.5 + num_sample: 32 + num_sample_perbin: 3 + loss: + name: "BMNLoss" + tscale: 200 + dscale: 200 + +DATASET: #DATASET field + batch_size: 4 #single card bacth size + test_batch_size: 1 + num_workers: 8 + train: + format: "BMNDataset" + file_path: "/home/aistudio/work/BMN/Input_for_bmn/label_fixed.json" + subset: "train" + valid: + format: "BMNDataset" + file_path: "/home/aistudio/work/BMN/Input_for_bmn/label_fixed.json" + subset: "validation" + test: + format: "BMNDataset" + test_mode: True + file_path: "/home/aistudio/work/BMN/Input_for_bmn/label_fixed.json" + subset: "validation" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data + load_feat: + name: "LoadFeat" + feat_path: "/home/aistudio/work/BMN/Input_for_bmn/feature" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + + valid: #Mandotary, indicate the pipeline to deal with the training data + load_feat: + name: "LoadFeat" + feat_path: "/home/aistudio/work/BMN/Input_for_bmn/feature" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + + test: #Mandatory, indicate the pipeline to deal with the validing data + load_feat: + name: "LoadFeat" + feat_path: "/home/aistudio/work/BMN/Input_for_bmn/feature" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 200 + - GetVideoLabel: + tscale: 200 + dscale: 200 + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + iter_step: True + name: 'CustomPiecewiseDecay' + boundaries: [4200] + values: [0.001, 0.0001] + weight_decay: + name: 'L2' + value: 1e-4 + +METRIC: + name: 'BMNMetric' + tscale: 200 + dscale: 200 + file_path: "/home/aistudio/work/BMN/Input_for_bmn/label_fixed.json" + ground_truth_filename: "/home/aistudio/work/BMN/Input_for_bmn/label_gts.json" + subset: "validation" + output_path: "data/bmn/BMN_Test_output" + result_path: "data/bmn/BMN_Test_results" + +INFERENCE: + name: 'BMN_Inference_helper' + feat_dim: 2048 + dscale: 200 + tscale: 200 + result_path: "data/bmn/BMN_INFERENCE_results" + +model_name: BMN +epochs: 20 # Mandatory, total epoch +log_level: "INFO" +resume_from: "" #checkpoint path. diff --git a/applications/TableTennis/datasets/script/submission_format_transfer.py b/applications/TableTennis/datasets/script/submission_format_transfer.py new file mode 100644 index 0000000000000000000000000000000000000000..ecd3487c28cc8e6efa0f9f50f23ed2dd8f1c3d7d --- /dev/null +++ b/applications/TableTennis/datasets/script/submission_format_transfer.py @@ -0,0 +1,64 @@ +import json +import math + +with open('/workspace/bianjiang03/DATA/Output_for_bmn/prop.json') as f: + data = json.load(f) +f.close() + +transferred = dict() + +# 25 fps for all videos +fps = 25 + +for item in data: + temp = [] + for seg in item['bmn_results']: + temp_dict = { + 'score': seg['score'], + 'segment': + [round(seg['start'] / fps, 2), + round(seg['end'] / fps, 2)] + } + temp.append(temp_dict) + transferred[item['video_name']] = temp + +target_format = { + 'version': 'A-test', + 'results': transferred, + 'external_data': {} +} + +jsonString = json.dumps(target_format, indent=4, ensure_ascii=False) +jsonFile = open('/workspace/bianjiang03/DATA/Output_for_bmn/submission.json', + 'w') +jsonFile.write(jsonString) +jsonFile.close() + +# target format +# { +# "version": NA, +# "results": { +# "name_of_clip_1": [ +# { +# "score": 0.64, +# "segment": [2.33,3.15] +# }, +# { +# "score": 0.77, +# "segment": [7.64, 7.84] +# } +# ], +# "name_of_clip_2": [ +# { +# "score": 0.84, +# "segment": [9.73,10.15] +# }, +# { +# "score": 0.87, +# "segment": [17.11, 17.84] +# } +# ], +# ... +# } +# "external_data": {} +# } diff --git a/applications/TableTennis/extractor/configs/configs.yaml b/applications/TableTennis/extractor/configs/configs.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a776c5c86277fe3334b4123ef488005360b9bb41 --- /dev/null +++ b/applications/TableTennis/extractor/configs/configs.yaml @@ -0,0 +1,61 @@ +COMMON: + fps: 25 + use_gpu: True + label_dic: '/extractor/configs/index_label_tabletennis_14.json' + # debug + PCM_ONLY: False + DEBUG: False + BMN_ONLY: False + LSTM_ONLY: False + +PPTSM: + name: "PPTSM" + model_file: "/extractor/ppTSM.pdmodel" + params_file: "/extractor/ppTSM.pdiparams" + gpu_mem: 8000 + device_id: 0 + seg_num: 8 + seglen: 1 + short_size: 256 + target_size: 224 + batch_size: 32 + image_mean: [0.485, 0.456, 0.406] + image_std: [0.229, 0.224, 0.225] + reader_threads: 12 + buf_size: 1024 + +AUDIO: + name: "AUDIO" + model_file: "/checkpoints/AUDIO/__model__" + params_file: "/checkpoints/AUDIO/__param__" + gpu_mem: 8000 + device_id: 0 + sample_rate: 16000 + batch_size: 32 + +BMN: + name: "BMN" + model_file: "/inference/BMN/BMN.pdmodel" + params_file: "/inference/BMN/BMN.pdiparams" + gpu_mem: 8000 + device_id: 0 + window_step: 200 # 200 + tscale: 200 + dscale: 200 + batch_size: 8 # 8 + nms_thread: 0.7 + score_thread: 0.5 # 0.05 + +ACTION: + name: "ACTION" + model_file: "/train_lstm/inference/__model__" + params_file: "train_lstm/inference/__param__" + gpu_mem: 8000 + device_id: 0 + batch_size: 32 + topk: 1 + nms_thread: 0.01 + nms_offset: 10 + + classify_score_thread: 0.05 # 0.15 + iou_score_thread: 0.1 # 0.4 diff --git a/applications/TableTennis/extractor/extract_bmn_for_tabletennis.py b/applications/TableTennis/extractor/extract_bmn_for_tabletennis.py new file mode 100644 index 0000000000000000000000000000000000000000..9da8e12a5cd76046858538e32c6f9ad71e625b8a --- /dev/null +++ b/applications/TableTennis/extractor/extract_bmn_for_tabletennis.py @@ -0,0 +1,93 @@ +#!./python27-gcc482/bin/python +# coding: utf-8 +""" +BAIDU CLOUD action +""" + +import os +import sys +import pickle +import json +import time +import shutil + +import numpy as np + +sys.path.append( + "/workspace/bianjiang03/App_TableTennis/PaddleVideo/FootballAction/predict/action_detect" +) +import models.bmn_infer as prop_model +from utils.preprocess import get_images +from utils.config_utils import parse_config, print_configs +import utils.config_utils as config_utils + +import logger + +logger = logger.Logger() + + +def load_model(cfg_file="configs/configs.yaml"): + """ + load_model + """ + logger.info("load model ... ") + global infer_configs + infer_configs = parse_config(cfg_file) + print_configs(infer_configs, "Infer") + + t0 = time.time() + global prop_model + prop_model = prop_model.InferModel(infer_configs) + t1 = time.time() + logger.info("step0: load model time: {} min\n".format((t1 - t0) * 1.0 / 60)) + + +def video_classify(video_name, dataset_dir): + """ + extract_feature + """ + logger.info('predict ... ') + logger.info(video_name) + + # step 1: extract feature + + feature_path = dataset_dir + video_name + video_features = pickle.load(open(feature_path, 'rb')) + print('===video_features===', video_name) + + # step2: get proposal + t0 = time.time() + bmn_results = prop_model.predict(infer_configs, material=video_features) + t1 = time.time() + logger.info(np.array(bmn_results).shape) + logger.info("step2: proposal time: {} min".format((t1 - t0) * 1.0 / 60)) + + return bmn_results + + +if __name__ == '__main__': + dataset_dir = '/workspace/bianjiang03/DATA/Features_competition_test_A/' + output_dir = '/workspace/bianjiang03/DATA' + if not os.path.exists(output_dir + '/Output_for_bmn'): + os.mkdir(output_dir + '/Output_for_bmn') + results = [] + + load_model() + + directory = os.fsencode(dataset_dir) + + for file in os.listdir(directory): + filename = os.fsdecode(file) + bmn_results = video_classify(filename, dataset_dir) + results.append({ + 'video_name': filename.split('.pkl')[0], + 'num_proposal': len(bmn_results), + 'bmn_results': bmn_results + }) + + with open(output_dir + '/Output_for_bmn/prop.json', 'w', + encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + + print('Done with the inference!') diff --git a/applications/TableTennis/fix_bad_label.py b/applications/TableTennis/fix_bad_label.py new file mode 100644 index 0000000000000000000000000000000000000000..3d99ed1428bf0940a11581b3c3eccdb7d14e477e --- /dev/null +++ b/applications/TableTennis/fix_bad_label.py @@ -0,0 +1,37 @@ +import copy +import json +import re +import os + +url = '/home/aistudio/work/BMN/Input_for_bmn/feature/' +directory = os.fsencode(url) +count = 0 +target_set = [] + +for file in os.listdir(directory): + filename = os.fsdecode(file) + target_name = filename.split('.npy')[0] + target_set.append(target_name) + count += 1 +print('Feature size:', len(target_set)) + +with open('/home/aistudio/work/BMN/Input_for_bmn/label.json') as f: + data = json.load(f) + +delet_set = [] +for key in data.keys(): + if not key in target_set: + delet_set.append(key) + +print('(Label) Original size:', len(data)) +print('(Label) Deleted size:', len(delet_set)) + +for item in delet_set: + data.pop(item, None) + +print('(Label) Fixed size:', len(data)) + +jsonString = json.dumps(data, indent=4, ensure_ascii=False) +jsonFile = open('/home/aistudio/work/BMN/Input_for_bmn/label_fixed.json', 'w') +jsonFile.write(jsonString) +jsonFile.close() diff --git a/applications/TableTennis/get_instance_for_bmn.py b/applications/TableTennis/get_instance_for_bmn.py new file mode 100644 index 0000000000000000000000000000000000000000..40e681e7070a8f2650b60bda74208f226ef65e2c --- /dev/null +++ b/applications/TableTennis/get_instance_for_bmn.py @@ -0,0 +1,227 @@ +""" +get instance for bmn +使用winds=8的滑窗,将所有子窗口的长度之和小于winds的进行合并 +合并后,父窗口代表bmn训练数据,子窗口代表tsn训练数据 +""" +import os +import sys +import json +import random +import pickle +import numpy as np +import math + +# for table tennis +bmn_window = 8 +dataset = "/home/aistudio/work/BMN/" +feat_dir = dataset + '/Features_example' +out_dir = dataset + '/Input_for_bmn' +label_files = { + 'train': 'label_cls14_small_train.json', + 'validation': 'label_cls14_small_test.json' +} + +global fps + + +def gen_gts_for_bmn(gts_data): + """ + @param, gts_data, original gts for action detection + @return, gts_bmn, output gts dict for bmn + """ + fps = gts_data['fps'] + gts_bmn = {'fps': fps, 'gts': []} + for sub_item in gts_data['gts']: + url = sub_item['url'] + + max_length = sub_item['total_frames'] + + gts_bmn['gts'].append({ + 'url': url, + 'total_frames': max_length, + 'root_actions': [] + }) + sub_actions = sub_item['actions'] + # 跳过没有动作的片段 + if len(sub_actions) == 0: + continue + # duration > bmn_window, 动作持续时间大于bmn_windows,直接删除 + for idx, sub_action in enumerate(sub_actions): + if sub_action['end_id'] - sub_action['start_id'] > bmn_window: + sub_actions.pop(idx) + + # 【滑动窗口,把每一个视频里的动作片段提取出来】 + root_actions = [sub_actions[0]] + # before_id, 前一动作的最后一帧 + # after_id, 后一动作的第一帧 + before_id = 0 + for idx in range(1, len(sub_actions)): + cur_action = sub_actions[idx] + duration = (cur_action['end_id'] - root_actions[0]['start_id']) + if duration > bmn_window: # windows只能包住一个动作就包,包不住就包多个 + after_id = cur_action['start_id'] + gts_bmn['gts'][-1]['root_actions'].append({ + 'before_id': + before_id, + 'after_id': + after_id, + 'actions': + root_actions + }) + before_id = root_actions[-1]['end_id'] #更新滑窗 + root_actions = [cur_action] + else: + root_actions.append(cur_action) + if idx == len(sub_actions) - 1: + after_id = max_length + gts_bmn['gts'][-1]['root_actions'].append({ + 'before_id': + before_id, + 'after_id': + after_id, + 'actions': + root_actions + }) + + return gts_bmn + + +def combile_gts(gts_bmn, gts_process, mode): + """ + 1、bmn_window 范围内只有一个动作,只取一个目标框 + 2、bmn_window 范围内有多个动作,取三个目标框(第一个动作、最后一个动作、所有动作) + """ + global fps + fps = gts_process['fps'] + duration_second = bmn_window * 1.0 + duration_frame = bmn_window * fps + feature_frame = duration_frame + for item in gts_process['gts']: + url = item['url'] + basename = os.path.basename(url).split('.')[0] + root_actions = item['root_actions'] + # 把每一个视频里的动作片段提取出来 + for root_action in root_actions: + segments = [] + # all actions + segments.append({ + 'actions': root_action['actions'], + 'before_id': root_action['before_id'], + 'after_id': root_action['after_id'] + }) + if len(root_action['actions']) > 1: #如果有多个动作,则第一个动作和最后一个动作,额外添加一次 + # first action + segments.append({ + 'actions': [root_action['actions'][0]], + 'before_id': + root_action['before_id'], + 'after_id': + root_action['actions'][1]['start_id'] + }) + # last action + segments.append({ + 'actions': [root_action['actions'][-1]], + 'before_id': + root_action['actions'][-2]['end_id'], + 'after_id': + root_action['after_id'] + }) + + # 把动作片段处理成window size大小,以适配BMN输入 + for segment in segments: + before_id = segment['before_id'] + after_id = segment['after_id'] + actions = segment['actions'] + # before_id到after_id太长了,从里面取window_size帧,要先确定一个起始点,然后动作都要包住 + box0 = max(actions[-1]['end_id'] - bmn_window, + before_id) #确定起始点 + box1 = min(actions[0]['start_id'], + after_id - bmn_window) #确实起始点 + if box0 <= box1: # 一次检查 + if int(box0) - int(box1) == 0: + cur_start = box0 + else: + box0 = math.ceil(box0) + box1 = int(box1) + cur_start = random.randint(box0, box1) + cur_end = cur_start + bmn_window + cur_start = round(cur_start, 2) + cur_end = round(cur_end, 2) + name = '{}_{}_{}'.format(basename, cur_start, cur_end) + annotations = [] + for action in actions: + label = str(1.0 * action['label_ids'][0]) + label_name = action['label_names'][0] + seg0 = 1.0 * round((action['start_id'] - cur_start), + 2) #存储的是到开始位置(时间: s)的距离 + seg1 = 1.0 * round((action['end_id'] - cur_start), 2) + annotations.append({ + 'segment': [seg0, seg1], + 'label': label, + 'label_name': label_name + }) + gts_bmn[name] = { + 'duration_second': duration_second, + 'duration_frame': duration_frame, + 'feature_frame': feature_frame, + 'subset': mode, + 'annotations': annotations + } + + return gts_bmn + + +def save_feature_to_numpy(gts_bmn, folder): + global fps + print('save feature for bmn ...') + if not os.path.exists(folder): + os.mkdir(folder) + process_gts_bmn = {} + miss = 0 + for item, value in gts_bmn.items(): + # split to rsplit 针对文件命名修改 + basename, start_id, end_id = item.rsplit('_', 2) + if not basename in process_gts_bmn: + process_gts_bmn[basename] = [] + process_gts_bmn[basename].append({ + 'name': item, + 'start': float(start_id), + 'end': float(end_id) + }) + for item, values in process_gts_bmn.items(): + feat_path = os.path.join(feat_dir, item + '.pkl') + feature_video = pickle.load(open(feat_path, 'rb'))['image_feature'] + for value in values: + save_cut_name = os.path.join(folder, value['name']) + a, b, c = save_cut_name.rsplit('_', 2) + if float(b) > 360: + print(b) + start_frame = round(value['start'] * fps) + end_frame = round(value['end'] * fps) + if end_frame > len(feature_video): + miss += 1 + continue + feature_cut = [ + feature_video[i] for i in range(start_frame, end_frame) + ] + np_feature_cut = np.array(feature_cut, dtype=np.float32) + np.save(save_cut_name, np_feature_cut) + + print('miss number (broken sample):', miss) + + +if __name__ == "__main__": + if not os.path.exists(out_dir): + os.mkdir(out_dir) + gts_bmn = {} + for item, value in label_files.items(): + label_file = os.path.join(dataset, value) + gts_data = json.load(open(label_file, 'rb')) + gts_process = gen_gts_for_bmn(gts_data) + gts_bmn = combile_gts(gts_bmn, gts_process, item) + + with open(out_dir + '/label.json', 'w', encoding='utf-8') as f: + data = json.dumps(gts_bmn, indent=4, ensure_ascii=False) + f.write(data) + + save_feature_to_numpy(gts_bmn, out_dir + '/feature') diff --git a/applications/TableTennis/gts_format_transfer.py b/applications/TableTennis/gts_format_transfer.py new file mode 100644 index 0000000000000000000000000000000000000000..ad1c51ecfc37d2badd37784b686ad00dd71a33f8 --- /dev/null +++ b/applications/TableTennis/gts_format_transfer.py @@ -0,0 +1,12 @@ +import json + +with open('/home/aistudio/work/BMN/Input_for_bmn/label_fixed.json') as f: + data = json.load(f) +f.close() + +target_format = {'taxonomy': None, 'database': data, 'version': None} + +jsonString = json.dumps(target_format, indent=4, ensure_ascii=False) +jsonFile = open('/home/aistudio/work/BMN/Input_for_bmn/label_gts.json', 'w') +jsonFile.write(jsonString) +jsonFile.close() diff --git a/applications/TableTennis/predict/action_detect/action.py b/applications/TableTennis/predict/action_detect/action.py new file mode 100644 index 0000000000000000000000000000000000000000..c1776981f55a7deb596f67b5699942453cb71984 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/action.py @@ -0,0 +1,186 @@ +#!./python27-gcc482/bin/python +# coding: utf-8 +""" +BAIDU CLOUD action +""" + +import os +import sys +import pickle +import json +import time +import functools + +import numpy as np + +from utils.preprocess import get_images +from utils.config_utils import parse_config, print_configs +import mfcc.feature_extractor as mfcc_extractor + +import models.pptsm_infer as image_model +import models.audio_infer as audio_model +import models.bmn_infer as prop_model +import models.lstm_infer as classify_model + +import logger + +logger = logger.Logger() + + +def record_time_info(func): + """decorator func to log cost time for func + """ + @functools.wraps(func) + def timer(*args): + """log cost time for func + """ + logger.info("function [{}] processing ...".format(func.__name__)) + start_time = time.time() + retval = func(*args) + cost_time = round(time.time() - start_time, 5) + logger.info("function [{}] run time: {:.2f} min".format( + func.__name__, cost_time / 60)) + return retval + + return timer + + +class ActionDetection(object): + """ModelPredict""" + def __init__(self, cfg_file="configs/configs.yaml"): + cfg = parse_config(cfg_file) + self.configs = cfg + print_configs(self.configs, "Infer") + + name = 'COMMON' + self.DEBUG = cfg[name]['DEBUG'] + self.BMN_ONLY = cfg[name]['BMN_ONLY'] + self.LSTM_ONLY = cfg[name]['LSTM_ONLY'] + self.PCM_ONLY = cfg[name]['PCM_ONLY'] + if self.LSTM_ONLY: + self.prop_dict = {} + for dataset in ['EuroCup2016']: + prop_json = '/home/work/datasets/{}/feature_bmn/prop.json'.format( + dataset) + json_data = json.load(open(prop_json, 'r')) + for item in json_data: + basename = prop_json.replace('feature_bmn/prop.json', 'mp4') + basename = basename + '/' + item['video_name'] + '.mp4' + self.prop_dict[basename] = item['bmn_results'] + + @record_time_info + def load_model(self): + """ + load_model + """ + if not self.DEBUG: + self.image_model = image_model.InferModel(self.configs) + if not self.PCM_ONLY: + self.audio_model = audio_model.InferModel(self.configs) + + if not self.LSTM_ONLY: + self.prop_model = prop_model.InferModel(self.configs) + + if not self.BMN_ONLY: + self.classify_model = classify_model.InferModel(self.configs) + + logger.info("==> Action Detection prepared.") + + @record_time_info + def infer(self, imgs_path, pcm_path, fps=5): + """ + extract_feature + """ + self.imgs_path = imgs_path + self.pcm_path = pcm_path + self.configs['COMMON']['fps'] = fps + + logger.info("==> input video {}".format(os.path.basename( + self.imgs_path))) + + # step 1: extract feature + video_features = self.extract_feature() + + # step2: get proposal + bmn_results = self.extract_proposal(video_features) + + # step3: classify + material = {'feature': video_features, 'proposal': bmn_results} + action_results = self.video_classify(material) + + return bmn_results, action_results + + @record_time_info + def video_classify(self, material): + """video classify""" + if self.BMN_ONLY: + return [] + action_results = self.classify_model.predict(self.configs, + material=material) + logger.info('action shape {}'.format(np.array(action_results).shape)) + return action_results + + @record_time_info + def extract_proposal(self, video_features): + """extract proposal""" + if self.LSTM_ONLY: + basename = self.imgs_path.replace('frames', 'mp4') + '.mp4' + bmn_results = self.prop_dict[basename] + return bmn_results + bmn_results = self.prop_model.predict(self.configs, + material=video_features) + logger.info('proposal shape {}'.format(np.array(bmn_results).shape)) + return bmn_results + + @record_time_info + def extract_feature(self): + """extract feature""" + if not self.DEBUG: + image_path_list = get_images(self.imgs_path) + self.configs['PPTSM']['frame_list'] = image_path_list + self.configs['AUDIO']['pcm_file'] = self.pcm_path + image_features = self.image_model.predict(self.configs) + if self.PCM_ONLY: + sample_rate = self.configs['AUDIO']['sample_rate'] + pcm_features = mfcc_extractor.extract_pcm( + self.pcm_path, sample_rate) + audio_features = [] + else: + audio_features, pcm_features = self.audio_model.predict( + self.configs) + + np_image_features = np.array(image_features, dtype=np.float32) + np_audio_features = np.array(audio_features, dtype=np.float32) + np_pcm_features = np.array(pcm_features, dtype=np.float32) + + video_features = { + 'image_feature': np_image_features, + 'audio_feature': np_audio_features, + 'pcm_feature': np_pcm_features + } + else: + feature_path = self.imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + logger.info("feature shape {} {} {}".format( + video_features['image_feature'].shape, + video_features['audio_feature'].shape, + video_features['pcm_feature'].shape)) + + return video_features + + +if __name__ == '__main__': + + model_predict = ActionDetection(cfg_file="../configs/configs.yaml") + model_predict.load_model() + + imgs_path = "/home/work/datasets/EuroCup2016/frames/1be705a8f67648da8ec4b4296fa80895" + pcm_path = "/home/work/datasets/EuroCup2016/pcm/1be705a8f67648da8ec4b4296fa80895.pcm" + + bmn_results, action_results = model_predict.infer(imgs_path, pcm_path) + results = {'bmn_results': bmn_results, 'action_results': action_results} + + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) diff --git a/applications/TableTennis/predict/action_detect/logger.py b/applications/TableTennis/predict/action_detect/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..5d1c3bb9a2601f061c451eda532b56976fba2c57 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/logger.py @@ -0,0 +1,24 @@ +""" +logger +""" +import os +import logging + + +class Logger(logging.Logger): + """Customized logger for news stripper + """ + def __init__(self): + super(Logger, self).__init__(self) + if not os.path.exists('logs'): + os.mkdir('logs') + handler = logging.FileHandler("logs/action_detect.log") + # handler.setLevel(logging.DEBUG) + handler.setLevel(logging.INFO) + + format = "%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d %(message)s" + datefmt = "%y-%m-%d %H:%M:%S" + + formatter = logging.Formatter(format, datefmt) + handler.setFormatter(formatter) + self.addHandler(handler) diff --git a/applications/TableTennis/predict/action_detect/mfcc/__init__.py b/applications/TableTennis/predict/action_detect/mfcc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/TableTennis/predict/action_detect/mfcc/feature_extractor.py b/applications/TableTennis/predict/action_detect/mfcc/feature_extractor.py new file mode 100755 index 0000000000000000000000000000000000000000..505f923ccc2039109d4d2ef38dfea0c52daae4c4 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/mfcc/feature_extractor.py @@ -0,0 +1,183 @@ +""" +audio feature extract +""" +# coding: utf-8 +import os +import numpy as np +import pickle +import mfcc.vgg_params as vgg_params +import sys + + +def frame(data, window_length, hop_length): + """ + frame + """ + num_samples = data.shape[0] + #print("window_length , hop_length", window_length, hop_length) + #print("num_sample = ", num_samples) + num_frames = 1 + int(np.floor((num_samples - window_length) / hop_length)) + #print(" num_frames = ", num_frames) + shape = (num_frames, window_length) + data.shape[1:] + #print(" shape = ", shape) + strides = (data.strides[0] * hop_length, ) + data.strides + #print("data.strides = ", data.strides) + #print("strides = ", strides) + return np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides) + + +def periodic_hann(window_length): + """ + periodic_hann + """ + return 0.5 - (0.5 * + np.cos(2 * np.pi / window_length * np.arange(window_length))) + + +def stft_magnitude(signal, fft_length, hop_length=None, window_length=None): + """ + stft_magnitude + """ + frames = frame(signal, window_length, hop_length) + window = periodic_hann(window_length) + windowed_frames = frames * window + return np.abs(np.fft.rfft(windowed_frames, int(fft_length))) + + +_MEL_BREAK_FREQUENCY_HERTZ = 700.0 +_MEL_HIGH_FREQUENCY_Q = 1127.0 + + +def hertz_to_mel(frequencies_hertz): + """ + hertz_to_mel + """ + return _MEL_HIGH_FREQUENCY_Q * np.log(1.0 + (frequencies_hertz / + _MEL_BREAK_FREQUENCY_HERTZ)) + + +def spectrogram_to_mel_matrix(num_mel_bins=20, + num_spectrogram_bins=129, + audio_sample_rate=8000, + lower_edge_hertz=125.0, + upper_edge_hertz=3800.0): + """ + spectrogram_to_mel_matrix + """ + nyquist_hertz = audio_sample_rate / 2. + if lower_edge_hertz >= upper_edge_hertz: + raise ValueError("lower_edge_hertz %.1f >= upper_edge_hertz %.1f" % + (lower_edge_hertz, upper_edge_hertz)) + spectrogram_bins_hertz = np.linspace(0.0, nyquist_hertz, + num_spectrogram_bins) + spectrogram_bins_mel = hertz_to_mel(spectrogram_bins_hertz) + band_edges_mel = np.linspace(hertz_to_mel(lower_edge_hertz), + hertz_to_mel(upper_edge_hertz), + num_mel_bins + 2) + mel_weights_matrix = np.empty((num_spectrogram_bins, num_mel_bins)) + for i in range(num_mel_bins): + lower_edge_mel, center_mel, upper_edge_mel = band_edges_mel[i:i + 3] + lower_slope = ((spectrogram_bins_mel - lower_edge_mel) / + (center_mel - lower_edge_mel)) + upper_slope = ((upper_edge_mel - spectrogram_bins_mel) / + (upper_edge_mel - center_mel)) + mel_weights_matrix[:, + i] = np.maximum(0.0, + np.minimum(lower_slope, upper_slope)) + mel_weights_matrix[0, :] = 0.0 + return mel_weights_matrix + + +def log_mel_spectrogram(data, + audio_sample_rate=8000, + log_offset=0.0, + window_length_secs=0.025, + hop_length_secs=0.010, + **kwargs): + """ + log_mel_spectrogram + """ + window_length_samples = int(round(audio_sample_rate * window_length_secs)) + #print("audio_sample_rate = ", audio_sample_rate) + #print("window_length_secs = ", window_length_secs) + #print("window_length_sample ", window_length_samples) + hop_length_samples = int(round(audio_sample_rate * hop_length_secs)) + #print("hop_length_samples ", hop_length_samples) + fft_length = 2**int(np.ceil(np.log(window_length_samples) / np.log(2.0))) + #print(" fft_lengt = ", fft_length) + spectrogram = stft_magnitude(data, + fft_length=fft_length, + hop_length=hop_length_samples, + window_length=window_length_samples) + #print(" spectrogram.shape = ", spectrogram.shape) + mel_spectrogram = np.dot( + spectrogram, + spectrogram_to_mel_matrix(num_spectrogram_bins=spectrogram.shape[1], + audio_sample_rate=audio_sample_rate, + **kwargs)) + + return np.log(mel_spectrogram + log_offset) + + +def wav_to_example(wav_data, sample_rate): + """ + wav_to_example + """ + #sample_rate, wav_data = wavfile.read(wav_file) + assert wav_data.dtype == np.int16, 'Bad sample type: %r' % wav_data.dtype + #wav_data = wav_data[:16000*30] + #print(" wav_data ", wav_data.shape) + #print(" wav_data ", wav_data.shape) + pad_zero_num = int(sample_rate * (vgg_params.STFT_WINDOW_LENGTH_SECONDS - + vgg_params.STFT_HOP_LENGTH_SECONDS)) + wav_data_extend = np.hstack((wav_data, np.zeros(pad_zero_num))) + wav_data = wav_data_extend + #print(" wav_data ", wav_data.shape) + wav_data = wav_data / 32768.0 # Convert to [-1.0, +1.0] + #print(" wav_data after convert to -1 1", wav_data) + #if wav_data.shape[0] > max_second * sample_rate: + # wav_data = wav_data[:max_second * sample_rate, :] + if len(wav_data.shape) > 1: + wav_data = np.mean(wav_data, axis=1) + #print(" wav_data after mean", wav_data.shape, len(wav_data.shape), wav_data) + # Resample to the rate assumed by vgg. + #if sample_rate != vgg_params.SAMPLE_RATE: + # wav_data = resampy.resample(wav_data, sample_rate, vgg_params.SAMPLE_RATE) + log_mel = log_mel_spectrogram( + wav_data, + audio_sample_rate=vgg_params.SAMPLE_RATE, + log_offset=vgg_params.LOG_OFFSET, + window_length_secs=vgg_params.STFT_WINDOW_LENGTH_SECONDS, + hop_length_secs=vgg_params.STFT_HOP_LENGTH_SECONDS, + num_mel_bins=vgg_params.NUM_MEL_BINS, + lower_edge_hertz=vgg_params.MEL_MIN_HZ, + upper_edge_hertz=vgg_params.MEL_MAX_HZ) + # Frame features into examples. + features_sample_rate = 1.0 / vgg_params.STFT_HOP_LENGTH_SECONDS + example_window_length = int( + round(vgg_params.EXAMPLE_WINDOW_SECONDS * features_sample_rate)) + + example_hop_length = int( + round(vgg_params.EXAMPLE_HOP_SECONDS * features_sample_rate)) + log_mel_examples = frame(log_mel, + window_length=example_window_length, + hop_length=example_hop_length) + return log_mel_examples + + +def extract_pcm(pcm_file, sample_rate): + with open(pcm_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + examples = wav_to_example(audio_data, sample_rate) + return examples + + +if __name__ == "__main__": + wav_file = sys.argv[1] + print("wav_file = ", wav_file) + with open(wav_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + examples_batch = wav_to_example(audio_data, 16000) + print("examples_batch.shape", examples_batch.shape) diff --git a/applications/TableTennis/predict/action_detect/mfcc/model_config.py b/applications/TableTennis/predict/action_detect/mfcc/model_config.py new file mode 100644 index 0000000000000000000000000000000000000000..194365ece7545bd469c8ad23a46e7e461ac29edf --- /dev/null +++ b/applications/TableTennis/predict/action_detect/mfcc/model_config.py @@ -0,0 +1,51 @@ +""" +audio model config +""" +import numpy as np + +import mfcc.feature_extractor as feature_extractor + + +class ModelAudio(object): + """ + modelAudio + """ + def __init__(self, configs, use_gpu=1): + self.use_gpu = use_gpu + + self.audio_fps = configs.COMMON.fps + self.audio_feat_scale = configs.TSN.audio_scale + self.sample_rate = 16000 + + def predict_slice(self, wav_data, sample_rate): + """ + audio predict + """ + examples_batch = feature_extractor.wav_to_example( + wav_data, sample_rate)[0] + return examples_batch + + def predict_audio(self, audio_file): + """ + predict_audio + """ + audio_feature_list = [] + # read pcm + sample_rate = self.sample_rate + try: + with open(audio_file, "rb") as f: + pcm_data = f.read() + audio_data = np.fromstring(pcm_data, dtype=np.int16) + audio_status = "audio load success" + except Exception as e: + audio_data = [] + audio_status = "audio load failed" + step = 1 + len_video = int(len(audio_data) / sample_rate) + print(len_video) + for i in range(0, len_video, step): + audio_data_part = audio_data[i * sample_rate:(i + step) * + sample_rate] + feature_audio = self.predict_slice(audio_data_part, sample_rate) + audio_feature_list.append(feature_audio) + return audio_feature_list diff --git a/applications/TableTennis/predict/action_detect/mfcc/vgg_params.py b/applications/TableTennis/predict/action_detect/mfcc/vgg_params.py new file mode 100755 index 0000000000000000000000000000000000000000..0a995196103f12e1d975d4d7ee7eaa122a19fb5f --- /dev/null +++ b/applications/TableTennis/predict/action_detect/mfcc/vgg_params.py @@ -0,0 +1,37 @@ +"""Global parameters for the VGGish model. +See vggish_slim.py for more information. +""" + +# Architectural constants. +NUM_FRAMES = 50 # Frames in input mel-spectrogram patch. +NUM_BANDS = 64 # Frequency bands in input mel-spectrogram patch. +EMBEDDING_SIZE = 128 # Size of embedding layer. + +# Hyperparameters used in feature and example generation. +SAMPLE_RATE = 16000 +STFT_WINDOW_LENGTH_SECONDS = 0.040 +STFT_HOP_LENGTH_SECONDS = 0.020 +NUM_MEL_BINS = NUM_BANDS +MEL_MIN_HZ = 125 +MEL_MAX_HZ = 7500 +LOG_OFFSET = 0.01 # Offset used for stabilized log of input mel-spectrogram. +EXAMPLE_WINDOW_SECONDS = 1.00 # Each example contains 96 10ms frames +EXAMPLE_HOP_SECONDS = 1.00 # with zero overlap. + +# Parameters used for embedding postprocessing. +PCA_EIGEN_VECTORS_NAME = 'pca_eigen_vectors' +PCA_MEANS_NAME = 'pca_means' +QUANTIZE_MIN_VAL = -2.0 +QUANTIZE_MAX_VAL = +2.0 + +# Hyperparameters used in training. +INIT_STDDEV = 0.01 # Standard deviation used to initialize weights. +LEARNING_RATE = 1e-4 # Learning rate for the Adam optimizer. +ADAM_EPSILON = 1e-8 # Epsilon for the Adam optimizer. + +# Names of ops, tensors, and features. +INPUT_OP_NAME = 'vggish/input_features' +INPUT_TENSOR_NAME = INPUT_OP_NAME + ':0' +OUTPUT_OP_NAME = 'vggish/embedding' +OUTPUT_TENSOR_NAME = OUTPUT_OP_NAME + ':0' +AUDIO_EMBEDDING_FEATURE_NAME = 'audio_embedding' diff --git a/applications/TableTennis/predict/action_detect/models/__init__.py b/applications/TableTennis/predict/action_detect/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/TableTennis/predict/action_detect/models/audio_infer.py b/applications/TableTennis/predict/action_detect/models/audio_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..f50b7efa50448c9e0d632dced88f95288511809e --- /dev/null +++ b/applications/TableTennis/predict/action_detect/models/audio_infer.py @@ -0,0 +1,78 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """audio infer""" + def __init__(self, cfg, name='AUDIO'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + self.output_tensor = self.predictor.get_output_handle(output_names[0]) + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output = self.output_tensor.copy_to_cpu() + return output + + def predict(self, infer_config): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config) + feature_list = [] + pcm_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = np.array(data, dtype='float32') + output = self.infer(inputs) + feature_list.append(np.squeeze(output)) + pcm_list.append(inputs) + feature_values = np.vstack(feature_list) + pcm_values = np.vstack(pcm_list) + return feature_values, pcm_values + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + pcm_path = '/home/work/datasets/WorldCup2018/pcm/6e577252c4004961ac7caa738a52c238.pcm' + t0 = time.time() + cfg['AUDIO']['pcm_file'] = pcm_path + outputs = model.predict(cfg) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + t1 = time.time() + + print(outputs.shape) + print(outputs[0]) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/TableTennis/predict/action_detect/models/bmn_infer.py b/applications/TableTennis/predict/action_detect/models/bmn_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..dce2fcdd6612c3be2fda3b2d3201557c629aec28 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/models/bmn_infer.py @@ -0,0 +1,164 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import json +import pickle +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config +from utils.process_result import process_proposal + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """bmn infer""" + def __init__(self, cfg, name='BMN'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + self.nms_thread = cfg[name]['nms_thread'] + self.min_pred_score = cfg[name]['score_thread'] + self.min_frame_thread = cfg['COMMON']['fps'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + self.output1_tensor = self.predictor.get_output_handle(output_names[0]) + self.output2_tensor = self.predictor.get_output_handle(output_names[1]) + self.output3_tensor = self.predictor.get_output_handle(output_names[2]) + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output1 = self.output1_tensor.copy_to_cpu() + output2 = self.output2_tensor.copy_to_cpu() + output3 = self.output3_tensor.copy_to_cpu() + return output1, output2, output3 + + def generate_props(self, + pred_bmn, + pred_start, + pred_end, + max_window=200, + min_window=5): + """generate_props""" + video_len = min(pred_bmn.shape[-1], + min(pred_start.shape[-1], pred_end.shape[-1])) + pred_bmn = pred_bmn[0, :, :] * pred_bmn[1, :, :] + start_mask = self.boundary_choose(pred_start) + start_mask[0] = 1. + end_mask = self.boundary_choose(pred_end) + end_mask[-1] = 1. + score_results = [] + for idx in range(min_window, max_window): + for jdx in range(video_len): + start_index = jdx + end_index = start_index + idx + if end_index < video_len and start_mask[ + start_index] == 1 and end_mask[end_index] == 1: + xmin = start_index + xmax = end_index + xmin_score = pred_start[start_index] + xmax_score = pred_end[end_index] + bmn_score = pred_bmn[idx, jdx] + conf_score = xmin_score * xmax_score * bmn_score + score_results.append([xmin, xmax, conf_score]) + return score_results + + def boundary_choose(self, score_list): + """boundary_choose""" + max_score = max(score_list) + mask_high = (score_list > max_score * 0.5) + score_list = list(score_list) + score_middle = np.array([0.0] + score_list + [0.0]) + score_front = np.array([0.0, 0.0] + score_list) + score_back = np.array(score_list + [0.0, 0.0]) + mask_peak = ((score_middle > score_front) & (score_middle > score_back)) + mask_peak = mask_peak[1:-1] + mask = (mask_high | mask_peak).astype('float32') + return mask + + def predict(self, infer_config, material): + """predict""" + infer_reader = reader.get_reader(self.name, + 'infer', + infer_config, + material=material) + feature_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = [items[0] for items in data] + winds = [items[1] for items in data] + feat_info = [items[2] for items in data] + feature_T = feat_info[0][0] + feature_N = feat_info[0][1] + + inputs = np.array(inputs) + pred_bmn, pred_sta, pred_end = self.infer(inputs) + + if infer_iter == 0: + sum_pred_bmn = np.zeros((2, feature_N, feature_T)) + sum_pred_sta = np.zeros((feature_T, )) + sum_pred_end = np.zeros((feature_T, )) + sum_pred_cnt = np.zeros((feature_T, )) + + for idx, sub_wind in enumerate(winds): + sum_pred_bmn[:, :, sub_wind[0]:sub_wind[1]] += pred_bmn[idx] + sum_pred_sta[sub_wind[0]:sub_wind[1]] += pred_sta[idx] + sum_pred_end[sub_wind[0]:sub_wind[1]] += pred_end[idx] + sum_pred_cnt[sub_wind[0]:sub_wind[1]] += np.ones( + (sub_wind[1] - sub_wind[0], )) + + pred_bmn = sum_pred_bmn / sum_pred_cnt + pred_sta = sum_pred_sta / sum_pred_cnt + pred_end = sum_pred_end / sum_pred_cnt + + score_result = self.generate_props(pred_bmn, pred_sta, pred_end) + results = process_proposal(score_result, self.min_frame_thread, + self.nms_thread, self.min_pred_score) + + return results + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238' + + # feature + feature_path = imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + t0 = time.time() + outputs = model.predict(cfg, video_features) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + t1 = time.time() + + results = {'proposal': outputs} + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/TableTennis/predict/action_detect/models/lstm_infer.py b/applications/TableTennis/predict/action_detect/models/lstm_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..90685fefff29f6c203831b31a82aadb849d30501 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/models/lstm_infer.py @@ -0,0 +1,158 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import json +import pickle +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config +from utils.process_result import get_action_result + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """lstm infer""" + def __init__(self, cfg, name='ACTION'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + self.topk = cfg[name]['topk'] + self.frame_offset = cfg[name]['nms_offset'] + self.nms_thread = cfg[name]['nms_thread'] + self.cls_thread = cfg[name]['classify_score_thread'] + self.iou_thread = cfg[name]['iou_score_thread'] + + self.label_map_file = cfg['COMMON']['label_dic'] + self.fps = cfg['COMMON']['fps'] + self.nms_id = 5 + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input1_tensor = self.predictor.get_input_handle(input_names[0]) + self.input2_tensor = self.predictor.get_input_handle(input_names[1]) + + output_names = self.predictor.get_output_names() + self.output1_tensor = self.predictor.get_output_handle(output_names[0]) + self.output2_tensor = self.predictor.get_output_handle(output_names[1]) + + def infer(self, input1_arr, input1_lod, input2_arr=None, input2_lod=None): + """infer""" + self.input1_tensor.copy_from_cpu(input1_arr) + self.input1_tensor.set_lod(input1_lod) + if not input2_arr is None: + self.input2_tensor.copy_from_cpu(input2_arr) + self.input2_tensor.set_lod(input2_lod) + self.predictor.run() + output1 = self.output1_tensor.copy_to_cpu() + output2 = self.output2_tensor.copy_to_cpu() + # print(output.shape) + return output1, output2 + + def pre_process(self, input): + """pre process""" + input_arr = [] + input_lod = [0] + start_lod = 0 + end_lod = 0 + for sub_item in input: + end_lod = start_lod + len(sub_item) + input_lod.append(end_lod) + input_arr.extend(sub_item) + start_lod = end_lod + input_arr = np.array(input_arr) + # print(input_arr.shape) + # print([input_lod]) + return input_arr, [input_lod] + + def predict(self, infer_config, material): + """predict""" + infer_reader = reader.get_reader(self.name, + 'infer', + infer_config, + material=material) + results = [] + for infer_iter, data in enumerate(infer_reader()): + video_id = [[items[-2], items[-1]] for items in data] + input1 = [items[0] for items in data] + input2 = [items[1] for items in data] + input1_arr, input1_lod = self.pre_process(input1) + input2_arr, input2_lod = self.pre_process(input2) + output1, output2 = self.infer(input1_arr, input1_lod, input2_arr, + input2_lod) + # output1, output2 = self.infer(input1_arr, input1_lod) + + predictions_id = output1 + predictions_iou = output2 + for i in range(len(predictions_id)): + topk_inds = predictions_id[i].argsort()[0 - self.topk:] + topk_inds = topk_inds[::-1] + preds_id = predictions_id[i][topk_inds] + preds_iou = predictions_iou[i][0] + results.append((video_id[i], preds_id.tolist(), + topk_inds.tolist(), preds_iou.tolist())) + + predict_result = get_action_result(results, self.label_map_file, + self.fps, self.cls_thread, + self.iou_thread, self.nms_id, + self.nms_thread, self.frame_offset) + return predict_result + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + # proposal total + prop_dict = {} + for dataset in ['EuroCup2016', 'WorldCup2018']: + prop_json = '/home/work/datasets/{}/feature_bmn/prop.json'.format( + dataset) + json_data = json.load(open(prop_json, 'r')) + for item in json_data: + basename = prop_json.replace('feature_bmn/prop.json', 'mp4') + basename = basename + '/' + item['video_name'] + '.mp4' + prop_dict[basename] = item['bmn_results'] + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238' + + # feature + feature_path = imgs_path.replace("frames", "features") + '.pkl' + video_features = pickle.load(open(feature_path, 'rb')) + + # proposal + basename = imgs_path.replace('frames', 'mp4') + '.mp4' + bmn_results = prop_dict[basename] + + material = {'feature': video_features, 'proposal': bmn_results} + + t0 = time.time() + outputs = model.predict(cfg, material) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + # print(outputs.shape) + t1 = time.time() + results = {'actions': outputs} + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) + + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/TableTennis/predict/action_detect/models/pptsm_infer.py b/applications/TableTennis/predict/action_detect/models/pptsm_infer.py new file mode 100644 index 0000000000000000000000000000000000000000..58cf95707db1f422f0aee428ae2d324b4de571f2 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/models/pptsm_infer.py @@ -0,0 +1,77 @@ +""" +ppTSM InferModel +""" +import sys +import numpy as np +import time + +sys.path.append('../') +from utils.preprocess import get_images +from utils.config_utils import parse_config + +import reader +from paddle.inference import Config +from paddle.inference import create_predictor + + +class InferModel(object): + """pptsm infer""" + def __init__(self, cfg, name='PPTSM'): + name = name.upper() + self.name = name + model_file = cfg[name]['model_file'] + params_file = cfg[name]['params_file'] + gpu_mem = cfg[name]['gpu_mem'] + device_id = cfg[name]['device_id'] + + # model init + config = Config(model_file, params_file) + config.enable_use_gpu(gpu_mem, device_id) + config.switch_ir_optim(True) # default true + config.enable_memory_optim() + + # use zero copy + config.switch_use_feed_fetch_ops(False) + self.predictor = create_predictor(config) + + input_names = self.predictor.get_input_names() + self.input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + self.output_tensor = self.predictor.get_output_handle(output_names[1]) + + def infer(self, input): + """infer""" + self.input_tensor.copy_from_cpu(input) + self.predictor.run() + output = self.output_tensor.copy_to_cpu() + return output + + def predict(self, infer_config): + """predict""" + infer_reader = reader.get_reader(self.name, 'infer', infer_config) + feature_list = [] + for infer_iter, data in enumerate(infer_reader()): + inputs = [items[:-1] for items in data] + inputs = np.array(inputs) + output = self.infer(inputs) + feature_list.append(np.squeeze(output)) + feature_list = np.vstack(feature_list) + return feature_list + + +if __name__ == "__main__": + cfg_file = '/home/work/inference/configs/configs.yaml' + cfg = parse_config(cfg_file) + model = InferModel(cfg) + + imgs_path = '/home/work/datasets/WorldCup2018/frames/6e577252c4004961ac7caa738a52c238/' + imgs_list = get_images(imgs_path) + t0 = time.time() + cfg['PPTSM']['frame_list'] = imgs_list + outputs = model.predict(cfg) + # outputs = model.infer(np.random.rand(32, 8, 3, 224, 224).astype(np.float32)) + t1 = time.time() + + print(outputs.shape) + print('cost time = {} min'.format((t1 - t0) / 60.0)) diff --git a/applications/TableTennis/predict/action_detect/reader/__init__.py b/applications/TableTennis/predict/action_detect/reader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c4cc42a9e26ae56d5d0f9ff5faff6fd4828d08da --- /dev/null +++ b/applications/TableTennis/predict/action_detect/reader/__init__.py @@ -0,0 +1,15 @@ +""" +read map for model +""" +from reader.reader_utils import regist_reader, get_reader +# import reader.tsminf_reader as tsminf_reader +# import reader.audio_reader as audio_reader +import reader.bmninf_reader as bmninf_reader +import reader.feature_reader as feature_reader + +# regist reader, sort by alphabet +# regist_reader("TSM", tsminf_reader.TSMINFReader) +# regist_reader("PPTSM", tsminf_reader.TSMINFReader) +# regist_reader("AUDIO", audio_reader.AudioReader) +regist_reader("BMN", bmninf_reader.BMNINFReader) +regist_reader("ACTION", feature_reader.FeatureReader) diff --git a/applications/TableTennis/predict/action_detect/reader/bmninf_reader.py b/applications/TableTennis/predict/action_detect/reader/bmninf_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..112c9cb3fa35caaf6f1c90f7686ab9b58caddb88 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/reader/bmninf_reader.py @@ -0,0 +1,154 @@ +""" +# @File : bmninf_reader.py +# @Author: macaihong +# @Date : 2019/12/15 +# @Desc : +""" + +import os +import random +import pickle +import json +import numpy as np +import multiprocessing + +import numpy as np + +from .reader_utils import DataReader + + +def get_sw_prop(duration, window=200, step=10): + """ + get_sw_prop + """ + pr = [] + local_boxes = [] + for k in np.arange(0, duration - window + step, step): + start_id = k + end_id = min(duration, k + window) + if end_id - start_id < window: + start_id = end_id - window + local_boxes = (start_id, end_id) + pr.append(local_boxes) + + def valid_proposal(duration, span): + """ + valid_proposal + """ + # fileter proposals + # a valid proposal should have at least one second in the video + real_span = min(duration, span[1]) - span[0] + return real_span >= 1 + + pr = list(filter(lambda x: valid_proposal(duration, x), pr)) + return pr + + +class BMNINFReader(DataReader): + """ + Data reader for BMN model, which was stored as features extracted by prior networks + dataset cfg: feat_path, feature path, + tscale, temporal length of BM map, + dscale, duration scale of BM map, + anchor_xmin, anchor_xmax, the range of each point in the feature sequence, + batch_size, batch size of input data, + num_threads, number of threads of data processing + """ + def __init__(self, name, mode, cfg, material=None): + self.name = name + self.mode = mode + self.tscale = cfg[self.name.upper()]['tscale'] # 200 + self.dscale = cfg[self.name.upper()]['dscale'] # 200 + # self.subset = cfg[self.name.upper()]['subset'] + self.tgap = 1. / self.tscale + self.step = cfg[self.name.upper()]['window_step'] + + self.material = material + src_feature = self.material + + image_feature = src_feature['image_feature'] + # pcm_feature = src_feature['pcm_feature'] + # pcm_feature = pcm_feature.reshape((pcm_feature.shape[0] * 5, 640)) + # print(rgb_feature.shape, audio_feature.shape, pcm_feature.shape) + # min_length = min(image_feature.shape[0], pcm_feature.shape[0]) + #if min_length == 0: + # continue + # image_feature = image_feature[:min_length, :] + # pcm_feature = pcm_feature[:min_length, :] + # self.features = np.concatenate((image_feature, pcm_feature), axis=1) + self.features = image_feature + self.duration = len(self.features) + self.window = self.tscale + + self.get_dataset_dict() + self.get_match_map() + + self.batch_size = cfg[self.name.upper()]['batch_size'] + if (mode == 'test') or (mode == 'infer'): + self.num_threads = 1 # set num_threads as 1 for test and infer + + def get_dataset_dict(self): + """ + get_dataset_dict + """ + self.video_list = get_sw_prop(self.duration, self.window, self.step) + + def get_match_map(self): + """ + get_match_map + """ + match_map = [] + for idx in range(self.tscale): + tmp_match_window = [] + xmin = self.tgap * idx + for jdx in range(1, self.tscale + 1): + xmax = xmin + self.tgap * jdx + tmp_match_window.append([xmin, xmax]) + match_map.append(tmp_match_window) + match_map = np.array(match_map) + match_map = np.transpose(match_map, [1, 0, 2]) + match_map = np.reshape(match_map, [-1, 2]) + self.match_map = match_map + self.anchor_xmin = [self.tgap * i for i in range(self.tscale)] + self.anchor_xmax = [self.tgap * i for i in range(1, self.tscale + 1)] + + def load_file(self, video_wind): + """ + load_file + """ + start_feat_id = video_wind[0] + end_feat_id = video_wind[1] + video_feat = self.features[video_wind[0]:video_wind[1]] + video_feat = video_feat.T + video_feat = video_feat.astype("float32") + return video_feat + + def create_reader(self): + """ + reader creator for ctcn model + """ + return self.make_infer_reader() + + def make_infer_reader(self): + """ + reader for inference + """ + def reader(): + """ + reader + """ + batch_out = [] + # for video_name in self.video_list: + for video_wind in self.video_list: + video_idx = self.video_list.index(video_wind) + video_feat = self.load_file(video_wind) + batch_out.append( + (video_feat, video_wind, [self.duration, self.dscale])) + + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + if len(batch_out) > 0: + yield batch_out + + return reader diff --git a/applications/TableTennis/predict/action_detect/reader/feature_reader.py b/applications/TableTennis/predict/action_detect/reader/feature_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..f46dd3f41f471929a7a5d41ed8b197f79521e566 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/reader/feature_reader.py @@ -0,0 +1,91 @@ +""" +attention-lstm feature reader +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle +import numpy as np +import random +import code + +from .reader_utils import DataReader + + +class FeatureReader(DataReader): + """ + Data reader for youtube-8M dataset, which was stored as features extracted by prior networks + This is for the three models: lstm, attention cluster, nextvlad + + dataset cfg: num_classes + batch_size + list + NextVlad only: eigen_file + """ + def __init__(self, name, mode, cfg, material=None): + self.name = name + self.mode = mode + self.batch_size = cfg[self.name.upper()]['batch_size'] + + self.feature = material['feature'] + self.proposal = material['proposal'] + self.fps = 5 + + def create_reader(self): + """ + create_reader + """ + image_feature_list = self.feature['image_feature'] + audio_feature_list = self.feature['audio_feature'] + pcm_feature_list = self.feature['pcm_feature'] + pcm_feature_list = pcm_feature_list.reshape( + (pcm_feature_list.shape[0] * 5, 640)) + + fl = self.proposal + + if self.mode == 'train': + random.shuffle(fl) + + def reader(): + """ + reader + """ + batch_out = [] + for prop_info in fl: + start_id = int(prop_info['start']) + end_id = int(prop_info['end']) + bmn_score = float(prop_info['score']) + try: + image_feature = image_feature_list[start_id:end_id] + audio_feature = audio_feature_list[int(start_id / self.fps + ):int(end_id / + self.fps)] + pcm_feature = pcm_feature_list[start_id:end_id] + + # image_feature = np.concatenate((image_feature, pcm_feature), axis=1) + + batch_out.append( + (image_feature, audio_feature, 0, prop_info)) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + except Exception as e: + continue + + return reader diff --git a/applications/TableTennis/predict/action_detect/reader/reader_utils.py b/applications/TableTennis/predict/action_detect/reader/reader_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..5acff17787af03552aa234259f452b87e73fc98a --- /dev/null +++ b/applications/TableTennis/predict/action_detect/reader/reader_utils.py @@ -0,0 +1,107 @@ +""" +reader_util +""" +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import numpy as np + + +class ReaderNotFoundError(Exception): + """ + "Error: reader not found" + """ + def __init__(self, reader_name, avail_readers): + super(ReaderNotFoundError, self).__init__() + self.reader_name = reader_name + self.avail_readers = avail_readers + + def __str__(self): + msg = "Reader {} Not Found.\nAvailiable readers:\n".format( + self.reader_name) + for reader in self.avail_readers: + msg += " {}\n".format(reader) + return msg + + +class DataReader(object): + """ + data reader for video input + """ + def __init__(self, model_name, mode, cfg): + self.name = model_name + self.mode = mode + self.cfg = cfg + + def create_reader(self): + """ + Not implemented + """ + pass + + def get_config_from_sec(self, sec, item, default=None): + """ + get_config_from_sec + """ + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + +class ReaderZoo(object): + """ + ReaderZoo + """ + def __init__(self): + """ + __init__ + """ + self.reader_zoo = {} + + def regist(self, name, reader): + """ + regist + """ + assert reader.__base__ == DataReader, "Unknow model type {}".format( + type(reader)) + self.reader_zoo[name] = reader + + def get(self, name, mode, cfg, material=None): + """ + get + """ + for k, v in self.reader_zoo.items(): + if k == name: + return v(name, mode, cfg, material) + raise ReaderNotFoundError(name, self.reader_zoo.keys()) + + +# singleton reader_zoo +reader_zoo = ReaderZoo() + + +def regist_reader(name, reader): + """ + regist_reader + """ + reader_zoo.regist(name, reader) + + +def get_reader(name, mode, cfg, material=None): + """ + get_reader + """ + reader_model = reader_zoo.get(name, mode, cfg, material) + return reader_model.create_reader() diff --git a/applications/TableTennis/predict/action_detect/utils/__init__.py b/applications/TableTennis/predict/action_detect/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/TableTennis/predict/action_detect/utils/config_utils.py b/applications/TableTennis/predict/action_detect/utils/config_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a5f3f5f3b87a4633f81f679b29933292f6036bc1 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/utils/config_utils.py @@ -0,0 +1,81 @@ +""" +config_utils +""" +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import yaml +import ast + +import logger + +logger = logger.Logger() + +CONFIG_SECS = [ + 'train', + 'valid', + 'test', + 'infer', +] + + +class AttrDict(dict): + """ + AttrDict + """ + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value + + +def parse_config(cfg_file): + """Load a config file into AttrDict""" + import yaml + with open(cfg_file, 'r') as fopen: + yaml_config = AttrDict(yaml.load(fopen, Loader=yaml.Loader)) + create_attr_dict(yaml_config) + return yaml_config + + +def create_attr_dict(yaml_config): + """create_attr_dict""" + for key, value in yaml_config.items(): + if isinstance(value, dict): + yaml_config[key] = value = AttrDict(value) + if isinstance(value, str): + try: + value = ast.literal_eval(value) + except BaseException: + pass + if isinstance(value, AttrDict): + create_attr_dict(yaml_config[key]) + else: + yaml_config[key] = value + return + + +def print_configs(cfg, mode): + """print_configs""" + logger.info( + "---------------- {:>5} Arguments ----------------".format(mode)) + for sec, sec_items in cfg.items(): + logger.info("{}:".format(sec)) + for k, v in sec_items.items(): + logger.info(" {}:{}".format(k, v)) + logger.info("-------------------------------------------------") diff --git a/applications/TableTennis/predict/action_detect/utils/preprocess.py b/applications/TableTennis/predict/action_detect/utils/preprocess.py new file mode 100644 index 0000000000000000000000000000000000000000..1451df1fee14251fe8e2907e6f54f1071ff107b1 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/utils/preprocess.py @@ -0,0 +1,36 @@ +""" extract frames and pcm""" +import os +import sys +import shutil + + +def ffmpeg_frames(mp4_addr, frame_out_folder, fps=5): + """ffmpeg_frames""" + if os.path.exists(frame_out_folder): + shutil.rmtree(frame_out_folder) + os.makedirs(frame_out_folder) + cmd = './src/utils/ffmpeg -v 0 -i %s -r %d -q 0 %s/%s.jpg' % ( + mp4_addr, fps, frame_out_folder, '%08d') + os.system(cmd) + + +def ffmpeg_pcm(mp4_addr, save_file_name): + """ffmpeg_pcm""" + cmd = './src/utils/ffmpeg -y -i %s -acodec pcm_s16le -f s16le -ac 1 -ar 16000 %s -v 0' \ + % (mp4_addr, save_file_name) + os.system(cmd) + + +def ffmpeg_mp4(mp4_url, mp4_addr): + """ffmpeg_mp4""" + cmd = "wget %s -O %s -q" % (mp4_url, mp4_addr) + print("cmd = ", cmd) + os.system(cmd) + + +def get_images(image_path): + """get_images""" + images = sorted(os.listdir(image_path)) + images = images + images_path_list = [image_path + '/' + im for im in images] + return images_path_list diff --git a/applications/TableTennis/predict/action_detect/utils/process_result.py b/applications/TableTennis/predict/action_detect/utils/process_result.py new file mode 100644 index 0000000000000000000000000000000000000000..b7e351264dd609167d04fb6262ef1a715422a174 --- /dev/null +++ b/applications/TableTennis/predict/action_detect/utils/process_result.py @@ -0,0 +1,155 @@ +""" +# @File : process_result.py +# @Author: macaihong +# @Date : 2019/12/15 +# @Desc : +""" + +import sys +import os +import re +import numpy as np +import pickle +import json +import logger + +logger = logger.Logger() + + +def get_data_res(label_map, data, topk): + """get_data_res""" + sum_vid = len(data) + video_result = [] + for i in range(sum_vid): + vid_name = data[i][0][0] + # true_label predict_start predict_end predict_score predict_len gt_iou gt_start gt_ioa + feature_start_id = float(data[i][0][1]['start']) + feature_end_id = float(data[i][0][1]['end']) + feature_stage1_score = data[i][0][1]['score'] + predict_res = [] + for k in range(topk): + score_top = data[i][1][k] + labelid_top = data[i][2][k] + label_iou = data[i][3] + labelname_top = label_map[str(labelid_top)] + video_result.append([ + feature_start_id, feature_end_id, labelid_top, labelname_top, + score_top, label_iou + ]) + return video_result + + +def base_nms(bboxes, thresh, delta=0, nms_id=2): + """ + One-dimensional non-maximal suppression + :param bboxes: [[vid, label, st, ed, score, ...], ...] + :param thresh: + :return: + """ + """ + t1 = bboxes[:, 0] + t2 = bboxes[:, 1] + scores = bboxes[:, nms_id] + """ + + t1 = np.array([max(0, x[0] - delta) for x in bboxes]) + t2 = np.array([x[1] + delta for x in bboxes]) + scores = np.array([x[nms_id] for x in bboxes]) + + durations = t2 - t1 + order = scores.argsort()[::-1] + + keep = [] + while order.size > 0: + i = order[0] + keep.append(i) + tt1 = np.maximum(t1[i], t1[order[1:]]) + tt2 = np.minimum(t2[i], t2[order[1:]]) + intersection = tt2 - tt1 + IoU = intersection / (durations[i] + durations[order[1:]] - + intersection).astype(float) + + inds = np.where(IoU <= thresh)[0] + order = order[inds + 1] + return [bboxes[i] for i in keep] + + +def process_proposal(source_prop_box, + min_frame_thread=5, + nms_thresh=0.7, + score_thresh=0.01): + """process_video_prop""" + prop_box = [] + for items in source_prop_box: + start_frame = float(items[0]) + end_frame = float(items[1]) + score = float(items[2]) + if end_frame - start_frame < min_frame_thread or score < score_thresh: + continue + prop_box.append([start_frame, end_frame, score]) + + prop_box_keep = base_nms(prop_box, nms_thresh) + + prop_res = [] + for res in prop_box_keep: + prop_res.append({'start': res[0], 'end': res[1], 'score': res[2]}) + + return prop_res + + +def process_video_classify(video_prop, fps, score_thread, iou_thread, \ + nms_id=5, nms_thread=0.01, nms_delta=10, backgroundid=0): + """process_video_classify""" + prop_filter = [] + for item in video_prop: + if item[2] == backgroundid: + continue + prop_filter.append(item) + + # prop_filter = sorted(prop_filter, key=lambda x: x[nms_id], reverse=True) + prop_filter = base_nms(prop_filter, nms_thread, nms_delta, nms_id) + prop_filter = sorted(prop_filter, key=lambda x: x[0]) + + video_results = [] + for item in prop_filter: + start_sec = item[0] / fps + end_sec = item[1] / fps + + start_id_frame = item[0] + end_id_frame = item[1] + # start_time = "%02d:%02d:%02d" % ((start_id_frame / fps) / 3600, \ + # ((start_id_frame / fps) % 3600) / 60, (start_id_frame / fps) % 60) + # end_time = "%02d:%02d:%02d" % ((end_id_frame / fps) / 3600, \ + # ((end_id_frame / fps) % 3600) / 60, (end_id_frame / fps) % 60) + start_time = int(start_id_frame / fps) + end_time = int(end_id_frame / fps) + + label_id = item[2] + label_name = item[3] + label_classify_score = item[4] + label_iou_score = item[5] + if label_classify_score > score_thread and label_iou_score > iou_thread: + video_results.append({ + "start_time": start_time, + "end_time": end_time, + "label_id": label_id, + "label_name": label_name, + "classify_score": label_classify_score, + "iou_score": label_iou_score + }) + + return video_results + + +def get_action_result(result_info, label_map_file, fps, score_thread=0, \ + iou_thread=0, nms_id=5, nms_thread=0.01, frame_offset=10, topk=1): + """get_action_result""" + + label_map = json.load(open(label_map_file, 'r', encoding='utf-8')) + + org_result = get_data_res(label_map, result_info, topk) + nms_result = process_video_classify(org_result, fps, score_thread, + iou_thread, nms_id, nms_thread, + frame_offset) + + return nms_result diff --git a/applications/TableTennis/predict/configs/configs.yaml b/applications/TableTennis/predict/configs/configs.yaml new file mode 100644 index 0000000000000000000000000000000000000000..78ddb29a317b30de48a655879cfa96c32d41ec9f --- /dev/null +++ b/applications/TableTennis/predict/configs/configs.yaml @@ -0,0 +1,61 @@ +COMMON: + fps: 5 + use_gpu: True + label_dic: '/home/work/inference/configs/index_label_football_8.json' + # debug + PCM_ONLY: False + DEBUG: False + BMN_ONLY: False + LSTM_ONLY: False + +PPTSM: + name: "PPTSM" + model_file: "/home/work/inference/checkpoints/ppTSM/ppTSM.pdmodel" + params_file: "/home/work/inference/checkpoints/ppTSM/ppTSM.pdiparams" + gpu_mem: 8000 + device_id: 0 + seg_num: 8 + seglen: 1 + short_size: 256 + target_size: 224 + batch_size: 32 + image_mean: [0.485, 0.456, 0.406] + image_std: [0.229, 0.224, 0.225] + reader_threads: 12 + buf_size: 1024 + +AUDIO: + name: "AUDIO" + model_file: "/home/work/inference/checkpoints/AUDIO/__model__" + params_file: "/home/work/inference/checkpoints/AUDIO/__param__" + gpu_mem: 8000 + device_id: 0 + sample_rate: 16000 + batch_size: 32 + +BMN: + name: "BMN" + model_file: "/home/work/inference/checkpoints/BMN/__model__" + params_file: "/home/work/inference/checkpoints/BMN/__param__" + gpu_mem: 8000 + device_id: 0 + window_step: 200 # 200 + tscale: 200 + dscale: 200 + batch_size: 8 # 8 + nms_thread: 0.7 + score_thread: 0.03 # 0.05 + +ACTION: + name: "ACTION" + model_file: "/home/work/inference/checkpoints/LSTM/__model__" + params_file: "/home/work/inference/checkpoints/LSTM/__param__" + gpu_mem: 8000 + device_id: 0 + batch_size: 32 + topk: 1 + nms_thread: 0.01 + nms_offset: 10 + + classify_score_thread: 0.05 # 0.15 + iou_score_thread: 0.1 # 0.4 diff --git a/applications/TableTennis/predict/configs/index_label_football_8.json b/applications/TableTennis/predict/configs/index_label_football_8.json new file mode 100644 index 0000000000000000000000000000000000000000..4984fbdc4a845bd7b1e925d08d2a4783adf5514e --- /dev/null +++ b/applications/TableTennis/predict/configs/index_label_football_8.json @@ -0,0 +1,10 @@ +{ + "0": "背景", + "1": "进球", + "2": "角球", + "3": "任意球", + "4": "黄牌", + "5": "红牌", + "6": "换人", + "7": "界外球" +} diff --git a/applications/TableTennis/predict/eval.py b/applications/TableTennis/predict/eval.py new file mode 100644 index 0000000000000000000000000000000000000000..5455f935c3b4f3e82f36290fe70a83abfe77462f --- /dev/null +++ b/applications/TableTennis/predict/eval.py @@ -0,0 +1,287 @@ +""" +get instance for lstm +根据gts计算每个proposal_bmn的iou、ioa、label等信息 +""" +import os +import sys +import json +import random +import pickle +import numpy as np + +import io + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + +dataset = "/home/work/datasets" + +label_index_file = './configs/index_label_football_7.json' +eval_datasets = ['EuroCup2016'] +label_files = { + 'train': 'label_cls8_train.json', + 'validation': 'label_cls8_val.json' +} + +global fps, mode +label_index = json.load(open(label_index_file, 'rb')) + + +def load_gts(): + global fps + gts_data = {'fps': 0, 'gts': {}} + for eval_data in eval_datasets: + for item, value in label_files.items(): + label_file = '{}/{}/{}'.format(dataset, eval_data, value) + gts = json.load(open(label_file, 'rb')) + gts_data['fps'] = gts['fps'] + fps = gts['fps'] + for gt in gts['gts']: + gt['mode'] = item + basename = '{}/{}/mp4/{}'.format(dataset, eval_data, + os.path.basename(gt['url'])) + gts_data['gts'][basename] = gt + return gts_data['gts'] + + +def computeIoU(e1, e2): + """ + clc iou and ioa + """ + if not (e1['label'] == e2['label'] and e1['basename'] == e2['basename']): + return 0. + area1 = e1["end"] - e1["start"] + area2 = e2["end"] - e2["start"] + x1 = np.maximum(e1["start"], e2["start"]) + x2 = np.minimum(e1["end"], e2["end"]) + inter = np.maximum(0.0, x2 - x1) + iou = 0.0 if (area1 + area2 - + inter) == 0 else inter * 1.0 / (area1 + area2 - inter) + if not mode == 'proposal': + iou = 0.0 if area2 == 0 else inter * 1.0 / area2 + return iou + + +def convert_proposal(boxes, basename, score_threshold=0.01): + boxes = sorted(boxes, key=lambda x: float(x['score']), reverse=True) + res = [] + for box in boxes: + if not float(box['score']) >= score_threshold: + continue + res.append({ + 'basename': basename, + 'start': int(float(box['start']) / fps), + 'end': int(float(box['end']) / fps), + 'label': 0 + }) + return res + + +def convert_classify(boxes, basename, iou_threshold, score_threshold): + boxes = sorted(boxes, + key=lambda x: + (float(x['classify_score']), float(x['iou_score'])), + reverse=True) + + def convert_time_to_frame(time_type): + return int(time_type) + h, m, s = time_type.split(':') + return int(h) * 3600 + int(m) * 60 + int(s) + + res = [] + for box in boxes: + if not (box['iou_score'] >= iou_threshold + and box['classify_score'] >= score_threshold): + continue + res.append({ + 'basename': basename, + 'start': convert_time_to_frame(box['start_time']), + 'end': convert_time_to_frame(box['end_time']), + 'label': box['label_id'] + }) + return res + + +def convert_groundtruth(boxes, basename, phase=None): + res = [] + for box in boxes: + for item in box['label_ids']: + label = 0 if phase == 'proposal' else item + res.append({ + 'basename': basename, + 'start': box['start_id'], + 'end': box['end_id'], + 'label': label + }) + return res + + +def print_head(iou): + print("\nioa = {:.1f}".format(iou)) + res_str = '' + for item in ['label_name']: + res_str += '{:<12s}'.format(item) + for item in [ + 'label_id', 'precision', 'recall', 'hit_prop', 'num_prop', + 'hit_gts', 'num_gts' + ]: + res_str += '{:<10s}'.format(item) + print(res_str) + + +def print_result(res_dict, label='avg'): + if label == 'avg': + res_str = '{:<22s}'.format(str(label)) + else: + res_str = '{0:{2}<6s}{1:<10s}'.format(label_index[str(label)], + str(label), chr(12288)) + + for item in ['prec', 'recall']: + res_str += '{:<10.4f}'.format(res_dict[item]) + for item in ['hit_prop', 'num_prop', 'hit_gts', 'num_gts']: + res_str += '{:<10d}'.format(res_dict[item]) + print(res_str) + + +def evaluation(res_boxes, gts_boxes, label_range, iou_range, show_sub=False): + iou_map = [computeIoU(resId, gtsId) for resId in res_boxes \ + for gtsId in gts_boxes] + iou_map = np.array(iou_map).reshape((len(res_boxes), len(gts_boxes))) + hit_map_prop_total = np.max(iou_map, axis=1) + hit_map_index_total = np.argmax(iou_map, axis=1) + + res_dict = ['hit_prop', 'num_prop', 'hit_gts', 'num_gts'] + + for iou_threshold in iou_range: + if show_sub: + print_head(iou_threshold) + + iou_prop = np.array([k >= iou_threshold for k in hit_map_prop_total]) + average_results = {} + for label_id in label_range: + sub_results = {} + label_prop = np.array([k['label'] == label_id for k in res_boxes]) + label_gts = np.array([k['label'] == label_id for k in gts_boxes]) + sub_results['num_prop'] = sum(label_prop) + sub_results['num_gts'] = sum(label_gts) + if sub_results['num_prop'] == 0: + hit_prop_index = [] + else: + hit_prop_index = label_prop & iou_prop + sub_results['hit_prop'] = sum(hit_prop_index) + sub_results['hit_gts'] = len( + set(hit_map_index_total[hit_prop_index])) + + sub_results['prec'] = 0.0 if sub_results['num_prop'] == 0 \ + else sub_results['hit_prop'] * 1.0 / sub_results['num_prop'] + sub_results['recall'] = 0.0 if sub_results['num_gts'] == 0 \ + else sub_results['hit_gts'] * 1.0 / sub_results['num_gts'] + if show_sub: + print_result(sub_results, label=label_id) + for item in res_dict: + if not item in average_results: + average_results[item] = 0 + average_results[item] += sub_results[item] + if len(label_range) == 1: # proposal 不需要输出average值 + continue + average_results['prec'] = 0.0 if average_results['num_prop'] == 0 \ + else average_results['hit_prop'] * 1.0 / average_results['num_prop'] + average_results['recall'] = 0.0 if average_results['num_gts'] == 0 \ + else average_results['hit_gts'] * 1.0 / average_results['num_gts'] + if show_sub: + print_result(average_results) + + average_results['F1'] = 0.0 if (average_results['prec'] + average_results['recall'] == 0) \ + else 2 * average_results['prec'] * average_results['recall'] / \ + (average_results['prec'] + average_results['recall']) + return average_results + + +def get_eval_results(predicts, + gts_data, + phase, + iou_threshold=0.3, + score_threshold=0.3, + show_sub=False): + global mode + mode = phase + res_boxes = [] + gts_boxes = [] + for ped_data in predicts: + basename = ped_data['video_name'] + + # eval sub data + such_eval = False + for eval_name in eval_datasets: + if eval_name in basename: + such_eval = True + break + if not such_eval: + continue + + gts = gts_data[basename]['actions'] + if phase == 'proposal': + res_boxes.extend( + convert_proposal(ped_data['bmn_results'], basename, + score_threshold)) + gts_boxes.extend( + convert_groundtruth(gts, basename, phase='proposal')) + label_range = [0] + iou_range = np.arange(0.1, 1, 0.1) + else: + res_boxes.extend( + convert_classify(ped_data['action_results'], basename, + iou_threshold, score_threshold)) + gts_boxes.extend(convert_groundtruth(gts, basename)) + label_range = range(1, len(label_index)) + iou_range = np.arange(0.5, 0.6, 0.1) + + eval_results = evaluation(res_boxes, + gts_boxes, + label_range, + iou_range, + show_sub=show_sub) + + return eval_results + + +if __name__ == "__main__": + result_file = sys.argv[1] + predicts = json.load(open(result_file, 'r', encoding='utf-8')) + gts_data = load_gts() + + get_eval_results(predicts, + gts_data, + 'proposal', + score_threshold=0.03, + show_sub=True) + #get_eval_results(predicts, gts_data, 'actions') + + best_F1 = -0.1 + best_res = {} + best_iou_threshold = 0. + best_score_threshold = 0. + for iou_threshold in np.arange(0.1, 0.9, 0.1): + for score_threshold in np.arange(0.1, 1, 0.1): + avg_res = get_eval_results(predicts, + gts_data, + 'actions', + iou_threshold=iou_threshold, + score_threshold=score_threshold, + show_sub=False) + if best_F1 < avg_res['F1']: + best_F1 = avg_res['F1'] + best_res = avg_res + best_iou_threshold = iou_threshold + best_score_threshold = score_threshold + print("best iou threshold = {:.1f}".format(best_iou_threshold)) + print("best score threshold = {:.1f}".format(best_score_threshold)) + print('best F1 score = {:.4f}'.format(best_F1)) + print_head(0.5) + print_result(best_res) + + get_eval_results(predicts, + gts_data, + 'actions', + iou_threshold=best_iou_threshold, + score_threshold=best_score_threshold, + show_sub=True) diff --git a/applications/TableTennis/predict/predict.py b/applications/TableTennis/predict/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..7407cad0b074f6c2eea6a17699228e38a83111b2 --- /dev/null +++ b/applications/TableTennis/predict/predict.py @@ -0,0 +1,36 @@ +import os +import sys +import json + +sys.path.append('action_detect') +from action import ActionDetection + +if __name__ == '__main__': + dataset_dir = "/home/work/datasets/EuroCup2016" + + model_predict = ActionDetection(cfg_file="./configs/configs.yaml") + model_predict.load_model() + + video_url = os.path.join(dataset_dir, 'url_val.list') + with open(video_url, 'r') as f: + lines = f.readlines() + lines = [os.path.join(dataset_dir, k.strip()) for k in lines] + + results = [] + for line in lines: + video_name = line + print(video_name) + + imgs_path = video_name.replace(".mp4", "").replace("mp4", "frames") + pcm_path = video_name.replace(".mp4", ".pcm").replace("mp4", "pcm") + + bmn_results, action_results = model_predict.infer(imgs_path, pcm_path) + results.append({ + 'video_name': line, + 'bmn_results': bmn_results, + 'action_results': action_results + }) + + with open('results.json', 'w', encoding='utf-8') as f: + data = json.dumps(results, indent=4, ensure_ascii=False) + f.write(data) diff --git a/applications/TableTennis/val_split.py b/applications/TableTennis/val_split.py new file mode 100644 index 0000000000000000000000000000000000000000..1a1e8e6ac0313228dd20678e622a38e5d9ee4449 --- /dev/null +++ b/applications/TableTennis/val_split.py @@ -0,0 +1,19 @@ +import json + +with open('/home/aistudio/data/label_cls14_train.json') as f: + data = json.load(f) +f.close() + +val = {'gts': data['gts'][0:5], 'fps': 25} + +jsonString = json.dumps(val, indent=4, ensure_ascii=False) +jsonFile = open('/home/aistudio/data/label_cls14_val.json', 'w') +jsonFile.write(jsonString) +jsonFile.close() + +train = {'gts': data['gts'][5:], 'fps': 25} + +jsonString = json.dumps(train, indent=4, ensure_ascii=False) +jsonFile = open('/home/aistudio/data/label_cls14_train.json', 'w') +jsonFile.write(jsonString) +jsonFile.close() diff --git a/applications/VideoQualityAssessment/README.md b/applications/VideoQualityAssessment/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4106e5e8d9ed3c3da754783d538605730ff5daab --- /dev/null +++ b/applications/VideoQualityAssessment/README.md @@ -0,0 +1,190 @@ +# 视频质量评价模型 +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型优化](#模型优化) +- [模型部署](#模型部署) +- [参考论文](#参考论文) + + +## 模型简介 + +该代码库主要基于paddle2.1版本开发,主要是在ppTSM网络模型的基础上修改的一种无参考视频质量评估方法,通过读入视频的视频帧来判断该视频的质量。 + +针对视频内容的理解,可以自动分析视频内容的质量,帮助选出最优的关键帧或关键片段作为视频封面,提升视频的点击转换和用户体验。 + +本项目目前支持Linux下的GPU单卡和多卡运行环境。 + +## 数据准备 + +``` +数据集来自公开数据集KonVid-150k,共153842个ugc视频,其中训练集(KonVid-150k-A)152265个,验证集(KonVid-150k-B)1577个 +示例数据集以及数据集官网地址: datasets/dataset_url.list +数据集标注文件为dataset中的train.txt和eval.txt +``` + +## 模型训练 + +环境安装: + +- PaddlePaddle >= 2.1.0 +- Python >= 3.7 +- PaddleX >= 2.0.0 + +- CUDA >= 10.1 +- cuDNN >= 7.6.4 +- nccl >= 2.1.2 + +安装Python依赖库: + +Python依赖库在[requirements.txt](https://github.com/PaddlePaddle/PaddleVideo/blob/master/requirements.txt)中给出,可通过如下命令安装: + +``` +python3.7 -m pip install --upgrade pip +pip3.7 install --upgrade -r requirements.txt +``` + +使用`paddle.distributed.launch`启动模型训练和测试脚本(`main.py`),可以更方便地启动多卡训练与测试,或直接运行(./run.sh) + +```shell +sh run.sh +``` +我们将所有标准的启动命令都放在了```run.sh```中,注意选择想要运行的脚本。 + +参考如下方式启动模型训练,`paddle.distributed.launch`通过设置`gpus`指定GPU运行卡号, +指定`--validate`来启动训练时评估。 + +```bash +# PaddleVideo通过launch方式启动多卡多进程训练 + +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + --log_dir=log_pptsm \ + main.py \ + --amp \ + --validate \ + -c ./configs/recognition/tsm/pptsm_regression.yaml +``` + +其中,`-c`用于指定配置文件的路径,可通过配置文件修改相关训练配置信息,也可以通过添加`-o`参数来更新配置: + +```bash +python -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/recognition/tsm/pptsm_regression.yaml \ + --validate \ + -o DATASET.batch_size=16 +``` +`-o`用于指定需要修改或者添加的参数,其中`-o DATASET.batch_size=16`表示更改batch_size大小为16。 + +运行上述命令,将会输出运行日志,并默认保存在./log目录下,如:`worker.0` , `worker.1` ... , worker日志文件对应每张卡上的输出 + +【train阶段】打印当前时间,当前epoch/epoch总数,当前batch id,评估指标,耗时,ips等信息: + + + [11/16 04:40:37] epoch:[ 1/1 ] train step:100 loss: 5.31382 lr: 0.000250 batch_cost: 0.73082 sec, reader_cost: 0.38075 sec, ips: 5.47330 instance/sec. + + +【eval阶段】打印当前时间,当前epoch/epoch总数,当前batch id,评估指标,耗时,ips等信息: + + + [11/16 04:40:37] epoch:[ 1/1 ] val step:0 loss: 4.42741 batch_cost: 1.37882 sec, reader_cost: 0.00000 sec, ips: 2.90104 instance/sec. + + +【epoch结束】打印当前时间,学习率,评估指标,耗时,ips等信息: + + + [11/16 04:40:37] lr=0.00012487 + [11/16 04:40:37] train_SROCC=0.4456697876616565 + [11/16 04:40:37] train_PLCC=0.48071880604403616 + [11/16 04:40:37] END epoch:1 val loss_avg: 5.21620 avg_batch_cost: 0.04321 sec, avg_reader_cost: 0.00000 sec, batch_cost_sum: 112.69575 sec, avg_ips: 8.41203 instance/sec. + + +当前为评估结果最好的epoch时,打印最优精度: + + [11/16 04:40:57] max_SROCC=0.7116468111328617 + [11/16 04:40:57] max_PLCC=0.733503995526737 + +### 模型恢复训练 + +如果训练任务终止,可以加载断点权重文件(优化器-学习率参数,断点文件)继续训练。 +需要指定`-o resume_epoch`参数,该参数表示从```resume_epoch```轮开始继续训练. + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + --amp \ + -c ./configs/recognition/tsm/pptsm_regression.yaml \ + --validate \ + -o resume_epoch=5 + +``` + +### 模型微调 + +进行模型微调(Finetune),对自定义数据集进行模型微调,需要指定 `--weights` 参数来加载预训练模型。 + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + --amp \ + -c ./configs/recognition/tsm/pptsm_regression.yaml \ + --validate \ + --weights=./output/model_name/ppTSM_best.pdparams +``` + +PaddleVideo会自动**不加载**shape不匹配的参数 + +## 模型测试 + +需要指定 `--test`来启动测试模式,并指定`--weights`来加载预训练模型。 + +```bash +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/recognition/tsm/pptsm_regression.yaml \ + --test \ + --weights=./output/model_name/ppTSM_best.pdparams +``` + +## 模型优化 + +在实际使用场景中可根据视频质量以及尺寸尝试优化策略 + +- 可通过原图输入来替换RandomCrop:224操作,准确率由SROCC=0.8176,PLCC=0.8361提升到SROCC=0.8617,PLCC=0.8910,不同模型以及特征增强操作的效果对比如下表所示 + + | 模型 | 特征增强 | val_SROCC | val_PLCC | + | :----: | :-----------------------------------------: | :-------: | :------: | + | GSTVQA | 原图输入 | 0.7932 | 0.8006 | + | ppTSM | train--RandomCrop=224 val--center_crop=224 | 0.8176 | 0.8361 | + | ppTSM | train--RandomCrop=512 val--center_crop=512 | 0.8603 | 0.8822 | + | ppTSM | 原图输入 | 0.8617 | 0.8910 | + + + +- 考虑应用场景视频的 aspect ratio 大都为 16:9 和 4:3 等,同时为了避免非均匀缩放拉伸带来的干扰 ,可以采用了(224x3)x(224x2)=672x448 的输入尺寸来更充分得利用有限的输入尺寸。 + +## 模型部署 + +本代码解决方案在官方验证集(KonVid-150k-B)上的指标效果为SROCC=0.8176,PLCC=0.8361。 + +## 参考论文 + +- [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf), Ji Lin, Chuang Gan, Song Han + +- [ [Quality Assessment of In-the-Wild Videos](https://dl.acm.org/citation.cfm?doid=3343031.3351028)](https://arxiv.org/pdf/1811.08383.pdf), Dingquan Li, Tingting Jiang, and Ming Jiang + diff --git a/applications/VideoQualityAssessment/configs/recognition/tsm/pptsm_regression.yaml b/applications/VideoQualityAssessment/configs/recognition/tsm/pptsm_regression.yaml new file mode 100644 index 0000000000000000000000000000000000000000..72bf34ab76c0dcd4ec87a155fb7bff242b00db2d --- /dev/null +++ b/applications/VideoQualityAssessment/configs/recognition/tsm/pptsm_regression.yaml @@ -0,0 +1,114 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTweaksTSM" #Mandatory, The name of backbone. + pretrained: "pretrained_dygraph/ppTSM.pdparams" #Optional, pretrained model path. + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSMRecHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 1 #101 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 4 #Mandatory, bacth size + num_workers: 8 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameRecDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, train data root path + file_path: "dataset/train.txt" + suffix: '{:08}.jpg' + valid: + format: "FrameRecDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "dataset/eval.txt" + suffix: '{:08}.jpg' + test: + format: "FrameRecDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "dataset/eval.txt" + suffix: '{:08}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + transform: #Mandotary, image transfrom operator + - RandomCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + valid_mode: True + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + valid_mode: True + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 50 + warmup_epochs: 10 + warmup_start_lr: 0 + cosine_base_lr: 0.0001 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + + +METRIC: + name: 'QuqlityMetric' + batch_size: 1 + data_size: 1 + +model_name: "ppTSM" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" +resume_from: "" #checkpoint path. diff --git a/applications/VideoQualityAssessment/dataset/dataset_url.txt b/applications/VideoQualityAssessment/dataset/dataset_url.txt new file mode 100644 index 0000000000000000000000000000000000000000..10a391a28c65cf6d0ef8f2547d95910ac019e5cb --- /dev/null +++ b/applications/VideoQualityAssessment/dataset/dataset_url.txt @@ -0,0 +1,2 @@ +示例数据集:https://bj.bcebos.com/acg-algo/PaddleVideo_application/VideoQualityAssessment/KonVid-150k.tar.gz +官网数据集下载地址:http://database.mmsp-kn.de/konvid-150k-vqa-database.html diff --git a/applications/VideoQualityAssessment/dataset/eval.txt b/applications/VideoQualityAssessment/dataset/eval.txt new file mode 100644 index 0000000000000000000000000000000000000000..ef39b09395fc92c9746df9e3ca0bdf1c3562540c --- /dev/null +++ b/applications/VideoQualityAssessment/dataset/eval.txt @@ -0,0 +1,50 @@ +orig_10007436115_540_5s.mp4 dataset/KonVid-150k/orig_10007436115_540_5s 26 0.722 +orig_10007901796_540_5s.mp4 dataset/KonVid-150k/orig_10007901796_540_5s 27 0.762 +orig_10041796874_540_5s.mp4 dataset/KonVid-150k/orig_10041796874_540_5s 26 0.65 +orig_10044664624_540_5s.mp4 dataset/KonVid-150k/orig_10044664624_540_5s 27 0.692 +orig_10045053595_540_5s.mp4 dataset/KonVid-150k/orig_10045053595_540_5s 27 0.732 +orig_10062498203_540_5s.mp4 dataset/KonVid-150k/orig_10062498203_540_5s 27 0.708 +orig_10084107653_540_5s.mp4 dataset/KonVid-150k/orig_10084107653_540_5s 26 0.618 +orig_10158770453_540_5s.mp4 dataset/KonVid-150k/orig_10158770453_540_5s 27 0.74 +orig_10181314856_540_5s.mp4 dataset/KonVid-150k/orig_10181314856_540_5s 27 0.682 +orig_10242523803_540_5s.mp4 dataset/KonVid-150k/orig_10242523803_540_5s 27 0.736 +orig_10320058616_540_5s.mp4 dataset/KonVid-150k/orig_10320058616_540_5s 27 0.658 +orig_10332507295_540_5s.mp4 dataset/KonVid-150k/orig_10332507295_540_5s 27 0.666 +orig_10349027905_540_5s.mp4 dataset/KonVid-150k/orig_10349027905_540_5s 27 0.658 +orig_10410965174_540_5s.mp4 dataset/KonVid-150k/orig_10410965174_540_5s 27 0.71 +orig_10414992923_540_5s.mp4 dataset/KonVid-150k/orig_10414992923_540_5s 27 0.718 +orig_10420562595_540_5s.mp4 dataset/KonVid-150k/orig_10420562595_540_5s 26 0.728 +orig_10564593194_540_5s.mp4 dataset/KonVid-150k/orig_10564593194_540_5s 27 0.72 +orig_10582782586_540_5s.mp4 dataset/KonVid-150k/orig_10582782586_540_5s 27 0.696 +orig_10583592443_540_5s.mp4 dataset/KonVid-150k/orig_10583592443_540_5s 27 0.68 +orig_10637184206_540_5s.mp4 dataset/KonVid-150k/orig_10637184206_540_5s 27 0.644 +orig_10674350206_540_5s.mp4 dataset/KonVid-150k/orig_10674350206_540_5s 26 0.676 +orig_10867101476_540_5s.mp4 dataset/KonVid-150k/orig_10867101476_540_5s 27 0.744 +orig_10910213084_540_5s.mp4 dataset/KonVid-150k/orig_10910213084_540_5s 27 0.718 +orig_10959956576_540_5s.mp4 dataset/KonVid-150k/orig_10959956576_540_5s 27 0.608 +orig_10979853044_540_5s.mp4 dataset/KonVid-150k/orig_10979853044_540_5s 27 0.692 +orig_11009028296_540_5s.mp4 dataset/KonVid-150k/orig_11009028296_540_5s 26 0.694 +orig_11020878843_540_5s.mp4 dataset/KonVid-150k/orig_11020878843_540_5s 27 0.602 +orig_11053897243_540_5s.mp4 dataset/KonVid-150k/orig_11053897243_540_5s 27 0.644 +orig_11070232293_540_5s.mp4 dataset/KonVid-150k/orig_11070232293_540_5s 27 0.684 +orig_11145664684_540_5s.mp4 dataset/KonVid-150k/orig_11145664684_540_5s 27 0.576 +orig_11163815184_540_5s.mp4 dataset/KonVid-150k/orig_11163815184_540_5s 27 0.76 +orig_11193003955_540_5s.mp4 dataset/KonVid-150k/orig_11193003955_540_5s 27 0.554 +orig_11241102906_540_5s.mp4 dataset/KonVid-150k/orig_11241102906_540_5s 27 0.79 +orig_11243731843_540_5s.mp4 dataset/KonVid-150k/orig_11243731843_540_5s 27 0.676 +orig_11257420374_540_5s.mp4 dataset/KonVid-150k/orig_11257420374_540_5s 27 0.702 +orig_11272718786_540_5s.mp4 dataset/KonVid-150k/orig_11272718786_540_5s 26 0.49 +orig_11277693604_540_5s.mp4 dataset/KonVid-150k/orig_11277693604_540_5s 27 0.724 +orig_11378375474_540_5s.mp4 dataset/KonVid-150k/orig_11378375474_540_5s 27 0.646 +orig_11404825244_540_5s.mp4 dataset/KonVid-150k/orig_11404825244_540_5s 27 0.79 +orig_11663413173_540_5s.mp4 dataset/KonVid-150k/orig_11663413173_540_5s 27 0.738 +orig_11670848953_540_5s.mp4 dataset/KonVid-150k/orig_11670848953_540_5s 27 0.664 +orig_11671137563_540_5s.mp4 dataset/KonVid-150k/orig_11671137563_540_5s 27 0.742 +orig_11671199403_540_5s.mp4 dataset/KonVid-150k/orig_11671199403_540_5s 27 0.648 +orig_11684333413_540_5s.mp4 dataset/KonVid-150k/orig_11684333413_540_5s 27 0.398 +orig_11705285984_540_5s.mp4 dataset/KonVid-150k/orig_11705285984_540_5s 27 0.648 +orig_11714293566_540_5s.mp4 dataset/KonVid-150k/orig_11714293566_540_5s 27 0.722 +orig_11780799405_540_5s.mp4 dataset/KonVid-150k/orig_11780799405_540_5s 27 0.69 +orig_11798466663_540_5s.mp4 dataset/KonVid-150k/orig_11798466663_540_5s 27 0.666 +orig_11848691003_540_5s.mp4 dataset/KonVid-150k/orig_11848691003_540_5s 27 0.696 +orig_11938552173_540_5s.mp4 dataset/KonVid-150k/orig_11938552173_540_5s 27 0.69 diff --git a/applications/VideoQualityAssessment/dataset/train.txt b/applications/VideoQualityAssessment/dataset/train.txt new file mode 100644 index 0000000000000000000000000000000000000000..5bdc1754c17fced8529cea359e1b852627dc620a --- /dev/null +++ b/applications/VideoQualityAssessment/dataset/train.txt @@ -0,0 +1,200 @@ +orig_10000251326_540_5s.mp4 dataset/KonVid-150k/orig_10000251326_540_5s 27 0.68 +orig_10000958013_540_5s.mp4 dataset/KonVid-150k/orig_10000958013_540_5s 27 0.56 +orig_10001646563_540_5s.mp4 dataset/KonVid-150k/orig_10001646563_540_5s 27 0.72 +orig_10001767205_540_5s.mp4 dataset/KonVid-150k/orig_10001767205_540_5s 27 0.56 +orig_10002025004_540_5s.mp4 dataset/KonVid-150k/orig_10002025004_540_5s 27 0.52 +orig_10002207236_540_5s.mp4 dataset/KonVid-150k/orig_10002207236_540_5s 27 0.8 +orig_10004838054_540_5s.mp4 dataset/KonVid-150k/orig_10004838054_540_5s 27 0.6 +orig_10004937375_540_5s.mp4 dataset/KonVid-150k/orig_10004937375_540_5s 27 0.76 +orig_10004945364_540_5s.mp4 dataset/KonVid-150k/orig_10004945364_540_5s 27 0.72 +orig_10005206083_540_5s.mp4 dataset/KonVid-150k/orig_10005206083_540_5s 27 0.52 +orig_10005430476_540_5s.mp4 dataset/KonVid-150k/orig_10005430476_540_5s 27 0.52 +orig_10005501126_540_5s.mp4 dataset/KonVid-150k/orig_10005501126_540_5s 27 0.64 +orig_10005592336_540_5s.mp4 dataset/KonVid-150k/orig_10005592336_540_5s 27 0.72 +orig_10005814894_540_5s.mp4 dataset/KonVid-150k/orig_10005814894_540_5s 26 0.6 +orig_10006095865_540_5s.mp4 dataset/KonVid-150k/orig_10006095865_540_5s 27 0.56 +orig_10006158815_540_5s.mp4 dataset/KonVid-150k/orig_10006158815_540_5s 26 0.72 +orig_10006225104_540_5s.mp4 dataset/KonVid-150k/orig_10006225104_540_5s 27 0.68 +orig_10006434483_540_5s.mp4 dataset/KonVid-150k/orig_10006434483_540_5s 27 0.8 +orig_10006824545_540_5s.mp4 dataset/KonVid-150k/orig_10006824545_540_5s 27 0.64 +orig_10006985874_540_5s.mp4 dataset/KonVid-150k/orig_10006985874_540_5s 27 0.64 +orig_10007125144_540_5s.mp4 dataset/KonVid-150k/orig_10007125144_540_5s 27 0.76 +orig_10007144274_540_5s.mp4 dataset/KonVid-150k/orig_10007144274_540_5s 27 0.6 +orig_10007156543_540_5s.mp4 dataset/KonVid-150k/orig_10007156543_540_5s 27 0.72 +orig_10007160286_540_5s.mp4 dataset/KonVid-150k/orig_10007160286_540_5s 27 0.64 +orig_10007202903_540_5s.mp4 dataset/KonVid-150k/orig_10007202903_540_5s 27 0.52 +orig_10007259736_540_5s.mp4 dataset/KonVid-150k/orig_10007259736_540_5s 27 0.48 +orig_10007266185_540_5s.mp4 dataset/KonVid-150k/orig_10007266185_540_5s 27 0.72 +orig_10007436806_540_5s.mp4 dataset/KonVid-150k/orig_10007436806_540_5s 27 0.72 +orig_10007472075_540_5s.mp4 dataset/KonVid-150k/orig_10007472075_540_5s 27 0.6 +orig_10007499104_540_5s.mp4 dataset/KonVid-150k/orig_10007499104_540_5s 27 0.72 +orig_10007508856_540_5s.mp4 dataset/KonVid-150k/orig_10007508856_540_5s 27 0.68 +orig_10007553766_540_5s.mp4 dataset/KonVid-150k/orig_10007553766_540_5s 26 0.56 +orig_10007562636_540_5s.mp4 dataset/KonVid-150k/orig_10007562636_540_5s 27 0.72 +orig_10007591225_540_5s.mp4 dataset/KonVid-150k/orig_10007591225_540_5s 27 0.56 +orig_10007676053_540_5s.mp4 dataset/KonVid-150k/orig_10007676053_540_5s 27 0.68 +orig_10007706256_540_5s.mp4 dataset/KonVid-150k/orig_10007706256_540_5s 27 0.6 +orig_10007765775_540_5s.mp4 dataset/KonVid-150k/orig_10007765775_540_5s 27 0.6 +orig_10007798396_540_5s.mp4 dataset/KonVid-150k/orig_10007798396_540_5s 27 0.68 +orig_10007809603_540_5s.mp4 dataset/KonVid-150k/orig_10007809603_540_5s 27 0.84 +orig_10007811994_540_5s.mp4 dataset/KonVid-150k/orig_10007811994_540_5s 27 0.76 +orig_10008004183_540_5s.mp4 dataset/KonVid-150k/orig_10008004183_540_5s 27 0.76 +orig_10008012003_540_5s.mp4 dataset/KonVid-150k/orig_10008012003_540_5s 27 0.64 +orig_10008199764_540_5s.mp4 dataset/KonVid-150k/orig_10008199764_540_5s 27 0.6 +orig_10008284774_540_5s.mp4 dataset/KonVid-150k/orig_10008284774_540_5s 26 0.48 +orig_10008467515_540_5s.mp4 dataset/KonVid-150k/orig_10008467515_540_5s 27 0.64 +orig_10008553263_540_5s.mp4 dataset/KonVid-150k/orig_10008553263_540_5s 27 0.72 +orig_10008962816_540_5s.mp4 dataset/KonVid-150k/orig_10008962816_540_5s 27 0.68 +orig_10009224333_540_5s.mp4 dataset/KonVid-150k/orig_10009224333_540_5s 27 0.72 +orig_10009608044_540_5s.mp4 dataset/KonVid-150k/orig_10009608044_540_5s 27 0.64 +orig_10009662916_540_5s.mp4 dataset/KonVid-150k/orig_10009662916_540_5s 27 0.68 +orig_10010456364_540_5s.mp4 dataset/KonVid-150k/orig_10010456364_540_5s 27 0.64 +orig_10010534764_540_5s.mp4 dataset/KonVid-150k/orig_10010534764_540_5s 27 0.6 +orig_10010719366_540_5s.mp4 dataset/KonVid-150k/orig_10010719366_540_5s 27 0.76 +orig_10010801643_540_5s.mp4 dataset/KonVid-150k/orig_10010801643_540_5s 27 0.84 +orig_10011053514_540_5s.mp4 dataset/KonVid-150k/orig_10011053514_540_5s 27 0.76 +orig_10011061534_540_5s.mp4 dataset/KonVid-150k/orig_10011061534_540_5s 27 0.6 +orig_10011062064_540_5s.mp4 dataset/KonVid-150k/orig_10011062064_540_5s 27 0.76 +orig_10011169015_540_5s.mp4 dataset/KonVid-150k/orig_10011169015_540_5s 27 0.8 +orig_10011241065_540_5s.mp4 dataset/KonVid-150k/orig_10011241065_540_5s 27 0.68 +orig_10011555465_540_5s.mp4 dataset/KonVid-150k/orig_10011555465_540_5s 27 0.44 +orig_10011557455_540_5s.mp4 dataset/KonVid-150k/orig_10011557455_540_5s 27 0.56 +orig_10011632586_540_5s.mp4 dataset/KonVid-150k/orig_10011632586_540_5s 27 0.64 +orig_10011701646_540_5s.mp4 dataset/KonVid-150k/orig_10011701646_540_5s 27 0.6 +orig_10011725614_540_5s.mp4 dataset/KonVid-150k/orig_10011725614_540_5s 27 0.68 +orig_10011987845_540_5s.mp4 dataset/KonVid-150k/orig_10011987845_540_5s 27 0.76 +orig_10012116333_540_5s.mp4 dataset/KonVid-150k/orig_10012116333_540_5s 27 0.64 +orig_10012238206_540_5s.mp4 dataset/KonVid-150k/orig_10012238206_540_5s 27 0.44 +orig_10012824424_540_5s.mp4 dataset/KonVid-150k/orig_10012824424_540_5s 27 0.88 +orig_10012847416_540_5s.mp4 dataset/KonVid-150k/orig_10012847416_540_5s 27 0.8 +orig_10012990555_540_5s.mp4 dataset/KonVid-150k/orig_10012990555_540_5s 27 0.56 +orig_10013258214_540_5s.mp4 dataset/KonVid-150k/orig_10013258214_540_5s 27 0.76 +orig_10013321156_540_5s.mp4 dataset/KonVid-150k/orig_10013321156_540_5s 27 0.56 +orig_10013362424_540_5s.mp4 dataset/KonVid-150k/orig_10013362424_540_5s 27 0.88 +orig_10013365284_540_5s.mp4 dataset/KonVid-150k/orig_10013365284_540_5s 27 0.72 +orig_10013372094_540_5s.mp4 dataset/KonVid-150k/orig_10013372094_540_5s 27 0.64 +orig_10013396873_540_5s.mp4 dataset/KonVid-150k/orig_10013396873_540_5s 27 0.8 +orig_10013411946_540_5s.mp4 dataset/KonVid-150k/orig_10013411946_540_5s 27 0.76 +orig_10013423236_540_5s.mp4 dataset/KonVid-150k/orig_10013423236_540_5s 27 0.64 +orig_10013427046_540_5s.mp4 dataset/KonVid-150k/orig_10013427046_540_5s 27 0.88 +orig_10013440546_540_5s.mp4 dataset/KonVid-150k/orig_10013440546_540_5s 27 0.72 +orig_10013441246_540_5s.mp4 dataset/KonVid-150k/orig_10013441246_540_5s 27 0.8 +orig_10013698564_540_5s.mp4 dataset/KonVid-150k/orig_10013698564_540_5s 27 0.48 +orig_10013733486_540_5s.mp4 dataset/KonVid-150k/orig_10013733486_540_5s 27 0.8 +orig_10014107655_540_5s.mp4 dataset/KonVid-150k/orig_10014107655_540_5s 27 0.6 +orig_10014566295_540_5s.mp4 dataset/KonVid-150k/orig_10014566295_540_5s 27 0.4 +orig_10014591473_540_5s.mp4 dataset/KonVid-150k/orig_10014591473_540_5s 27 0.56 +orig_10014702575_540_5s.mp4 dataset/KonVid-150k/orig_10014702575_540_5s 27 0.68 +orig_10014767146_540_5s.mp4 dataset/KonVid-150k/orig_10014767146_540_5s 27 0.64 +orig_10014821783_540_5s.mp4 dataset/KonVid-150k/orig_10014821783_540_5s 27 0.84 +orig_10014831134_540_5s.mp4 dataset/KonVid-150k/orig_10014831134_540_5s 27 0.8 +orig_10015000824_540_5s.mp4 dataset/KonVid-150k/orig_10015000824_540_5s 27 0.76 +orig_10015213114_540_5s.mp4 dataset/KonVid-150k/orig_10015213114_540_5s 27 0.84 +orig_10015301044_540_5s.mp4 dataset/KonVid-150k/orig_10015301044_540_5s 27 0.4 +orig_10015364556_540_5s.mp4 dataset/KonVid-150k/orig_10015364556_540_5s 27 0.56 +orig_10015404736_540_5s.mp4 dataset/KonVid-150k/orig_10015404736_540_5s 27 0.56 +orig_10015443855_540_5s.mp4 dataset/KonVid-150k/orig_10015443855_540_5s 27 0.44 +orig_10015504523_540_5s.mp4 dataset/KonVid-150k/orig_10015504523_540_5s 27 0.36 +orig_10015604956_540_5s.mp4 dataset/KonVid-150k/orig_10015604956_540_5s 27 0.32 +orig_10015653405_540_5s.mp4 dataset/KonVid-150k/orig_10015653405_540_5s 27 0.48 +orig_10015713906_540_5s.mp4 dataset/KonVid-150k/orig_10015713906_540_5s 27 0.88 +orig_10015785976_540_5s.mp4 dataset/KonVid-150k/orig_10015785976_540_5s 27 0.6 +orig_10016118206_540_5s.mp4 dataset/KonVid-150k/orig_10016118206_540_5s 26 0.68 +orig_10016377746_540_5s.mp4 dataset/KonVid-150k/orig_10016377746_540_5s 27 0.6 +orig_10016382545_540_5s.mp4 dataset/KonVid-150k/orig_10016382545_540_5s 27 0.6 +orig_10016409216_540_5s.mp4 dataset/KonVid-150k/orig_10016409216_540_5s 27 0.76 +orig_10017158323_540_5s.mp4 dataset/KonVid-150k/orig_10017158323_540_5s 27 0.68 +orig_10017178975_540_5s.mp4 dataset/KonVid-150k/orig_10017178975_540_5s 27 0.68 +orig_10017297794_540_5s.mp4 dataset/KonVid-150k/orig_10017297794_540_5s 27 0.8 +orig_10017303604_540_5s.mp4 dataset/KonVid-150k/orig_10017303604_540_5s 27 0.88 +orig_10017542406_540_5s.mp4 dataset/KonVid-150k/orig_10017542406_540_5s 27 0.88 +orig_10019048055_540_5s.mp4 dataset/KonVid-150k/orig_10019048055_540_5s 27 0.76 +orig_10019365243_540_5s.mp4 dataset/KonVid-150k/orig_10019365243_540_5s 27 0.56 +orig_10019485755_540_5s.mp4 dataset/KonVid-150k/orig_10019485755_540_5s 27 0.76 +orig_10019628726_540_5s.mp4 dataset/KonVid-150k/orig_10019628726_540_5s 27 0.44 +orig_10019913435_540_5s.mp4 dataset/KonVid-150k/orig_10019913435_540_5s 27 0.72 +orig_10019996216_540_5s.mp4 dataset/KonVid-150k/orig_10019996216_540_5s 27 0.48 +orig_10020203895_540_5s.mp4 dataset/KonVid-150k/orig_10020203895_540_5s 27 0.92 +orig_10020565034_540_5s.mp4 dataset/KonVid-150k/orig_10020565034_540_5s 27 0.68 +orig_10020570684_540_5s.mp4 dataset/KonVid-150k/orig_10020570684_540_5s 27 0.84 +orig_10020611266_540_5s.mp4 dataset/KonVid-150k/orig_10020611266_540_5s 27 0.72 +orig_10020618715_540_5s.mp4 dataset/KonVid-150k/orig_10020618715_540_5s 27 0.68 +orig_10020622206_540_5s.mp4 dataset/KonVid-150k/orig_10020622206_540_5s 27 0.68 +orig_10020690973_540_5s.mp4 dataset/KonVid-150k/orig_10020690973_540_5s 27 0.64 +orig_10022104903_540_5s.mp4 dataset/KonVid-150k/orig_10022104903_540_5s 27 0.52 +orig_10022295695_540_5s.mp4 dataset/KonVid-150k/orig_10022295695_540_5s 27 0.76 +orig_10023068726_540_5s.mp4 dataset/KonVid-150k/orig_10023068726_540_5s 27 0.72 +orig_10023166634_540_5s.mp4 dataset/KonVid-150k/orig_10023166634_540_5s 27 0.92 +orig_10023555633_540_5s.mp4 dataset/KonVid-150k/orig_10023555633_540_5s 27 0.76 +orig_10023611244_540_5s.mp4 dataset/KonVid-150k/orig_10023611244_540_5s 27 0.84 +orig_10023648666_540_5s.mp4 dataset/KonVid-150k/orig_10023648666_540_5s 27 0.8 +orig_10023807496_540_5s.mp4 dataset/KonVid-150k/orig_10023807496_540_5s 27 0.76 +orig_10023881763_540_5s.mp4 dataset/KonVid-150k/orig_10023881763_540_5s 27 0.64 +orig_10023950366_540_5s.mp4 dataset/KonVid-150k/orig_10023950366_540_5s 27 0.8 +orig_10023980785_540_5s.mp4 dataset/KonVid-150k/orig_10023980785_540_5s 27 0.8 +orig_10024067406_540_5s.mp4 dataset/KonVid-150k/orig_10024067406_540_5s 27 0.48 +orig_10024078356_540_5s.mp4 dataset/KonVid-150k/orig_10024078356_540_5s 27 0.6 +orig_10024107254_540_5s.mp4 dataset/KonVid-150k/orig_10024107254_540_5s 27 0.6 +orig_10024214584_540_5s.mp4 dataset/KonVid-150k/orig_10024214584_540_5s 26 0.76 +orig_10024252673_540_5s.mp4 dataset/KonVid-150k/orig_10024252673_540_5s 26 0.8 +orig_10024278303_540_5s.mp4 dataset/KonVid-150k/orig_10024278303_540_5s 26 0.72 +orig_10024379054_540_5s.mp4 dataset/KonVid-150k/orig_10024379054_540_5s 26 0.68 +orig_10024417363_540_5s.mp4 dataset/KonVid-150k/orig_10024417363_540_5s 26 0.76 +orig_10024712346_540_5s.mp4 dataset/KonVid-150k/orig_10024712346_540_5s 26 0.68 +orig_10024744433_540_5s.mp4 dataset/KonVid-150k/orig_10024744433_540_5s 26 0.72 +orig_10024879803_540_5s.mp4 dataset/KonVid-150k/orig_10024879803_540_5s 26 0.52 +orig_10024922393_540_5s.mp4 dataset/KonVid-150k/orig_10024922393_540_5s 26 0.6 +orig_10026138756_540_5s.mp4 dataset/KonVid-150k/orig_10026138756_540_5s 27 0.56 +orig_10026847005_540_5s.mp4 dataset/KonVid-150k/orig_10026847005_540_5s 27 0.64 +orig_10026882385_540_5s.mp4 dataset/KonVid-150k/orig_10026882385_540_5s 27 0.76 +orig_10026908966_540_5s.mp4 dataset/KonVid-150k/orig_10026908966_540_5s 27 0.76 +orig_10026948093_540_5s.mp4 dataset/KonVid-150k/orig_10026948093_540_5s 27 0.4 +orig_10027007645_540_5s.mp4 dataset/KonVid-150k/orig_10027007645_540_5s 27 0.84 +orig_10027055546_540_5s.mp4 dataset/KonVid-150k/orig_10027055546_540_5s 27 0.64 +orig_10027124844_540_5s.mp4 dataset/KonVid-150k/orig_10027124844_540_5s 27 0.52 +orig_10027152133_540_5s.mp4 dataset/KonVid-150k/orig_10027152133_540_5s 27 0.68 +orig_10027227476_540_5s.mp4 dataset/KonVid-150k/orig_10027227476_540_5s 27 0.64 +orig_10027287845_540_5s.mp4 dataset/KonVid-150k/orig_10027287845_540_5s 27 0.64 +orig_10027377666_540_5s.mp4 dataset/KonVid-150k/orig_10027377666_540_5s 27 0.6 +orig_10027398054_540_5s.mp4 dataset/KonVid-150k/orig_10027398054_540_5s 27 0.68 +orig_10027409606_540_5s.mp4 dataset/KonVid-150k/orig_10027409606_540_5s 27 0.64 +orig_10027516434_540_5s.mp4 dataset/KonVid-150k/orig_10027516434_540_5s 27 0.72 +orig_10027546895_540_5s.mp4 dataset/KonVid-150k/orig_10027546895_540_5s 27 0.72 +orig_10027562966_540_5s.mp4 dataset/KonVid-150k/orig_10027562966_540_5s 27 0.84 +orig_10027816373_540_5s.mp4 dataset/KonVid-150k/orig_10027816373_540_5s 27 0.64 +orig_10027860716_540_5s.mp4 dataset/KonVid-150k/orig_10027860716_540_5s 27 0.56 +orig_10027946873_540_5s.mp4 dataset/KonVid-150k/orig_10027946873_540_5s 27 0.68 +orig_10028008425_540_5s.mp4 dataset/KonVid-150k/orig_10028008425_540_5s 26 0.64 +orig_10028042924_540_5s.mp4 dataset/KonVid-150k/orig_10028042924_540_5s 27 0.76 +orig_10028071313_540_5s.mp4 dataset/KonVid-150k/orig_10028071313_540_5s 26 0.68 +orig_10028083336_540_5s.mp4 dataset/KonVid-150k/orig_10028083336_540_5s 27 0.64 +orig_10028089525_540_5s.mp4 dataset/KonVid-150k/orig_10028089525_540_5s 27 0.72 +orig_10028099125_540_5s.mp4 dataset/KonVid-150k/orig_10028099125_540_5s 26 0.76 +orig_10028101135_540_5s.mp4 dataset/KonVid-150k/orig_10028101135_540_5s 27 0.52 +orig_10028187176_540_5s.mp4 dataset/KonVid-150k/orig_10028187176_540_5s 26 0.6 +orig_10028235633_540_5s.mp4 dataset/KonVid-150k/orig_10028235633_540_5s 27 0.56 +orig_10028412443_540_5s.mp4 dataset/KonVid-150k/orig_10028412443_540_5s 27 0.72 +orig_10028414874_540_5s.mp4 dataset/KonVid-150k/orig_10028414874_540_5s 27 0.76 +orig_10028423426_540_5s.mp4 dataset/KonVid-150k/orig_10028423426_540_5s 26 0.68 +orig_10028433665_540_5s.mp4 dataset/KonVid-150k/orig_10028433665_540_5s 27 0.8 +orig_10028506284_540_5s.mp4 dataset/KonVid-150k/orig_10028506284_540_5s 26 0.6 +orig_10028519776_540_5s.mp4 dataset/KonVid-150k/orig_10028519776_540_5s 26 0.6 +orig_10028526824_540_5s.mp4 dataset/KonVid-150k/orig_10028526824_540_5s 27 0.56 +orig_10028549695_540_5s.mp4 dataset/KonVid-150k/orig_10028549695_540_5s 27 0.64 +orig_10028629206_540_5s.mp4 dataset/KonVid-150k/orig_10028629206_540_5s 26 0.68 +orig_10028664876_540_5s.mp4 dataset/KonVid-150k/orig_10028664876_540_5s 26 0.64 +orig_10028716696_540_5s.mp4 dataset/KonVid-150k/orig_10028716696_540_5s 27 0.68 +orig_10028720625_540_5s.mp4 dataset/KonVid-150k/orig_10028720625_540_5s 27 0.4 +orig_10028838444_540_5s.mp4 dataset/KonVid-150k/orig_10028838444_540_5s 26 0.72 +orig_10029033734_540_5s.mp4 dataset/KonVid-150k/orig_10029033734_540_5s 26 0.68 +orig_10029143176_540_5s.mp4 dataset/KonVid-150k/orig_10029143176_540_5s 26 0.64 +orig_10029237424_540_5s.mp4 dataset/KonVid-150k/orig_10029237424_540_5s 26 0.44 +orig_10029241143_540_5s.mp4 dataset/KonVid-150k/orig_10029241143_540_5s 26 0.8 +orig_10029748354_540_5s.mp4 dataset/KonVid-150k/orig_10029748354_540_5s 27 0.76 +orig_10029888734_540_5s.mp4 dataset/KonVid-150k/orig_10029888734_540_5s 27 0.8 +orig_10029953335_540_5s.mp4 dataset/KonVid-150k/orig_10029953335_540_5s 27 0.44 +orig_10029967365_540_5s.mp4 dataset/KonVid-150k/orig_10029967365_540_5s 27 0.6 +orig_10030001126_540_5s.mp4 dataset/KonVid-150k/orig_10030001126_540_5s 27 0.68 +orig_10030002173_540_5s.mp4 dataset/KonVid-150k/orig_10030002173_540_5s 27 0.64 +orig_10030237673_540_5s.mp4 dataset/KonVid-150k/orig_10030237673_540_5s 27 0.76 +orig_10030294504_540_5s.mp4 dataset/KonVid-150k/orig_10030294504_540_5s 27 0.72 diff --git a/applications/VideoQualityAssessment/main.py b/applications/VideoQualityAssessment/main.py new file mode 100644 index 0000000000000000000000000000000000000000..1d68e173cdc52efca3506567543eea6c41a11eeb --- /dev/null +++ b/applications/VideoQualityAssessment/main.py @@ -0,0 +1,88 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import paddle +import argparse +from paddlevideo.utils import get_config +from paddlevideo.tasks import train_model, test_model +from paddlevideo.utils import get_dist_info + + +def parse_args(): + """parse_args""" + parser = argparse.ArgumentParser("PaddleVideo train script") + parser.add_argument('-c', + '--config', + type=str, + default='configs/example.yaml', + help='config file path') + parser.add_argument('-o', + '--override', + action='append', + default=[], + help='config options to be overridden') + parser.add_argument('--test', + action='store_true', + help='whether to test a model') + parser.add_argument('--train_dali', + action='store_true', + help='whether to use dali to speed up training') + parser.add_argument('--multigrid', + action='store_true', + help='whether to use multigrid training') + parser.add_argument('-w', + '--weights', + type=str, + help='weights for finetuning or testing') + parser.add_argument('--fleet', + action='store_true', + help='whether to use fleet run distributed training') + parser.add_argument('--amp', + action='store_true', + help='whether to open amp training.') + + parser.add_argument( + '--validate', + action='store_true', + help='whether to evaluate the checkpoint during training') + + args = parser.parse_args() + return args + + +def main(): + """main""" + args = parse_args() + cfg = get_config(args.config, overrides=args.override) + + _, world_size = get_dist_info() + parallel = world_size != 1 + if parallel: + paddle.distributed.init_parallel_env() + + if args.test: + test_model(cfg, weights=args.weights, parallel=parallel) + else: + train_model(cfg, + weights=args.weights, + parallel=parallel, + validate=args.validate, + fleet=args.fleet, + amp=args.amp) + + +if __name__ == '__main__': + main() diff --git a/applications/VideoQualityAssessment/paddlevideo/__init__.py b/applications/VideoQualityAssessment/paddlevideo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..49a4637b2c983eb709ae4b48eb02dfe15eb87da8 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/__init__.py @@ -0,0 +1,17 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .version import paddlevideo_version diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/__init__.py b/applications/VideoQualityAssessment/paddlevideo/loader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a3f8fa24c3576dc8fb0bb1f7e7ee9e11cd2edfbd --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/__init__.py @@ -0,0 +1,21 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .builder import build_dataset, build_dataloader, build_batch_pipeline +from .dataset import VideoDataset +__all__ = [ + 'build_dataset', 'build_dataloader', 'build_batch_pipeline', 'VideoDataset' +] diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/builder.py b/applications/VideoQualityAssessment/paddlevideo/loader/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..6dc1d74bdd2fd8a4a85e1164a7819c016829d439 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/builder.py @@ -0,0 +1,126 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +import signal +import os +import paddle +from paddle.io import DataLoader, DistributedBatchSampler +from .registry import DATASETS, PIPELINES +from ..utils.build_utils import build +from .pipelines.compose import Compose +from paddlevideo.utils import get_logger +import numpy as np + +logger = get_logger("paddlevideo") + + +def build_pipeline(cfg): + """Build pipeline. + Args: + cfg (dict): root config dict. + """ + return Compose(cfg) + + +def build_dataset(cfg): + """Build dataset. + Args: + cfg (dict): root config dict. + + Returns: + dataset: dataset. + """ + #XXX: ugly code here! + cfg_dataset, cfg_pipeline = cfg + cfg_dataset.pipeline = build_pipeline(cfg_pipeline) + dataset = build(cfg_dataset, DATASETS, key="format") + return dataset + + +def build_batch_pipeline(cfg): + """build batch pipeline""" + batch_pipeline = build(cfg, PIPELINES) + return batch_pipeline + + +def build_dataloader(dataset, + batch_size, + num_workers, + places, + shuffle=True, + drop_last=True, + multigrid=False, + collate_fn_cfg=None, + **kwargs): + """Build Paddle Dataloader. + + XXX explain how the dataloader work! + + Args: + dataset (paddle.dataset): A PaddlePaddle dataset object. + batch_size (int): batch size on single card. + num_worker (int): num_worker + shuffle(bool): whether to shuffle the data at every epoch. + """ + sampler = DistributedBatchSampler(dataset, + batch_size=batch_size, + shuffle=shuffle, + drop_last=drop_last) + + #NOTE(shipping): when switch the mix operator on, such as: mixup, cutmix. + # batch like: [[img, label, attibute, ...], [imgs, label, attribute, ...], ...] will recollate to: + # [[img, img, ...], [label, label, ...], [attribute, attribute, ...], ...] as using numpy.transpose. + + def mix_collate_fn(batch): + """mix collate fn""" + pipeline = build_batch_pipeline(collate_fn_cfg) + batch = pipeline(batch) + slots = [] + for items in batch: + for i, item in enumerate(items): + if len(slots) < len(items): + slots.append([item]) + else: + slots[i].append(item) + return [np.stack(slot, axis=0) for slot in slots] + + #if collate_fn_cfg is not None: + #ugly code here. collate_fn is mix op config + # collate_fn = mix_collate_fn(collate_fn_cfg) + + data_loader = DataLoader( + dataset, + batch_sampler=sampler, + places=places, + num_workers=num_workers, + collate_fn=mix_collate_fn if collate_fn_cfg is not None else None, + return_list=True, + **kwargs) + + return data_loader + + +def term_mp(sig_num, frame): + """ kill all child processes + """ + pid = os.getpid() + pgid = os.getpgid(os.getpid()) + logger.info("main proc {} exit, kill process group " "{}".format(pid, pgid)) + os.killpg(pgid, signal.SIGKILL) + return + + +signal.signal(signal.SIGINT, term_mp) +signal.signal(signal.SIGTERM, term_mp) diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/dataset/__init__.py b/applications/VideoQualityAssessment/paddlevideo/loader/dataset/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2de277e1d2f3aa618242df9f6f79a19ac73993a0 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/dataset/__init__.py @@ -0,0 +1,21 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .video import VideoDataset +#from .frame import FrameDataset +from .frame_rec import FrameRecDataset + +__all__ = ['VideoDataset', 'FrameRecDataset'] diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/dataset/base.py b/applications/VideoQualityAssessment/paddlevideo/loader/dataset/base.py new file mode 100644 index 0000000000000000000000000000000000000000..9400aca56ab353279d4400d725eb84221c58efc2 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/dataset/base.py @@ -0,0 +1,83 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os.path as osp +import copy +import numpy as np +from abc import ABC, abstractmethod + +import paddle +from paddle.io import Dataset + + +class BaseDataset(Dataset, ABC): + """Base class for datasets + + All datasets should subclass it. + All subclass should overwrite: + + - Method: `load_file`, load info from index file. + - Method: `prepare_train`, providing train data. + - Method: `prepare_test`, providing test data. + + Args: + file_path (str): index file path. + pipeline (Sequence XXX) + data_prefix (str): directory path of the data. Default: None. + test_mode (bool): whether to build test dataset. Default: False. + + """ + def __init__(self, file_path, pipeline, data_prefix=None, test_mode=False): + + super().__init__() + self.file_path = file_path + self.data_prefix = osp.realpath(data_prefix) if \ + data_prefix is not None and osp.isdir(data_prefix) else data_prefix + self.test_mode = test_mode + self.pipeline = pipeline + self.info = self.load_file() + + @abstractmethod + def load_file(self): + """load the video information from the index file path.""" + pass + + def prepare_train(self, idx): + """TRAIN & VALID. Prepare the data for training/valid given the index.""" + #Note: For now, paddle.io.DataLoader cannot support dict type retval, so convert to list here + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + #unsqueeze label to list + return results['imgs'], np.array([results['labels']]) + + def prepare_test(self, idx): + """TEST: Prepare the data for test given the index.""" + #Note: For now, paddle.io.DataLoader cannot support dict type retval, so convert to list here + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + #unsqueeze label to list + return results['imgs'], np.array([results['labels']]) + + def __len__(self): + """get the size of the dataset.""" + return len(self.info) + + def __getitem__(self, idx): + """ Get the sample for either training or testing given index""" + if self.test_mode: + return self.prepare_test(idx) + else: + return self.prepare_train(idx) diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/dataset/frame_rec.py b/applications/VideoQualityAssessment/paddlevideo/loader/dataset/frame_rec.py new file mode 100644 index 0000000000000000000000000000000000000000..de2dab63a2f710cd6b7019b2384c279d4b5b45b3 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/dataset/frame_rec.py @@ -0,0 +1,110 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os.path as osp +import copy +import random +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class FrameRecDataset(BaseDataset): + """Rawframe dataset for action recognition. + The dataset loads raw frames from frame files, and apply specified transform operatation them. + The indecx file is a text file with multiple lines, and each line indicates the directory of frames of a video, toatl frames of the video, and its label, which split with a whitespace. + Example of an index file: + + .. code-block:: txt + + file_path-1 150 1 + file_path-2 160 1 + file_path-3 170 2 + file_path-4 180 2 + + Args: + file_path (str): Path to the index file. + pipeline(XXX): + data_prefix (str): directory path of the data. Default: None. + test_mode (bool): Whether to bulid the test dataset. Default: False. + suffix (str): suffix of file. Default: 'img_{:05}.jpg'. + + """ + def __init__(self, + file_path, + pipeline, + num_retries=5, + data_prefix=None, + test_mode=False, + suffix='img_{:05}.jpg'): + self.num_retries = num_retries + self.suffix = suffix + super().__init__(file_path, pipeline, data_prefix, test_mode) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split() + mp4_path, frame_dir, frames_len, labels = line_split + + if self.data_prefix is not None: + frame_dir = osp.join(self.data_prefix, frame_dir) + info.append( + dict(frame_dir=frame_dir, + suffix=self.suffix, + frames_len=frames_len, + labels=float(labels))) + return info + + def prepare_train(self, idx): + """Prepare the frames for training/valid given index. """ + #Try to catch Exception caused by reading missing frames files + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['frame_dir'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) + + def prepare_test(self, idx): + """Prepare the frames for test given index. """ + #Try to catch Exception caused by reading missing frames files + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['frame_dir'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/dataset/video.py b/applications/VideoQualityAssessment/paddlevideo/loader/dataset/video.py new file mode 100644 index 0000000000000000000000000000000000000000..289e526f5ab45332ae079754bffffead1df8f836 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/dataset/video.py @@ -0,0 +1,95 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os.path as osp +import copy +import random +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class VideoDataset(BaseDataset): + """Video dataset for action recognition + The dataset loads raw videos and apply specified transforms on them. + The index file is a file with multiple lines, and each line indicates + a sample video with the filepath and label, which are split with a whitesapce. + Example of a inde file: + .. code-block:: txt + path/000.mp4 1 + path/001.mp4 1 + path/002.mp4 2 + path/003.mp4 2 + Args: + file_path(str): Path to the index file. + pipeline(XXX): A sequence of data transforms. + **kwargs: Keyword arguments for ```BaseDataset```. + """ + def __init__(self, file_path, pipeline, num_retries=5, **kwargs): + self.num_retries = num_retries + super().__init__(file_path, pipeline, **kwargs) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split() + filename, labels = line_split + #TODO(hj): Required suffix format: may mp4/avi/wmv + filename = filename + '.avi' + if self.data_prefix is not None: + filename = osp.join(self.data_prefix, filename) + info.append(dict(filename=filename, labels=int(labels))) + return info + + def prepare_train(self, idx): + """TRAIN & VALID. Prepare the data for training/valid given the index.""" + #Try to catch Exception caused by reading corrupted video file + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['filename'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) + + def prepare_test(self, idx): + """TEST. Prepare the data for test given the index.""" + #Try to catch Exception caused by reading corrupted video file + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['filename'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/__init__.py b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b1fd63e3112cde336420e7497bc0bfe5eabe79ef --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/__init__.py @@ -0,0 +1,50 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .augmentations import ( + Scale, + RandomCrop, + CenterCrop, + RandomFlip, + Image2Array, + Normalization, + JitterScale, + MultiCrop, + PackOutput, +) + +from .compose import Compose +from .decode import VideoDecoder, FrameDecoder +from .sample import Sampler +from .mix import Mixup, Cutmix + +__all__ = [ + 'Scale', + 'RandomCrop', + 'CenterCrop', + 'RandomFlip', + 'Image2Array', + 'Normalization', + 'Compose', + 'VideoDecoder', + 'FrameDecoder', + 'Sampler', + 'Mixup', + 'Cutmix', + 'JitterScale', + 'MultiCrop', + 'PackOutput', +] diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/augmentations.py b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/augmentations.py new file mode 100644 index 0000000000000000000000000000000000000000..bee6b3281aefab88fe9d316696c963aaaf7c253d --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/augmentations.py @@ -0,0 +1,498 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import random +import numpy as np +import math +from PIL import Image +from ..registry import PIPELINES +from collections.abc import Sequence + + +@PIPELINES.register() +class Scale(object): + """ + Scale images. + Args: + short_size(float | int): Short size of an image will be scaled to the short_size. + """ + def __init__(self, short_size): + self.short_size = short_size + + def __call__(self, results): + """ + Performs resize operations. + Args: + imgs (Sequence[PIL.Image]): List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + resized_imgs: List where each item is a PIL.Image after scaling. + """ + imgs = results['imgs'] + resized_imgs = [] + for i in range(len(imgs)): + img = imgs[i] + w, h = img.size + if (w <= h and w == self.short_size) or (h <= w + and h == self.short_size): + resized_imgs.append(img) + continue + + if w < h: + ow = self.short_size + oh = int(self.short_size * 4.0 / 3.0) + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + else: + oh = self.short_size + ow = int(self.short_size * 4.0 / 3.0) + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + results['imgs'] = resized_imgs + return results + + +@PIPELINES.register() +class RandomCrop(object): + """ + Random crop images. + Args: + target_size(int): Random crop a square with the target_size from an image. + """ + def __init__(self, target_size): + self.target_size = target_size + + def __call__(self, results): + """ + Performs random crop operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + crop_imgs: List where each item is a PIL.Image after random crop. + """ + imgs = results['imgs'] + w, h = imgs[0].size + th, tw = self.target_size, self.target_size + + assert (w >= self.target_size) and (h >= self.target_size), \ + "image width({}) and height({}) should be larger than crop size {}".format( + w, h, self.target_size) + + crop_images = [] + x1 = random.randint(0, w - tw) + y1 = random.randint(0, h - th) + + for img in imgs: + if w == tw and h == th: + crop_images.append(img) + else: + crop_images.append(img.crop((x1, y1, x1 + tw, y1 + th))) + results['imgs'] = crop_images + return results + + +@PIPELINES.register() +class CenterCrop(object): + """ + Center crop images. + Args: + target_size(int): Center crop a square with the target_size from an image. + """ + def __init__(self, target_size): + self.target_size = target_size + + def __call__(self, results): + """ + Performs Center crop operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + ccrop_imgs: List where each item is a PIL.Image after Center crop. + """ + imgs = results['imgs'] + ccrop_imgs = [] + for img in imgs: + w, h = img.size + th, tw = self.target_size, self.target_size + assert (w >= self.target_size) and (h >= self.target_size), \ + "image width({}) and height({}) should be larger than crop size {}".format( + w, h, self.target_size) + x1 = int(round((w - tw) / 2.)) + y1 = int(round((h - th) / 2.)) + ccrop_imgs.append(img.crop((x1, y1, x1 + tw, y1 + th))) + results['imgs'] = ccrop_imgs + return results + + +@PIPELINES.register() +class MultiScaleCrop(object): + def __init__( + self, + target_size, #NOTE: named target size now, but still pass short size in it! + scales=None, + max_distort=1, + fix_crop=True, + more_fix_crop=True): + self.target_size = target_size + self.scales = scales if scales else [1, .875, .75, .66] + self.max_distort = max_distort + self.fix_crop = fix_crop + self.more_fix_crop = more_fix_crop + + def __call__(self, results): + """ + Performs MultiScaleCrop operations. + Args: + imgs: List where wach item is a PIL.Image. + XXX: + results: + + """ + imgs = results['imgs'] + + input_size = [self.target_size, self.target_size] + + im_size = imgs[0].size + + # get random crop offset + def _sample_crop_size(im_size): + image_w, image_h = im_size[0], im_size[1] + + base_size = min(image_w, image_h) + crop_sizes = [int(base_size * x) for x in self.scales] + crop_h = [ + input_size[1] if abs(x - input_size[1]) < 3 else x + for x in crop_sizes + ] + crop_w = [ + input_size[0] if abs(x - input_size[0]) < 3 else x + for x in crop_sizes + ] + + pairs = [] + for i, h in enumerate(crop_h): + for j, w in enumerate(crop_w): + if abs(i - j) <= self.max_distort: + pairs.append((w, h)) + crop_pair = random.choice(pairs) + if not self.fix_crop: + w_offset = random.randint(0, image_w - crop_pair[0]) + h_offset = random.randint(0, image_h - crop_pair[1]) + else: + w_step = (image_w - crop_pair[0]) / 4 + h_step = (image_h - crop_pair[1]) / 4 + + ret = list() + ret.append((0, 0)) # upper left + if w_step != 0: + ret.append((4 * w_step, 0)) # upper right + if h_step != 0: + ret.append((0, 4 * h_step)) # lower left + if h_step != 0 and w_step != 0: + ret.append((4 * w_step, 4 * h_step)) # lower right + if h_step != 0 or w_step != 0: + ret.append((2 * w_step, 2 * h_step)) # center + + if self.more_fix_crop: + ret.append((0, 2 * h_step)) # center left + ret.append((4 * w_step, 2 * h_step)) # center right + ret.append((2 * w_step, 4 * h_step)) # lower center + ret.append((2 * w_step, 0 * h_step)) # upper center + + ret.append((1 * w_step, 1 * h_step)) # upper left quarter + ret.append((3 * w_step, 1 * h_step)) # upper right quarter + ret.append((1 * w_step, 3 * h_step)) # lower left quarter + ret.append((3 * w_step, 3 * h_step)) # lower righ quarter + + w_offset, h_offset = random.choice(ret) + + return crop_pair[0], crop_pair[1], w_offset, h_offset + + crop_w, crop_h, offset_w, offset_h = _sample_crop_size(im_size) + crop_img_group = [ + img.crop((offset_w, offset_h, offset_w + crop_w, offset_h + crop_h)) + for img in imgs + ] + ret_img_group = [ + img.resize((input_size[0], input_size[1]), Image.BILINEAR) + for img in crop_img_group + ] + results['imgs'] = ret_img_group + return results + + +@PIPELINES.register() +class RandomFlip(object): + """ + Random Flip images. + Args: + p(float): Random flip images with the probability p. + """ + def __init__(self, p=0.5): + self.p = p + + def __call__(self, results): + """ + Performs random flip operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + flip_imgs: List where each item is a PIL.Image after random flip. + """ + imgs = results['imgs'] + v = random.random() + if v < self.p: + results['imgs'] = [ + img.transpose(Image.FLIP_LEFT_RIGHT) for img in imgs + ] + else: + results['imgs'] = imgs + return results + + +@PIPELINES.register() +class Image2Array(object): + """ + transfer PIL.Image to Numpy array and transpose dimensions from 'dhwc' to 'dchw'. + Args: + transpose: whether to transpose or not, default True, False for slowfast. + """ + def __init__(self, transpose=True): + self.transpose = transpose + + def __call__(self, results): + """ + Performs Image to NumpyArray operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + np_imgs: Numpy array. + """ + imgs = results['imgs'] + np_imgs = (np.stack(imgs)).astype('float32') + if self.transpose: + np_imgs = np_imgs.transpose(0, 3, 1, 2) #nchw + results['imgs'] = np_imgs + return results + + +@PIPELINES.register() +class Normalization(object): + """ + Normalization. + Args: + mean(Sequence[float]): mean values of different channels. + std(Sequence[float]): std values of different channels. + tensor_shape(list): size of mean, default [3,1,1]. For slowfast, [1,1,1,3] + """ + def __init__(self, mean, std, tensor_shape=[3, 1, 1]): + if not isinstance(mean, Sequence): + raise TypeError( + 'Mean must be list, tuple or np.ndarray, but got {type(mean)}') + if not isinstance(std, Sequence): + raise TypeError( + 'Std must be list, tuple or np.ndarray, but got {type(std)}') + self.mean = np.array(mean).reshape(tensor_shape).astype(np.float32) + self.std = np.array(std).reshape(tensor_shape).astype(np.float32) + + def __call__(self, results): + """ + Performs normalization operations. + Args: + imgs: Numpy array. + return: + np_imgs: Numpy array after normalization. + """ + imgs = results['imgs'] + norm_imgs = imgs / 255. + norm_imgs -= self.mean + norm_imgs /= self.std + results['imgs'] = norm_imgs + return results + + +@PIPELINES.register() +class JitterScale(object): + """ + Scale image, while the target short size is randomly select between min_size and max_size. + Args: + min_size: Lower bound for random sampler. + max_size: Higher bound for random sampler. + """ + def __init__(self, + min_size, + max_size, + short_cycle_factors=[0.5, 0.7071], + default_min_size=256): + self.default_min_size = default_min_size + self.orig_min_size = self.min_size = min_size + self.max_size = max_size + self.short_cycle_factors = short_cycle_factors + + def __call__(self, results): + """ + Performs jitter resize operations. + Args: + imgs (Sequence[PIL.Image]): List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + resized_imgs: List where each item is a PIL.Image after scaling. + """ + short_cycle_idx = results.get('short_cycle_idx') + if short_cycle_idx in [0, 1]: + self.min_size = int( + round(self.short_cycle_factors[short_cycle_idx] * + self.default_min_size)) + else: + self.min_size = self.orig_min_size + + imgs = results['imgs'] + size = int(round(np.random.uniform(self.min_size, self.max_size))) + assert (len(imgs) >= 1) , \ + "len(imgs):{} should be larger than 1".format(len(imgs)) + width, height = imgs[0].size + if (width <= height and width == size) or (height <= width + and height == size): + return results + + new_width = size + new_height = size + if width < height: + new_height = int(math.floor((float(height) / width) * size)) + else: + new_width = int(math.floor((float(width) / height) * size)) + + frames_resize = [] + for j in range(len(imgs)): + img = imgs[j] + scale_img = img.resize((new_width, new_height), Image.BILINEAR) + frames_resize.append(scale_img) + + results['imgs'] = frames_resize + return results + + +@PIPELINES.register() +class MultiCrop(object): + """ + Random crop image. + This operation can perform multi-crop during multi-clip test, as in slowfast model. + Args: + target_size(int): Random crop a square with the target_size from an image. + """ + def __init__(self, + target_size, + default_crop_size=224, + short_cycle_factors=[0.5, 0.7071], + test_mode=False): + self.orig_target_size = self.target_size = target_size + self.short_cycle_factors = short_cycle_factors + self.default_crop_size = default_crop_size + self.test_mode = test_mode + + def __call__(self, results): + """ + Performs random crop operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + crop_imgs: List where each item is a PIL.Image after random crop. + """ + imgs = results['imgs'] + spatial_sample_index = results['spatial_sample_index'] + spatial_num_clips = results['spatial_num_clips'] + + short_cycle_idx = results.get('short_cycle_idx') + if short_cycle_idx in [0, 1]: + self.target_size = int( + round(self.short_cycle_factors[short_cycle_idx] * + self.default_crop_size)) + else: + self.target_size = self.orig_target_size # use saved value before call + + w, h = imgs[0].size + if w == self.target_size and h == self.target_size: + return results + + assert (w >= self.target_size) and (h >= self.target_size), \ + "image width({}) and height({}) should be larger than crop size({},{})".format(w, h, self.target_size, self.target_size) + frames_crop = [] + if not self.test_mode: + x_offset = random.randint(0, w - self.target_size) + y_offset = random.randint(0, h - self.target_size) + else: #multi-crop + x_gap = int( + math.ceil((w - self.target_size) / (spatial_num_clips - 1))) + y_gap = int( + math.ceil((h - self.target_size) / (spatial_num_clips - 1))) + if h > w: + x_offset = int(math.ceil((w - self.target_size) / 2)) + if spatial_sample_index == 0: + y_offset = 0 + elif spatial_sample_index == spatial_num_clips - 1: + y_offset = h - self.target_size + else: + y_offset = y_gap * spatial_sample_index + else: + y_offset = int(math.ceil((h - self.target_size) / 2)) + if spatial_sample_index == 0: + x_offset = 0 + elif spatial_sample_index == spatial_num_clips - 1: + x_offset = w - self.target_size + else: + x_offset = x_gap * spatial_sample_index + + for img in imgs: + nimg = img.crop((x_offset, y_offset, x_offset + self.target_size, + y_offset + self.target_size)) + frames_crop.append(nimg) + results['imgs'] = frames_crop + return results + + +@PIPELINES.register() +class PackOutput(object): + """ + In slowfast model, we want to get slow pathway from fast pathway based on + alpha factor. + Args: + alpha(int): temporal length of fast/slow + """ + def __init__(self, alpha): + self.alpha = alpha + + def __call__(self, results): + fast_pathway = results['imgs'] + + # sample num points between start and end + slow_idx_start = 0 + slow_idx_end = fast_pathway.shape[0] - 1 + slow_idx_num = fast_pathway.shape[0] // self.alpha + slow_idxs_select = np.linspace(slow_idx_start, slow_idx_end, + slow_idx_num).astype("int64") + slow_pathway = fast_pathway[slow_idxs_select] + + # T H W C -> C T H W. + slow_pathway = slow_pathway.transpose(3, 0, 1, 2) + fast_pathway = fast_pathway.transpose(3, 0, 1, 2) + + # slow + fast + frames_list = [slow_pathway, fast_pathway] + results['imgs'] = frames_list + return results diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/compose.py b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/compose.py new file mode 100644 index 0000000000000000000000000000000000000000..ef0c1d008071fcd23e9d5f46644347998b4af589 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/compose.py @@ -0,0 +1,79 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from collections.abc import Sequence +from ..registry import PIPELINES +import traceback +from ...utils import build +from ...utils import get_logger + + +@PIPELINES.register() +class Compose(object): + """ + Composes several pipelines(include decode func, sample func, and transforms) together. + + Note: To deal with ```list``` type cfg temporaray, like: + + transform: + - Crop: # A list + attribute: 10 + - Resize: # A list + attribute: 20 + + every key of list will pass as the key name to build a module. + XXX: will be improved in the future. + + Args: + pipelines (list): List of transforms to compose. + Returns: + A compose object which is callable, __call__ for this Compose + object will call each given :attr:`transforms` sequencely. + """ + def __init__(self, pipelines): + #assert isinstance(pipelines, Sequence) + self.pipelines = [] + for p in pipelines.values(): + if isinstance(p, dict): + p = build(p, PIPELINES) + self.pipelines.append(p) + elif isinstance(p, list): + for t in p: + #XXX: to deal with old format cfg, ugly code here! + temp_dict = dict(name=list(t.keys())[0]) + for all_sub_t in t.values(): + if all_sub_t is not None: + temp_dict.update(all_sub_t) + + t = build(temp_dict, PIPELINES) + self.pipelines.append(t) + elif callable(p): + self.pipelines.append(p) + else: + raise TypeError('pipelines must be callable or a dict,' + 'but got {type(p)}') + def __call__(self, data): + """call""" + for p in self.pipelines: + try: + data = p(data) + except Exception as e: + stack_info = traceback.format_exc() + logger = get_logger("paddlevideo") + logger.info("fail to perform transform [{}] with error: " + "{} and stack:\n{}".format(p, e, str(stack_info))) + raise e + return data diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/decode.py b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/decode.py new file mode 100644 index 0000000000000000000000000000000000000000..b8e749aff48ce39e05c796d8ad539787fc92e4a2 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/decode.py @@ -0,0 +1,165 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import sys +from io import BytesIO +import os +import random + +import numpy as np +import pickle +import cv2 + +from ..registry import PIPELINES + + +@PIPELINES.register() +class VideoDecoder(object): + """ + Decode mp4 file to frames. + Args: + filepath: the file path of mp4 file + """ + def __init__(self): + pass + + def __call__(self, results): + """ + Perform mp4 decode operations. + return: + List where each item is a numpy array after decoder. + """ + #XXX get info from results!!! + file_path = results['filename'] + cap = cv2.VideoCapture(file_path) + videolen = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + sampledFrames = [] + for i in range(videolen): + ret, frame = cap.read() + # maybe first frame is empty + if ret == False: + continue + img = frame[:, :, ::-1] + sampledFrames.append(img) + results['frames'] = sampledFrames + results['frames_len'] = len(sampledFrames) + results['format'] = 'video' + return results + + +@PIPELINES.register() +class FrameDecoder(object): + """just parse results + """ + def __init__(self): + pass + + def __call__(self, results): + results['format'] = 'frame' + return results + + +@PIPELINES.register() +class FeatureDecoder(object): + """ + Perform feature decode operations.e.g.youtube8m + """ + def __init__(self, num_classes, max_len=512, has_label=True): + self.max_len = max_len + self.num_classes = num_classes + self.has_label = has_label + + def __call__(self, results): + """ + Perform feature decode operations. + return: + List where each item is a numpy array after decoder. + """ + #1. load pkl + #2. parse to rgb/audio/ + #3. padding + + filepath = results['filename'] + data = pickle.load(open(filepath, 'rb'), encoding='bytes') + + record = data + nframes = record[b'nframes'] + rgb = record[b'feature'].astype(float) + audio = record[b'audio'].astype(float) + if self.has_label: + label = record[b'label'] + one_hot_label = self.make_one_hot(label, self.num_classes) + + rgb = rgb[0:nframes, :] + audio = audio[0:nframes, :] + + rgb = self.dequantize(rgb, + max_quantized_value=2., + min_quantized_value=-2.) + audio = self.dequantize(audio, + max_quantized_value=2, + min_quantized_value=-2) + + if self.has_label: + results['labels'] = one_hot_label.astype("float32") + + feat_pad_list = [] + feat_len_list = [] + mask_list = [] + vitem = [rgb, audio] + for vi in range(2): #rgb and audio + if vi == 0: + prefix = "rgb_" + else: + prefix = "audio_" + feat = vitem[vi] + results[prefix + 'len'] = feat.shape[0] + #feat pad step 1. padding + feat_add = np.zeros((self.max_len - feat.shape[0], feat.shape[1]), + dtype=np.float32) + feat_pad = np.concatenate((feat, feat_add), axis=0) + results[prefix + 'data'] = feat_pad.astype("float32") + #feat pad step 2. mask + feat_mask_origin = np.ones(feat.shape, dtype=np.float32) + feat_mask_add = feat_add + feat_mask = np.concatenate((feat_mask_origin, feat_mask_add), + axis=0) + results[prefix + 'mask'] = feat_mask.astype("float32") + + return results + + def dequantize(self, + feat_vector, + max_quantized_value=2., + min_quantized_value=-2.): + """ + Dequantize the feature from the byte format to the float format + """ + + assert max_quantized_value > min_quantized_value + quantized_range = max_quantized_value - min_quantized_value + scalar = quantized_range / 255.0 + bias = (quantized_range / 512.0) + min_quantized_value + + return feat_vector * scalar + bias + + def make_one_hot(self, label, dim=3862): + """make one hot""" + one_hot_label = np.zeros(dim) + one_hot_label = one_hot_label.astype(float) + for ind in label: + one_hot_label[int(ind)] = 1 + return one_hot_label diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/mix.py b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/mix.py new file mode 100644 index 0000000000000000000000000000000000000000..fe7a8073bfdc322708d1773e7e4e15fe6c17088e --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/mix.py @@ -0,0 +1,91 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import random +import numpy as np +from ..registry import PIPELINES + + +@PIPELINES.register() +class Mixup(object): + """ + Mixup operator. + Args: + alpha(float): alpha value. + """ + def __init__(self, alpha=0.2): + assert alpha > 0., \ + 'parameter alpha[%f] should > 0.0' % (alpha) + self.alpha = alpha + + def __call__(self, batch): + imgs, labels = list(zip(*batch)) + imgs = np.array(imgs) + labels = np.array(labels) + bs = len(batch) + idx = np.random.permutation(bs) + lam = np.random.beta(self.alpha, self.alpha) + lams = np.array([lam] * bs, dtype=np.float32) + imgs = lam * imgs + (1 - lam) * imgs[idx] + return list(zip(imgs, labels, labels[idx], lams)) + + +@PIPELINES.register() +class Cutmix(object): + """ Cutmix operator + Args: + alpha(float): alpha value. + """ + def __init__(self, alpha=0.2): + assert alpha > 0., \ + 'parameter alpha[%f] should > 0.0' % (alpha) + self.alpha = alpha + + def rand_bbox(self, size, lam): + """ rand_bbox """ + w = size[2] + h = size[3] + cut_rat = np.sqrt(1. - lam) + cut_w = np.int(w * cut_rat) + cut_h = np.int(h * cut_rat) + + # uniform + cx = np.random.randint(w) + cy = np.random.randint(h) + + bbx1 = np.clip(cx - cut_w // 2, 0, w) + bby1 = np.clip(cy - cut_h // 2, 0, h) + bbx2 = np.clip(cx + cut_w // 2, 0, w) + bby2 = np.clip(cy + cut_h // 2, 0, h) + + return bbx1, bby1, bbx2, bby2 + + def __call__(self, batch): + imgs, labels = list(zip(*batch)) + imgs = np.array(imgs) + labels = np.array(labels) + + bs = len(batch) + idx = np.random.permutation(bs) + lam = np.random.beta(self.alpha, self.alpha) + + bbx1, bby1, bbx2, bby2 = self.rand_bbox(imgs.shape, lam) + imgs[:, :, bbx1:bbx2, bby1:bby2] = imgs[idx, :, bbx1:bbx2, bby1:bby2] + lam = 1 - (float(bbx2 - bbx1) * (bby2 - bby1) / + (imgs.shape[-2] * imgs.shape[-1])) + lams = np.array([lam] * bs, dtype=np.float32) + + return list(zip(imgs, labels, labels[idx], lams)) diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/sample.py b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/sample.py new file mode 100644 index 0000000000000000000000000000000000000000..6990f9eb02bd9683141a657c324418708e377a32 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/pipelines/sample.py @@ -0,0 +1,102 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +import random +from PIL import Image +from ..registry import PIPELINES +import os +import numpy as np + +@PIPELINES.register() +class Sampler(object): + """ + Sample frames id. + NOTE: Use PIL to read image here, has diff with CV2 + Args: + num_seg(int): number of segments. + seg_len(int): number of sampled frames in each segment. + mode(str): 'train', 'valid' + Returns: + frames_idx: the index of sampled #frames. + """ + + def __init__(self, num_seg, seg_len, valid_mode=False): + self.num_seg = num_seg + self.seg_len = seg_len + self.valid_mode = valid_mode + + + def _get(self, frames_idx, results): + data_format =results['format'] + + if data_format == "frame": + frame_dir = results['frame_dir'] + imgs = [] + for idx in frames_idx: + img = Image.open(os.path.join(frame_dir, results['suffix'].format(idx))).convert('RGB') + imgs.append(img) + + elif data_format == "video": + + frames = np.array(results['frames']) + imgs = [] + for idx in frames_idx: + imgbuf = frames[idx] + img = Image.fromarray(imgbuf, mode='RGB') + imgs.append(img) + else: + raise NotImplementedError + results['imgs'] = imgs + return results + + + def __call__(self, results): + """ + Args: + frames_len: length of frames. + return: + sampling id. + """ + frames_len = int(results['frames_len']) + average_dur = int(int(frames_len) / self.num_seg) + frames_idx = [] + for i in range(self.num_seg): + idx = 0 + if not self.valid_mode: + if average_dur >= self.seg_len: + idx = random.randint(0, average_dur - self.seg_len) + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: # average_dur = 0 + idx = i % frames_len + else: + if average_dur >= self.seg_len: + idx = (average_dur - 1) // 2 + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i % frames_len + for jj in range(idx, idx+self.seg_len): + if results['format'] == 'video': + frames_idx.append(int(jj%frames_len)) + elif results['format'] == 'frame': + #frame from 000001 + frames_idx.append(jj+1) + else: + raise NotImplementedError + + return self._get(frames_idx, results) diff --git a/applications/VideoQualityAssessment/paddlevideo/loader/registry.py b/applications/VideoQualityAssessment/paddlevideo/loader/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..88948acd0fad48c7626429c285db79af67c7e28a --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/loader/registry.py @@ -0,0 +1,20 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from ..utils import Registry + +PIPELINES = Registry("pipeline") +DATASETS = Registry("datasets") diff --git a/applications/VideoQualityAssessment/paddlevideo/metrics/__init__.py b/applications/VideoQualityAssessment/paddlevideo/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ee41ae8608e01d2fa15992993bc4e23cff7448cf --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/metrics/__init__.py @@ -0,0 +1,23 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .registry import METRIC +from .build import build_metric +from .quality_metric import QuqlityMetric + +__all__ = [ + 'METRIC', 'build_metric', 'QuqlityMetric' +] diff --git a/applications/VideoQualityAssessment/paddlevideo/metrics/base.py b/applications/VideoQualityAssessment/paddlevideo/metrics/base.py new file mode 100644 index 0000000000000000000000000000000000000000..b6e41bbceb3280ac621916e865efb9be19a1c113 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/metrics/base.py @@ -0,0 +1,39 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +from abc import abstractmethod +import numpy as np +import paddle +from paddlevideo.utils import get_dist_info + +from .registry import METRIC + + +class BaseMetric(object): + """Base Metric""" + def __init__(self, data_size, batch_size, log_interval=1, **kwargs): + self.data_size = data_size + self.batch_size = batch_size + _, self.world_size = get_dist_info() + self.log_interval = log_interval + + @abstractmethod + def update(self): + """update""" + raise NotImplementedError + + @abstractmethod + def accumulate(self): + """accumulate""" + raise NotImplementedError diff --git a/applications/VideoQualityAssessment/paddlevideo/metrics/build.py b/applications/VideoQualityAssessment/paddlevideo/metrics/build.py new file mode 100644 index 0000000000000000000000000000000000000000..852fe1514215c09c716a96450237f5b0a12b60c6 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/metrics/build.py @@ -0,0 +1,23 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .registry import METRIC +from ..utils import build + + +def build_metric(cfg): + """build metric""" + return build(cfg, METRIC) diff --git a/applications/VideoQualityAssessment/paddlevideo/metrics/quality_metric.py b/applications/VideoQualityAssessment/paddlevideo/metrics/quality_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..a4c50ad15d00d0a906e2da4547a42ff2d83cd51f --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/metrics/quality_metric.py @@ -0,0 +1,73 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import numpy as np +import paddle +from paddle.hapi.model import _all_gather + +from scipy import stats + +from .registry import METRIC +from .base import BaseMetric +from paddlevideo.utils import get_logger +logger = get_logger("paddlevideo") + + +@METRIC.register +class QuqlityMetric(BaseMetric): + """CenterCropQualityMetric""" + def __init__(self, data_size, batch_size, log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.output = [] + self.label = [] + self.y_pred = np.zeros(data_size) + self.y_test = np.zeros(data_size) + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + labels = data[1] + + predict_output = paddle.tolist(outputs) + predict_label = paddle.tolist(labels) + predict_output_len = len(predict_output) + for i in range(predict_output_len): + self.output.append(predict_output[i][0]) + self.label.append(predict_label[i][0]) + + if batch_id % self.log_interval == 0: + logger.info("[TEST] Processing batch {}/{} ...".format( + batch_id, + self.data_size // (self.batch_size * self.world_size))) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + test_output_np = np.array(self.output) + test_label_np = np.array(self.label) + PLCC = stats.pearsonr(test_output_np, test_label_np)[0] + SROCC = stats.spearmanr(test_output_np, test_label_np)[0] + + logger.info('[TEST] finished, PLCC= {}, SROCC= {} '.format(PLCC, SROCC)) + + def accumulate_train(self, output, label): + """accumulate_train""" + output_np = np.array(output) + label_np = np.array(label) + PLCC = stats.pearsonr(output_np, label_np)[0] + SROCC = stats.spearmanr(output_np, label_np)[0] + return PLCC, SROCC + diff --git a/applications/VideoQualityAssessment/paddlevideo/metrics/registry.py b/applications/VideoQualityAssessment/paddlevideo/metrics/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..24c74262e0f0dd37e793954ae5be4e18a4a75024 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/metrics/registry.py @@ -0,0 +1,19 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from ..utils import Registry + +METRIC = Registry('metric') diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/__init__.py b/applications/VideoQualityAssessment/paddlevideo/modeling/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a9c4e1a063ac6813d9a21536ab86971c11793adb --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/__init__.py @@ -0,0 +1,45 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .backbones import ResNet +from .builder import (build_backbone, build_head, build_recognizer, + build_localizer, build_loss) +from .heads import BaseHead, TSNHead, TSMRecHead +from .losses import SmoothL1Loss, L1Loss +from .framework.recognizers import BaseRecognizer, recognizer2d +from .registry import BACKBONES, HEADS, LOSSES, RECOGNIZERS, LOCALIZERS +from .weight_init import weight_init_ + +__all__ = [ + 'BACKBONES', + 'HEADS', + 'RECOGNIZERS', + 'LOCALIZERS', + 'LOSSES', + 'build_recognizer', + 'build_localizer', + 'build_head', + 'build_backbone', + 'build_loss', + 'ResNet', + 'TSNHead', + 'BaseHead', + 'TSMRecHead', + 'BaseRecognizer', + 'Recognizer2d', + 'SmoothL1Loss', + 'L1Loss', +] diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/__init__.py b/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..aa9e591c87b19c99e741a90efeac47a075aef034 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/__init__.py @@ -0,0 +1,20 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .resnet import ResNet +from .resnet_tweaks_tsm import ResNetTweaksTSM + +__all__ = ['ResNet', 'ResNetTweaksTSM'] diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/resnet.py b/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/resnet.py new file mode 100644 index 0000000000000000000000000000000000000000..a03b38c4b9d1ea662c963af21701c4d1fb7ec92e --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/resnet.py @@ -0,0 +1,290 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import numpy as np +import math + +import paddle +import paddle.nn as nn +from paddle.nn import (Conv2D, BatchNorm2D, Linear, Dropout, MaxPool2D, + AvgPool2D) +from paddle import ParamAttr +import paddle.nn.functional as F + +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from ...utils import load_ckpt + + +class ConvBNLayer(nn.Layer): + """Conv2D and BatchNorm2D layer. + + Args: + in_channels (int): Number of channels for the input. + out_channels (int): Number of channels for the output. + kernel_size (int): Kernel size. + stride (int): Stride in the Conv2D layer. Default: 1. + groups (int): Groups in the Conv2D, Default: 1. + act (str): Indicate activation after BatchNorm2D layer. + name (str): the name of an instance of ConvBNLayer. + + Note: weight and bias initialization include initialize values and name the restored parameters, values initialization are explicit declared in the ```init_weights``` method. + + """ + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + act=None, + name=None): + super(ConvBNLayer, self).__init__() + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + weight_attr=ParamAttr(name=name + "_weights"), + bias_attr=False) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + + self._act = act + + self._batch_norm = BatchNorm2D(out_channels, + weight_attr=ParamAttr(name=bn_name + + "_scale"), + bias_attr=ParamAttr(bn_name + "_offset")) + + def forward(self, inputs): + """forward""" + y = self._conv(inputs) + y = self._batch_norm(y) + if self._act: + y = getattr(paddle.nn.functional, self._act)(y) + return y + + +class BottleneckBlock(nn.Layer): + """BottleneckBlock""" + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + name=None): + super(BottleneckBlock, self).__init__() + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + act="relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act="relu", + name=name + "_branch2b") + + self.conv2 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels * 4, + kernel_size=1, + act=None, + name=name + "_branch2c") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels * 4, + kernel_size=1, + stride=stride, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + """forward""" + y = self.conv0(inputs) + conv1 = self.conv1(y) + conv2 = self.conv2(conv1) + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv2) + return F.relu(y) + + +class BasicBlock(nn.Layer): + """BasicBlock""" + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + name=None): + super(BasicBlock, self).__init__() + self.stride = stride + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act="relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + act=None, + name=name + "_branch2b") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=stride, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + """forward""" + y = self.conv0(inputs) + conv1 = self.conv1(y) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(short, conv1) + y = F.relu(y) + return y + + +@BACKBONES.register() +class ResNet(nn.Layer): + """ResNet backbone. + + Args: + depth (int): Depth of resnet model. + pretrained (str): pretrained model. Default: None. + """ + def __init__(self, depth, pretrained=None): + super(ResNet, self).__init__() + self.pretrained = pretrained + self.layers = depth + + supported_layers = [18, 34, 50, 101, 152] + assert self.layers in supported_layers, \ + "supported layers are {} but input layer is {}".format( + supported_layers, self.layers) + + if self.layers == 18: + depth = [2, 2, 2, 2] + elif self.layers == 34 or self.layers == 50: + depth = [3, 4, 6, 3] + elif self.layers == 101: + depth = [3, 4, 23, 3] + elif self.layers == 152: + depth = [3, 8, 36, 3] + + in_channels = [64, 256, 512, 1024] + out_channels = [64, 128, 256, 512] + + self.conv = ConvBNLayer(in_channels=3, + out_channels=64, + kernel_size=7, + stride=2, + act="relu", + name="conv1") + self.pool2D_max = MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.block_list = [] + if self.layers >= 50: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + if self.layers in [101, 152] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + bottleneck_block = self.add_sublayer( + conv_name, + BottleneckBlock( + # NOTE: Be careful! Here is different from TSM model. + in_channels=in_channels[block] + if i == 0 else out_channels[block] * 4, + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + name=conv_name)) + + self.block_list.append(bottleneck_block) + shortcut = True + else: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + conv_name = "res" + str(block + 2) + chr(97 + i) + basic_block = self.add_sublayer( + conv_name, + BasicBlock(in_channels=in_channels[block] + if i == 0 else out_channels[block], + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + name=conv_name)) + self.block_list.append(basic_block) + shortcut = True + + def init_weights(self): + """Initiate the parameters. + Note: + 1. when indicate pretrained loading path, will load it to initiate backbone. + 2. when not indicating pretrained loading path, will follow specific initialization initiate backbone. Always, Conv2D layer will be initiated by KaimingNormal function, and BatchNorm2d will be initiated by Constant function. + Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/kaiming/KaimingNormal_en.html + """ + #XXX: check bias!!! check pretrained!!! + + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + #XXX: no bias + weight_init_(layer, 'KaimingNormal') + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Constant', value=1) + + def forward(self, inputs): + """Define how the backbone is going to run. + + """ + #NOTE: Already merge axis 0(batches) and axis 1(channels) before extracting feature phase, + # please refer to paddlevideo/modeling/framework/recognizers/recognizer2d.py#L27 + #y = paddle.reshape( + # inputs, [-1, inputs.shape[2], inputs.shape[3], inputs.shape[4]]) + + y = self.conv(inputs) + y = self.pool2D_max(y) + for block in self.block_list: + y = block(y) + return y diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/resnet_tweaks_tsm.py b/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/resnet_tweaks_tsm.py new file mode 100644 index 0000000000000000000000000000000000000000..25f18538f96d3ab6db19f9797454d4c99afc8da7 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/backbones/resnet_tweaks_tsm.py @@ -0,0 +1,329 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import numpy as np +import math + +import sys +import paddle +import paddle.nn as nn +from paddle.nn import (Conv2D, BatchNorm2D, Linear, Dropout, MaxPool2D, + AvgPool2D) +from paddle import ParamAttr +import paddle.nn.functional as F + +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from ...utils.save_load import load_ckpt + + +class ConvBNLayer(nn.Layer): + """Conv2D and BatchNorm2D layer. + + Args: + in_channels (int): Number of channels for the input. + out_channels (int): Number of channels for the output. + kernel_size (int): Kernel size. + stride (int): Stride in the Conv2D layer. Default: 1. + groups (int): Groups in the Conv2D, Default: 1. + is_tweaks_mode (bool): switch for tweaks. Default: False. + act (str): Indicate activation after BatchNorm2D layer. + name (str): the name of an instance of ConvBNLayer. + + Note: weight and bias initialization include initialize values and name the restored parameters, values initialization are explicit declared in the ```init_weights``` method. + + """ + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + is_tweaks_mode=False, + act=None, + name=None): + super(ConvBNLayer, self).__init__() + self.is_tweaks_mode = is_tweaks_mode + self._pool2d_avg = AvgPool2D(kernel_size=2, + stride=2, + padding=0, + ceil_mode=True) + + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + weight_attr=ParamAttr(name=name + "_weights"), + bias_attr=False) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + + self._act = act + + self._batch_norm = BatchNorm2D(out_channels, + weight_attr=ParamAttr(name=bn_name + + "_scale"), + bias_attr=ParamAttr(bn_name + "_offset")) + + def forward(self, inputs): + """forward""" + if self.is_tweaks_mode: + inputs = self._pool2d_avg(inputs) + y = self._conv(inputs) + y = self._batch_norm(y) + if self._act: + y = getattr(paddle.nn.functional, self._act)(y) + return y + + +class BottleneckBlock(nn.Layer): + """BottleneckBlock""" + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + if_first=False, + num_seg=8, + name=None): + super(BottleneckBlock, self).__init__() + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + act="relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act="relu", + name=name + "_branch2b") + + self.conv2 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels * 4, + kernel_size=1, + act=None, + name=name + "_branch2c") + + if not shortcut: + self.short = ConvBNLayer( + in_channels=in_channels, + out_channels=out_channels * 4, + kernel_size=1, + stride= + 1, #ResNet-D 2/2:add a 2×2 average pooling layer with a stride of 2 before the convolution, + # whose stride is changed to 1, works well in practice. + is_tweaks_mode=False if if_first else True, + name=name + "_branch1") + + self.shortcut = shortcut + self.num_seg = num_seg + + def forward(self, inputs): + """forward""" + shifts = paddle.fluid.layers.temporal_shift(inputs, self.num_seg, + 1.0 / self.num_seg) + y = self.conv0(shifts) + conv1 = self.conv1(y) + conv2 = self.conv2(conv1) + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv2) + return F.relu(y) + + +class BasicBlock(nn.Layer): + """BasicBlock""" + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + name=None): + super(BasicBlock, self).__init__() + self.stride = stride + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act="relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + act=None, + name=name + "_branch2b") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=stride, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + """forward""" + y = self.conv0(inputs) + conv1 = self.conv1(y) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(short, conv1) + y = F.relu(y) + return y + + +@BACKBONES.register() +class ResNetTweaksTSM(nn.Layer): + """ResNet TSM backbone. + + Args: + depth (int): Depth of resnet model. + pretrained (str): pretrained model. Default: None. + """ + def __init__(self, depth, num_seg=8, pretrained=None): + super(ResNetTweaksTSM, self).__init__() + self.pretrained = pretrained + self.layers = depth + self.num_seg = num_seg + + supported_layers = [18, 34, 50, 101, 152] + assert self.layers in supported_layers, \ + "supported layers are {} but input layer is {}".format( + supported_layers, self.layers) + + if self.layers == 18: + depth = [2, 2, 2, 2] + elif self.layers == 34 or self.layers == 50: + depth = [3, 4, 6, 3] + elif self.layers == 101: + depth = [3, 4, 23, 3] + elif self.layers == 152: + depth = [3, 8, 36, 3] + + in_channels = 64 + out_channels = [64, 128, 256, 512] + + #ResNet-C: use three 3x3 conv, replace, one 7x7 conv + self.conv1_1 = ConvBNLayer(in_channels=3, + out_channels=32, + kernel_size=3, + stride=2, + act='relu', + name="conv1_1") + self.conv1_2 = ConvBNLayer(in_channels=32, + out_channels=32, + kernel_size=3, + stride=1, + act='relu', + name="conv1_2") + self.conv1_3 = ConvBNLayer(in_channels=32, + out_channels=64, + kernel_size=3, + stride=1, + act='relu', + name="conv1_3") + self.pool2D_max = MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.block_list = [] + if self.layers >= 50: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + if self.layers in [101, 152] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + bottleneck_block = self.add_sublayer( + 'bb_%d_%d' % + (block, i), #same with PaddleClas, for loading pretrain + BottleneckBlock( + in_channels=in_channels + if i == 0 else out_channels[block] * 4, + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + num_seg=self.num_seg, + shortcut=shortcut, + if_first=block == i == 0, + name=conv_name)) + in_channels = out_channels[block] * 4 + self.block_list.append(bottleneck_block) + shortcut = True + else: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + conv_name = "res" + str(block + 2) + chr(97 + i) + basic_block = self.add_sublayer( + conv_name, + BasicBlock(in_channels=in_channels[block] + if i == 0 else out_channels[block], + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + name=conv_name)) + self.block_list.append(basic_block) + shortcut = True + + def init_weights(self): + """Initiate the parameters. + Note: + 1. when indicate pretrained loading path, will load it to initiate backbone. + 2. when not indicating pretrained loading path, will follow specific initialization initiate backbone. Always, Conv2D layer will be initiated by KaimingNormal function, and BatchNorm2d will be initiated by Constant function. + Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/kaiming/KaimingNormal_en.html + """ + #XXX: check bias!!! check pretrained!!! + + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + #XXX: no bias + weight_init_(layer, 'KaimingNormal') + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Constant', value=1) + + def forward(self, inputs): + """Define how the backbone is going to run. + + """ + #NOTE: Already merge axis 0(batches) and axis 1(channels) before extracting feature phase, + # please refer to paddlevideo/modeling/framework/recognizers/recognizer2d.py#L27 + #y = paddle.reshape( + # inputs, [-1, inputs.shape[2], inputs.shape[3], inputs.shape[4]]) + ####ResNet-C: use three 3x3 conv, replace, one 7x7 conv + y = self.conv1_1(inputs) + y = self.conv1_2(y) + y = self.conv1_3(y) + + y = self.pool2D_max(y) + for block in self.block_list: + y = block(y) + return y diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/builder.py b/applications/VideoQualityAssessment/paddlevideo/modeling/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..3007a5d56746b4653fd68a276a808dd423aa7e97 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/builder.py @@ -0,0 +1,52 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .registry import BACKBONES, HEADS, LOSSES, RECOGNIZERS, LOCALIZERS +from ..utils import build + + +def build_backbone(cfg): + """Build backbone.""" + return build(cfg, BACKBONES) + + +def build_head(cfg): + """Build head.""" + return build(cfg, HEADS) + + +def build_loss(cfg): + """Build loss.""" + return build(cfg, LOSSES) + + +def build_recognizer(cfg): + """Build recognizer.""" + return build(cfg, RECOGNIZERS, key='framework') + + +def build_localizer(cfg): + """Build localizer.""" + return build(cfg, LOCALIZERS, key='framework') + + +def build_model(cfg): + cfg_copy = cfg.copy() + framework_type = cfg_copy.get('framework') + if framework_type in RECOGNIZERS: + return build_recognizer(cfg) + elif framework_type in LOCALIZERS: + return build_localizer(cfg) + else: + raise NotImplementedError diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/framework/__init__.py b/applications/VideoQualityAssessment/paddlevideo/modeling/framework/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f7977905ef3261af3178dc4253b367bbdfa21c5d --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/framework/__init__.py @@ -0,0 +1,22 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .recognizers import BaseRecognizer, Recognizer2D + +__all__ = [ + 'BaseRecognizer', + 'Recognizer2D', +] diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/__init__.py b/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..95e4b1fb2599a9704df2b826acd096ebb78657ad --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/__init__.py @@ -0,0 +1,19 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +from .base import BaseRecognizer +from .recognizer2d import Recognizer2D + + +__all__ = ['BaseRecognizer', 'Recognizer2D'] diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/base.py b/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/base.py new file mode 100644 index 0000000000000000000000000000000000000000..aa13090de67dddf26894ac83904b401105c2a9a0 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/base.py @@ -0,0 +1,97 @@ +""" +start +""" + +from abc import abstractmethod +from ... import builder +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + + +class BaseRecognizer(nn.Layer): + """Base class for recognizers. + + All recognizers should subclass it. + All subclass should overwrite: + + - Methods:``train_step``, supporting to forward when training. + - Methods:``valid_step``, supporting to forward when validating. + - Methods:``test_step``, supporting to forward when testing. + + Args: + backbone (dict): Backbone modules to extract feature. + head (dict): Classification head to process feature. + + """ + def __init__(self, backbone=None, head=None): + + super().__init__() + if backbone is not None: + self.backbone = builder.build_backbone(backbone) + self.backbone.init_weights() + else: + self.backbone = None + if head is not None: + self.head_name = head.name + self.head = builder.build_head(head) + self.head.init_weights() + else: + self.head = None + + + def init_weights(self): + """Initialize the model network weights. """ + + self.backbone.init_weights( + ) #TODO: required? while backbone without base class + self.head.init_weights() + + def extract_feature(self, imgs): + """Extract features through a backbone. + + Args: + imgs (paddle.Tensor) : The input images. + + Returns: + feature (paddle.Tensor) : The extracted features. + """ + feature = self.backbone(imgs) + return feature + + def forward(self, imgs, **kwargs): + """Define how the model is going to run, from input to output. + """ + batches = imgs.shape[0] + num_segs = imgs.shape[1] + imgs = paddle.reshape(imgs, [-1] + list(imgs.shape[2:])) + + if self.backbone is not None: + feature = self.extract_feature(imgs) + else: + feature = imgs + if self.head is not None: + cls_score = self.head(feature, num_segs) + else: + cls_score = None + + + return cls_score + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating step. + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Test step. + """ + raise NotImplementedError diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/recognizer2d.py b/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/recognizer2d.py new file mode 100644 index 0000000000000000000000000000000000000000..c33f2164059bd5f79b18310268e260a617bfc22f --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/framework/recognizers/recognizer2d.py @@ -0,0 +1,52 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer +import paddle +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class Recognizer2D(BaseRecognizer): + """2D recognizer model framework.""" + def train_step(self, data_batch): + """Define how the model is going to train, from input to output. + """ + #NOTE: As the num_segs is an attribute of dataset phase, and didn't pass to build_head phase, should obtain it from imgs(paddle.Tensor) now, then call self.head method. + + #labels = labels.squeeze() + #XXX: unsqueeze label to [label] ? + + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self(imgs) + loss_metrics = self.head.loss(cls_score, labels) + return loss_metrics + + def val_step(self, data_batch): + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self(imgs) + loss_metrics = self.head.loss(cls_score, labels, valid_mode=True) + return loss_metrics + + def test_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + #NOTE: (shipping) when testing, the net won't call head.loss, we deal with the test processing in /paddlevideo/metrics + imgs = data_batch[0] + cls_score = self(imgs) + return cls_score diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/heads/__init__.py b/applications/VideoQualityAssessment/paddlevideo/modeling/heads/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..011c71e03f9e724f3e5ebacfd18ec2cd9296b0ea --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/heads/__init__.py @@ -0,0 +1,21 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .base import BaseHead +from .tsn_head import TSNHead +from .tsm_rec_head import TSMRecHead + +__all__ = ['BaseHead', 'TSNHead', 'TSMRecHead'] diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/heads/base.py b/applications/VideoQualityAssessment/paddlevideo/modeling/heads/base.py new file mode 100644 index 0000000000000000000000000000000000000000..379cccb9cdc782733ed97c693e1a937723e2766d --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/heads/base.py @@ -0,0 +1,143 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import numpy as np +from abc import abstractmethod + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from ..builder import build_loss +from paddlevideo.utils import get_logger, get_dist_info + +logger = get_logger("paddlevideo") + + +class BaseHead(nn.Layer): + """Base class for head part. + + All head should subclass it. + All subclass should overwrite: + + - Methods: ```init_weights```, initializing weights. + - Methods: ```forward```, forward function. + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channels in input feature. + loss_cfg (dict): Config for building loss. Default: dict(type='CrossEntropyLoss'). + ls_eps (float): label smoothing epsilon. Default: 0. . + + """ + def __init__( + self, + num_classes, + in_channels, + loss_cfg=dict( + name="CrossEntropyLoss" + ), #TODO(shipping): only pass a name or standard build cfg format. + #multi_class=False, NOTE(shipping): not supported now. + ls_eps=0.): + + super().__init__() + self.num_classes = num_classes + self.in_channels = in_channels + self.loss_func = build_loss(loss_cfg) + #self.multi_class = multi_class NOTE(shipping): not supported now + self.ls_eps = ls_eps + + @abstractmethod + def init_weights(self): + """Initiate the parameters. + """ + raise NotImplementedError + + @abstractmethod + def forward(self, x): + """Define how the head is going to run. + """ + raise NotImplementedError + + def loss(self, scores, labels, valid_mode=False, **kwargs): + """Calculate the loss accroding to the model output ```scores```, + and the target ```labels```. + + Args: + scores (paddle.Tensor): The output of the model. + labels (paddle.Tensor): The target output of the model. + + Returns: + losses (dict): A dict containing field 'loss'(mandatory) and 'top1_acc', 'top5_acc'(optional). + + """ + if len(labels) == 1: #commonly case + labels = labels[0] + losses = dict() + if self.ls_eps != 0. and not valid_mode: # label_smooth + loss = self.label_smooth_loss(scores, labels, **kwargs) + else: + loss = self.loss_func(scores, labels, **kwargs) + + top1, top5 = self.get_acc(scores, labels, valid_mode) + losses['top1'] = top1 + losses['top5'] = top5 + losses['loss'] = loss + return losses + elif len(labels) == 3: # mix_up + labels_a, labels_b, lam = labels + lam = lam[0] # get lam value + losses = dict() + + if self.ls_eps != 0: + loss_a = self.label_smooth_loss(scores, labels_a, **kwargs) + loss_b = self.label_smooth_loss(scores, labels_b, **kwargs) + else: + loss_a = self.loss_func(scores, labels_a, **kwargs) + loss_b = self.loss_func(scores, labels_a, **kwargs) + loss = lam * loss_a + (1 - lam) * loss_b + top1a, top5a = self.get_acc(scores, labels_a, valid_mode) + top1b, top5b = self.get_acc(scores, labels_b, valid_mode) + top1 = lam * top1a + (1 - lam) * top1b + top5 = lam * top5a + (1 - lam) * top5b + losses['top1'] = top1 + losses['top5'] = top5 + losses['loss'] = loss + return losses + else: + raise NotImplementedError + + def label_smooth_loss(self, scores, labels, **kwargs): + """label smooth loss""" + labels = F.one_hot(labels, self.num_classes) + labels = F.label_smooth(labels, epsilon=self.ls_eps) + labels = paddle.squeeze(labels, axis=1) + loss = self.loss_func(scores, labels, soft_label=True, **kwargs) + return loss + + def get_acc(self, scores, labels, valid_mode): + """get acc""" + top1 = paddle.metric.accuracy(input=scores, label=labels, k=1) + top5 = paddle.metric.accuracy(input=scores, label=labels, k=5) + _, world_size = get_dist_info() + #NOTE(shipping): deal with multi cards validate + if world_size > 1 and valid_mode: #reduce sum when valid + top1 = paddle.distributed.all_reduce( + top1, op=paddle.distributed.ReduceOp.SUM) / world_size + top5 = paddle.distributed.all_reduce( + top5, op=paddle.distributed.ReduceOp.SUM) / world_size + + return top1, top5 diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/heads/tsm_rec_head.py b/applications/VideoQualityAssessment/paddlevideo/modeling/heads/tsm_rec_head.py new file mode 100644 index 0000000000000000000000000000000000000000..ae08693b50e0d77a15fcf72da4e2d33142e4e4c0 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/heads/tsm_rec_head.py @@ -0,0 +1,153 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +import math +import paddle +import paddle.nn.functional as F +from paddle.nn import AdaptiveAvgPool2D, Linear, Dropout +from .base import BaseHead +from .tsn_head import TSNHead +from ..registry import HEADS + +from ..weight_init import weight_init_ + + +@HEADS.register() +class TSMRecHead(TSNHead): + """ TSM Rec Head + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + drop_ratio(float): drop ratio. Default: 0.8. + std(float): Std(Scale) value in normal initilizar. Default: 0.001. + kwargs (dict, optional): Any keyword argument to initialize. + """ + def __init__(self, + num_classes, + in_channels, + loss_cfg=dict(name='L1Loss'), + drop_ratio=0.8, + std=0.01, + data_format="NCHW", + **kwargs): + + super().__init__(num_classes, + in_channels, + loss_cfg, + drop_ratio=drop_ratio, + std=std, + data_format=data_format, + **kwargs) + + self.stdv = 1.0 / math.sqrt(self.in_channels * 1.0) + + def init_weights(self): + """Initiate the FC layer parameters""" + + weight_init_(self.fc, + 'Uniform', + 'fc_0.w_0', + 'fc_0.b_0', + low=-self.stdv, + high=self.stdv) + self.fc.bias.learning_rate = 2.0 + self.fc.bias.regularizer = paddle.regularizer.L2Decay(0.) + + def forward(self, x, num_seg): + """Define how the head is going to run. + + Args: + x (paddle.Tensor): The input data. + num_segs (int): Number of segments. + Returns: + score: (paddle.Tensor) The classification scores for input samples. + """ + # [N * num_segs, in_channels, 7, 7] + x = self.avgpool2d(x) + # [N * num_segs, in_channels, 1, 1] + if self.dropout is not None: + x = self.dropout(x) + # [N * num_seg, in_channels, 1, 1] + x = paddle.reshape(x, [-1, num_seg, x.shape[1]]) + # [N, num_seg, in_channels] + x = paddle.mean(x, axis=1) + # [N, 1, in_channels] + x = paddle.reshape(x, shape=[-1, self.in_channels]) + # [N, in_channels] + score = self.fc(x) + # [N, num_class] + #m = paddle.nn.Sigmoid() + #score = m(score) + return score + + def loss(self, scores, labels, valid_mode=False, **kwargs): + """Calculate the loss accroding to the model output ```scores```, + and the target ```labels```. + + Args: + scores (paddle.Tensor): The output of the model. + labels (paddle.Tensor): The target output of the model. + + Returns: + losses (dict): A dict containing field 'loss'(mandatory). + + """ + if len(labels) == 1: #commonly case + output = [] + label = [] + labels = labels[0] + losses = dict() + loss = self.loss_func(scores, labels, **kwargs) + + score_list = paddle.tolist(scores) + label_list = paddle.tolist(labels) + score_list_len = len(score_list) + for i in range(score_list_len): + output.append(score_list[i][0]) + label.append(label_list[i][0]) + losses['loss'] = loss + losses['output'] = output + losses['label'] = label + return losses + elif len(labels) == 3: + labels_a, labels_b, lam = labels + labels_a = paddle.cast(labels_a, dtype='float32') + labels_b = paddle.cast(labels_b, dtype='float32') + lam = lam[0] # get lam value + losses = dict() + + if self.ls_eps != 0: + loss_a = self.label_smooth_loss(scores, labels_a, **kwargs) + loss_b = self.label_smooth_loss(scores, labels_b, **kwargs) + else: + loss_a = self.loss_func(scores, labels_a, **kwargs) + loss_b = self.loss_func(scores, labels_a, **kwargs) + loss = lam * loss_a + (1 - lam) * loss_b + + losses['loss'] = loss + losses['output'] = output + losses['label'] = label + return losses + else: + raise NotImplementedError + + def label_smooth_loss(self, scores, labels, **kwargs): + """label smooth loss""" + labels = F.label_smooth(labels, epsilon=self.ls_eps) + labels = paddle.squeeze(labels, axis=1) + loss = self.loss_func(scores, labels, **kwargs) + return loss diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/heads/tsn_head.py b/applications/VideoQualityAssessment/paddlevideo/modeling/heads/tsn_head.py new file mode 100644 index 0000000000000000000000000000000000000000..7ca1a43dccd9bc0c59bb36dec761be82195d2d01 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/heads/tsn_head.py @@ -0,0 +1,97 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from paddle.nn import AdaptiveAvgPool2D, Linear, Dropout + +from .base import BaseHead +from ..registry import HEADS +from ..weight_init import weight_init_ +import paddle.nn.functional as F + + +@HEADS.register() +class TSNHead(BaseHead): + """TSN Head. + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + drop_ratio(float): drop ratio. Default: 0.4. + std(float): Std(Scale) value in normal initilizar. Default: 0.01. + kwargs (dict, optional): Any keyword argument to initialize. + + """ + def __init__(self, + num_classes, + in_channels, + loss_cfg=dict(name='CrossEntropyLoss'), + drop_ratio=0.4, + std=0.01, + data_format="NCHW", + **kwargs): + + super().__init__(num_classes, in_channels, loss_cfg, **kwargs) + self.drop_ratio = drop_ratio + self.std = std + + #NOTE: global pool performance + self.avgpool2d = AdaptiveAvgPool2D((1, 1), data_format=data_format) + + if self.drop_ratio != 0: + self.dropout = Dropout(p=self.drop_ratio) + else: + self.dropout = None + + self.fc = Linear(self.in_channels, self.num_classes) + + def init_weights(self): + """Initiate the FC layer parameters""" + + weight_init_(self.fc, + 'Normal', + 'fc_0.w_0', + 'fc_0.b_0', + mean=0., + std=self.std) + + def forward(self, x, num_seg): + """Define how the head is going to run. + + Args: + x (paddle.Tensor): The input data. + num_segs (int): Number of segments. + Returns: + score: (paddle.Tensor) The classification scores for input samples. + """ + + #XXX: check dropout location! + + # [N * num_segs, in_channels, 7, 7] + x = self.avgpool2d(x) + # [N * num_segs, in_channels, 1, 1] + if self.dropout is not None: + x = self.dropout(x) + # [N * num_seg, in_channels, 1, 1] + x = paddle.reshape(x, [-1, num_seg, x.shape[1]]) + # [N, num_seg, in_channels] + x = paddle.mean(x, axis=1) + # [N, 1, in_channels] + x = paddle.reshape(x, shape=[-1, self.in_channels]) + # [N, in_channels] + score = self.fc(x) + # [N, num_class] + #score = F.softmax(score) #NOTE remove + return score diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/losses/__init__.py b/applications/VideoQualityAssessment/paddlevideo/modeling/losses/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ba45f3c653a57b3da1b4178a85158527281d93fa --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/losses/__init__.py @@ -0,0 +1,21 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .base import BaseWeightedLoss +from .smooth_l1_loss import SmoothL1Loss +from .l1_loss import L1Loss + +__all__ = ['SmoothL1Loss', 'L1Loss'] diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/losses/base.py b/applications/VideoQualityAssessment/paddlevideo/modeling/losses/base.py new file mode 100644 index 0000000000000000000000000000000000000000..b34ac4422f53f0c597b914a576fbb6bc6adaee2b --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/losses/base.py @@ -0,0 +1,51 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from abc import abstractmethod +import paddle +import paddle.nn as nn + +#XXX use _forward?? or forward?? +class BaseWeightedLoss(nn.Layer): + """Base class for loss. + + All subclass should overwrite the ``_forward()`` method which returns the + normal loss without loss weights. + + Args: + loss_weight (float): Factor scalar multiplied on the loss. + Default: 1.0. + """ + + def __init__(self, loss_weight=1.0): + super().__init__() + self.loss_weight = loss_weight + + @abstractmethod + def _forward(self, *args, **kwargs): + pass + + def forward(self, *args, **kwargs): + """Defines the computation performed at every call. + Args: + *args: The positional arguments for the corresponding + loss. + **kwargs: The keyword arguments for the corresponding + loss. + Returns: + paddle.Tensor: The calculated loss. + """ + return self._forward(*args, **kwargs) * self.loss_weight diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/losses/l1_loss.py b/applications/VideoQualityAssessment/paddlevideo/modeling/losses/l1_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..61272d2d0e25708974509903d90b0944e1a934e6 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/losses/l1_loss.py @@ -0,0 +1,38 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import paddle +import paddle.nn.functional as F + +from ..registry import LOSSES +from .base import BaseWeightedLoss + + +@LOSSES.register() +class L1Loss(BaseWeightedLoss): + """L1 Loss.""" + def _forward(self, score, labels): + """Forward function. + Args: + score (paddle.Tensor): The class score. + labels (paddle.Tensor): The ground truth labels. + Returns: + loss (paddle.Tensor): The returned L1 loss. + """ + + labels = labels.astype(score.dtype) + loss = F.l1_loss(score, labels) + return loss diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/losses/smooth_l1_loss.py b/applications/VideoQualityAssessment/paddlevideo/modeling/losses/smooth_l1_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..5ded6e7e35296b98c51759d62a262c5c59a2394b --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/losses/smooth_l1_loss.py @@ -0,0 +1,39 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import paddle +import paddle.nn.functional as F + +from ..registry import LOSSES +from .base import BaseWeightedLoss + + +@LOSSES.register() +class SmoothL1Loss(BaseWeightedLoss): + """smooth L1 Loss.""" + def _forward(self, score, labels): + """Forward function. + Args: + score (paddle.Tensor): The class score. + labels (paddle.Tensor): The ground truth labels. + Returns: + loss (paddle.Tensor): The returned smooth L1 Loss. + """ + + labels = labels.astype(score.dtype) + loss = F.smooth_l1_loss(score, labels) + + return loss diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/registry.py b/applications/VideoQualityAssessment/paddlevideo/modeling/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..8dcd6a8972413a7c7444bb2ec43844d4f62dc229 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/registry.py @@ -0,0 +1,23 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from ..utils import Registry + +BACKBONES = Registry('backbone') +HEADS = Registry('head') +RECOGNIZERS = Registry('recognizer') +LOCALIZERS = Registry('localizer') +LOSSES = Registry('loss') diff --git a/applications/VideoQualityAssessment/paddlevideo/modeling/weight_init.py b/applications/VideoQualityAssessment/paddlevideo/modeling/weight_init.py new file mode 100644 index 0000000000000000000000000000000000000000..ae3a670bb78b72425cc3ecba58be5b463f7e3bd3 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/modeling/weight_init.py @@ -0,0 +1,55 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import numpy as np +import paddle.nn.initializer as init + + +def weight_init_(layer, + func, + weight_name=None, + bias_name=None, + bias_value=0.0, + **kwargs): + """ + In-place params init function. + Usage: + .. code-block:: python + + import paddle + import numpy as np + + data = np.ones([3, 4], dtype='float32') + linear = paddle.nn.Linear(4, 4) + input = paddle.to_tensor(data) + print(linear.weight) + linear(input) + + weight_init_(linear, 'Normal', 'fc_w0', 'fc_b0', std=0.01, mean=0.1) + print(linear.weight) + """ + + if hasattr(layer, 'weight') and layer.weight is not None: + getattr(init, func)(**kwargs)(layer.weight) + if weight_name is not None: + # override weight name + layer.weight.name = weight_name + + if hasattr(layer, 'bias') and layer.bias is not None: + init.Constant(bias_value)(layer.bias) + if bias_name is not None: + # override bias name + layer.bias.name = bias_name diff --git a/applications/VideoQualityAssessment/paddlevideo/solver/__init__.py b/applications/VideoQualityAssessment/paddlevideo/solver/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ed48d380c46a9775c3b2df8a5a5d8ea572966130 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/solver/__init__.py @@ -0,0 +1,17 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +from .optimizer import build_optimizer +from .lr import build_lr diff --git a/applications/VideoQualityAssessment/paddlevideo/solver/custom_lr.py b/applications/VideoQualityAssessment/paddlevideo/solver/custom_lr.py new file mode 100644 index 0000000000000000000000000000000000000000..93afe970372c938c68fc7c643583aa289560a34d --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/solver/custom_lr.py @@ -0,0 +1,201 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import math +from paddle.optimizer.lr import * + +""" +PaddleVideo Learning Rate Schedule: +You can use paddle.optimizer.lr +or define your custom_lr in this file. +""" + + +class CustomWarmupCosineDecay(LRScheduler): + """ + We combine warmup and stepwise-cosine which is used in slowfast model. + + Args: + warmup_start_lr (float): start learning rate used in warmup stage. + warmup_epochs (int): the number epochs of warmup. + cosine_base_lr (float|int, optional): base learning rate in cosine schedule. + max_epoch (int): total training epochs. + num_iters(int): number iterations of each epoch. + last_epoch (int, optional): The index of last epoch. Can be set to restart training. Default: -1, means initial learning rate. + verbose (bool, optional): If ``True``, prints a message to stdout for each update. Default: ``False`` . + Returns: + ``CosineAnnealingDecay`` instance to schedule learning rate. + """ + def __init__(self, + warmup_start_lr, + warmup_epochs, + cosine_base_lr, + max_epoch, + num_iters, + last_epoch=-1, + verbose=False): + self.warmup_start_lr = warmup_start_lr + self.warmup_epochs = warmup_epochs + self.cosine_base_lr = cosine_base_lr + self.max_epoch = max_epoch + self.num_iters = num_iters + #call step() in base class, last_lr/last_epoch/base_lr will be update + super(CustomWarmupCosineDecay, self).__init__(last_epoch=last_epoch, + verbose=verbose) + + def step(self, epoch=None): + """ + ``step`` should be called after ``optimizer.step`` . It will update the learning rate in optimizer according to current ``epoch`` . + The new learning rate will take effect on next ``optimizer.step`` . + Args: + epoch (int, None): specify current epoch. Default: None. Auto-increment from last_epoch=-1. + Returns: + None + """ + if epoch is None: + if self.last_epoch == -1: + self.last_epoch += 1 + else: + self.last_epoch += 1 / self.num_iters # update step with iters + else: + self.last_epoch = epoch + self.last_lr = self.get_lr() + + if self.verbose: + print('Epoch {}: {} set learning rate to {}.'.format( + self.last_epoch, self.__class__.__name__, self.last_lr)) + + def _lr_func_cosine(self, cur_epoch, cosine_base_lr, max_epoch): + """start to cosine""" + return cosine_base_lr * (math.cos(math.pi * cur_epoch / max_epoch) + + 1.0) * 0.5 + + def get_lr(self): + """Define lr policy""" + lr = self._lr_func_cosine(self.last_epoch, self.cosine_base_lr, + self.max_epoch) + lr_end = self._lr_func_cosine(self.warmup_epochs, self.cosine_base_lr, + self.max_epoch) + + # Perform warm up. + if self.last_epoch < self.warmup_epochs: + lr_start = self.warmup_start_lr + alpha = (lr_end - lr_start) / self.warmup_epochs + lr = self.last_epoch * alpha + lr_start + return lr + + +class CustomWarmupPiecewiseDecay(LRScheduler): + """ + This op combine warmup and stepwise-cosine which is used in slowfast model. + + Args: + warmup_start_lr (float): start learning rate used in warmup stage. + warmup_epochs (int): the number epochs of warmup. + step_base_lr (float|int, optional): base learning rate in step schedule. + max_epoch (int): total training epochs. + num_iters(int): number iterations of each epoch. + last_epoch (int, optional): The index of last epoch. Can be set to restart training. Default: -1, means initial learning rate. + verbose (bool, optional): If ``True``, prints a message to stdout for each update. Default: ``False`` . + Returns: + ``CustomWarmupPiecewiseDecay`` instance to schedule learning rate. + """ + def __init__(self, + warmup_start_lr, + warmup_epochs, + step_base_lr, + lrs, + gamma, + steps, + max_epoch, + num_iters, + last_epoch=0, + verbose=False): + self.warmup_start_lr = warmup_start_lr + self.warmup_epochs = warmup_epochs + self.step_base_lr = step_base_lr + self.lrs = lrs + self.gamma = gamma + self.steps = steps + self.max_epoch = max_epoch + self.num_iters = num_iters + self.last_epoch = last_epoch + self.last_lr = self.warmup_start_lr # used in first iter + self.verbose = verbose + self._var_name = None + + def step(self, epoch=None, rebuild=False): + """ + ``step`` should be called after ``optimizer.step`` . It will update the learning rate in optimizer according to current ``epoch`` . + The new learning rate will take effect on next ``optimizer.step`` . + Args: + epoch (int, None): specify current epoch. Default: None. Auto-increment from last_epoch=-1. + Returns: + None + """ + if epoch is None: + if not rebuild: + self.last_epoch += 1 / self.num_iters # update step with iters + else: + self.last_epoch = epoch + self.last_lr = self.get_lr() + + if self.verbose: + print('Epoch {}: {} set learning rate to {}.'.format( + self.last_epoch, self.__class__.__name__, self.last_lr)) + + def _lr_func_steps_with_relative_lrs(self, cur_epoch, lrs, base_lr, steps, + max_epoch): + """lr func steps with relative lrs""" + # get step index + steps = steps + [max_epoch] + for ind, step in enumerate(steps): + if cur_epoch < step: + break + + return lrs[ind - 1] * base_lr + + def get_lr(self): + """Define lr policy""" + lr = self._lr_func_steps_with_relative_lrs( + self.last_epoch, + self.lrs, + self.step_base_lr, + self.steps, + self.max_epoch, + ) + lr_end = self._lr_func_steps_with_relative_lrs( + self.warmup_epochs, + self.lrs, + self.step_base_lr, + self.steps, + self.max_epoch, + ) + + # Perform warm up. + if self.last_epoch < self.warmup_epochs: + lr_start = self.warmup_start_lr + alpha = (lr_end - lr_start) / self.warmup_epochs + lr = self.last_epoch * alpha + lr_start + return lr + + +class CustomPiecewiseDecay(PiecewiseDecay): + """CustomPiecewiseDecay""" + def __init__(self, **kargs): + """start""" + kargs.pop('num_iters') + super().__init__(**kargs) diff --git a/applications/VideoQualityAssessment/paddlevideo/solver/lr.py b/applications/VideoQualityAssessment/paddlevideo/solver/lr.py new file mode 100644 index 0000000000000000000000000000000000000000..62c2c46253e6c1c74c5439760049c0fe8026fc89 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/solver/lr.py @@ -0,0 +1,49 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +import copy +import paddle +from . import custom_lr + + +def build_lr(cfg, num_iters): + """ + Build a learning rate scheduler accroding to ```OPTIMIZER``` configuration, and it always pass into the optimizer. + In configuration: + learning_rate: + name: 'PiecewiseDecay' + boundaries: [20, 60] + values: [0.00025, 0.000025, 0.0000025] + + + Returns: + A paddle.optimizer.lr instance. + """ + + cfg_copy = cfg.copy() + + #when learning_rate is LRScheduler + if cfg_copy.get('learning_rate') and isinstance(cfg_copy['learning_rate'], + dict): + cfg_copy['learning_rate'] = build_lr( + cfg_copy['learning_rate'], + num_iters) #not support only inner iter_step + + lr_name = cfg_copy.pop('name') + if cfg_copy.get('iter_step'): + cfg_copy['num_iters'] = num_iters + cfg_copy.pop('iter_step') + + return getattr(custom_lr, lr_name)(**cfg_copy) diff --git a/applications/VideoQualityAssessment/paddlevideo/solver/optimizer.py b/applications/VideoQualityAssessment/paddlevideo/solver/optimizer.py new file mode 100644 index 0000000000000000000000000000000000000000..9feaf777c62d12c6ecc508739a30b041855b0438 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/solver/optimizer.py @@ -0,0 +1,79 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import copy +import paddle + + +def build_optimizer(cfg, lr_scheduler, parameter_list=None): + + """ + Build an optimizer and learning rate scheduler to optimize parameters accroding to ```OPTIMIZER``` field in configuration . + + In configuration: + OPTIMIZER: + name: Momentum + momentum: 0.9 + weight_decay: 0.001 + or + + OPTIMIZER: + name: Momentum + momentum: 0.9 + weight_decay: + name: "L1" + value: 0.001 + + Momentum optimizer will be applied to optimize network and L1Decay regularizer will be applied to avoid overfit. + + OPTIMIZER: + name: Adam + weight_decay: + name: "L2" + value: 0.001 + + Adam optimizer will be applied to optimize network and L2Decay regularizer will applied to avoid overfit. + + Refer to ```https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/regularizer/L2Decay_en.html``` for more details. + + Args: + cfg (dict): optimizer configuration. + lr_schduler: learning rate scheduler. + parameter_list (list): parameters to be optimized. + + Returns: + optimizer (paddle.optimizer): paddle optimizer. + + """ + + + cfg_copy = cfg.copy() + #XXX check none and illegal cfg!!! + opt_name = cfg_copy.pop('name') + # deal with weight decay + if cfg_copy.get('weight_decay'): + if isinstance(cfg_copy.get('weight_decay'), float) or 'L1' in cfg_copy.get('weight_decay').get('name').upper(): + cfg_copy['weight_decay'] = cfg_copy.get('weight_decay').get('value') + elif 'L2' in cfg_copy.get('weight_decay').get('name').upper(): + cfg_copy['weight_decay'] = paddle.regularizer.L2Decay(cfg_copy.get('weight_decay').get('value')) + else: + raise ValueError + + cfg_copy.pop('learning_rate') + + return getattr(paddle.optimizer, opt_name)(lr_scheduler, + parameters=parameter_list, + **cfg_copy) diff --git a/applications/VideoQualityAssessment/paddlevideo/tasks/__init__.py b/applications/VideoQualityAssessment/paddlevideo/tasks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..53aa1f9b8f1216da4520d7abc085e1fbe477bc4d --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/tasks/__init__.py @@ -0,0 +1,20 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .train import train_model +from .test import test_model + +__all__ = ['train_model', 'test_model'] diff --git a/applications/VideoQualityAssessment/paddlevideo/tasks/test.py b/applications/VideoQualityAssessment/paddlevideo/tasks/test.py new file mode 100644 index 0000000000000000000000000000000000000000..df44c258056e93098c976b68c10002ccedb295ee --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/tasks/test.py @@ -0,0 +1,80 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import paddle +from paddlevideo.utils import get_logger +from ..loader.builder import build_dataloader, build_dataset +from ..metrics import build_metric +from ..modeling.builder import build_model +from paddlevideo.utils import load +import time + + +logger = get_logger("paddlevideo") + + +@paddle.no_grad() +def test_model(cfg, weights, parallel=True): + """Test model entry + + Args: + cfg (dict): configuration. + weights (str): weights path to load. + parallel (bool): Whether to do multi-cards testing. Default: True. + + """ + + # 1. Construct model. + model = build_model(cfg.MODEL) + if parallel: + model = paddle.DataParallel(model) + + # 2. Construct dataset and dataloader. + cfg.DATASET.test.test_mode = True + dataset = build_dataset((cfg.DATASET.test, cfg.PIPELINE.test)) + batch_size = cfg.DATASET.get("test_batch_size", 1) + places = paddle.set_device('gpu') + # default num worker: 0, which means no subprocess will be created + num_workers = cfg.DATASET.get('num_workers', 0) + dataloader_setting = dict(batch_size=batch_size, + num_workers=num_workers, + places=places, + drop_last=False, + shuffle=False) + + data_loader = build_dataloader(dataset, **dataloader_setting) + + model.eval() + + state_dicts = load(weights) + model.set_state_dict(state_dicts) + + # add params to metrics + cfg.METRIC.data_size = len(dataset) + cfg.METRIC.batch_size = batch_size + + Metric = build_metric(cfg.METRIC) + + for batch_id, data in enumerate(data_loader): + if parallel: + outputs = model._layers.test_step(data) + else: + outputs = model.test_step(data) + Metric.update(batch_id, data, outputs) + + Metric.accumulate() + + diff --git a/applications/VideoQualityAssessment/paddlevideo/tasks/train.py b/applications/VideoQualityAssessment/paddlevideo/tasks/train.py new file mode 100644 index 0000000000000000000000000000000000000000..c5cf7f8397fcf2531651f4afc6675a4eb1338ebf --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/tasks/train.py @@ -0,0 +1,289 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import time +import os.path as osp + +import paddle +import paddle.distributed.fleet as fleet +from ..loader.builder import build_dataloader, build_dataset +from ..modeling.builder import build_model +from ..solver import build_lr, build_optimizer +from ..metrics import build_metric +from ..utils import do_preciseBN +from paddlevideo.utils import get_logger, coloring +from paddlevideo.utils import (AverageMeter, build_rec_record, log_batch, log_epoch, + save, load, mkdir) +#from paddlevideo.metrics import QualityMetric +import numpy as np +from scipy import stats + +def train_model(cfg, + weights=None, + parallel=True, + validate=True, + amp=False, + fleet=False): + """Train model entry + + Args: + cfg (dict): configuration. + weights (str): weights path for finetuning. + parallel (bool): Whether multi-cards training. Default: True. + validate (bool): Whether to do evaluation. Default: False. + + """ + if fleet: + fleet.init(is_collective=True) + + logger = get_logger("paddlevideo") + batch_size = cfg.DATASET.get('batch_size', 8) + valid_batch_size = cfg.DATASET.get('valid_batch_size', batch_size) + places = paddle.set_device('gpu') + + # default num worker: 0, which means no subprocess will be created + num_workers = cfg.DATASET.get('num_workers', 0) + model_name = cfg.model_name + output_dir = cfg.get("output_dir", "./output/model_name/") + mkdir(output_dir) + + # 1. Construct model + model = build_model(cfg.MODEL) + if parallel: + model = paddle.DataParallel(model) + + if fleet: + model = paddle.distributed_model(model) + + # 2. Construct dataset and dataloader + train_dataset = build_dataset((cfg.DATASET.train, cfg.PIPELINE.train)) + train_dataloader_setting = dict(batch_size=batch_size, + num_workers=num_workers, + collate_fn_cfg=cfg.get('MIX', None), + places=places) + + train_loader = build_dataloader(train_dataset, **train_dataloader_setting) + if validate: + valid_dataset = build_dataset((cfg.DATASET.valid, cfg.PIPELINE.valid)) + validate_dataloader_setting = dict( + batch_size=valid_batch_size, + num_workers=num_workers, + places=places, + drop_last=False, + shuffle=cfg.DATASET.get( + 'shuffle_valid', + False) #NOTE: attention lstm need shuffle valid data. + ) + valid_loader = build_dataloader(valid_dataset, + **validate_dataloader_setting) + + # 3. Construct solver. + lr = build_lr(cfg.OPTIMIZER.learning_rate, len(train_loader)) + optimizer = build_optimizer(cfg.OPTIMIZER, + lr, + parameter_list=model.parameters()) + if fleet: + optimizer = fleet.distributed_optimizer(optimizer) + # Resume + resume_epoch = cfg.get("resume_epoch", 0) + if resume_epoch: + filename = osp.join(output_dir, + model_name + "_epoch_{}".format(resume_epoch)) + resume_model_dict = load(filename + '.pdparams') + resume_opt_dict = load(filename + '.pdopt') + model.set_state_dict(resume_model_dict) + optimizer.set_state_dict(resume_opt_dict) + + # Finetune: + if weights: + assert resume_epoch == 0, "Conflict occurs when finetuning, please switch resume function off by setting resume_epoch to 0 or not indicating it." + model_dict = load(weights) + model.set_state_dict(model_dict) + + # 4. Train Model + ###AMP### + if amp: + scaler = paddle.amp.GradScaler(init_loss_scaling=1024) + + best = 0. + max_SROCC = 0 + max_PLCC = 0 + Metric = build_metric(cfg.METRIC) + for epoch in range(0, cfg.epochs): + if epoch < resume_epoch: + logger.info( + "| epoch: [{epoch+1}] <= resume_epoch: [{ resume_epoch}], continue... " + ) + continue + model.train() + record_list = build_rec_record(cfg.MODEL) + tic = time.time() + train_output = [] + train_label = [] + + + for i, data in enumerate(train_loader): + record_list['reader_time'].update(time.time() - tic) + + # 4.1 forward + ###AMP### + if amp: + with paddle.amp.auto_cast( + custom_black_list={"temporal_shift", "reduce_mean"}): + if parallel: + outputs = model._layers.train_step(data) + ## required for DataParallel, will remove in next version + model._reducer.prepare_for_backward( + list(model._find_varbase(outputs))) + else: + outputs = model.train_step(data) + + train_output.extend(outputs['output']) + train_label.extend(outputs['label']) + + avg_loss = outputs['loss'] + scaled = scaler.scale(avg_loss) + scaled.backward() + # keep prior to 2.0 design + scaler.minimize(optimizer, scaled) + optimizer.clear_grad() + + else: + if parallel: + outputs = model._layers.train_step(data) + ## required for DataParallel, will remove in next version + model._reducer.prepare_for_backward( + list(model._find_varbase(outputs))) + else: + outputs = model.train_step(data) + + train_output.extend(outputs['output']) + train_label.extend(outputs['label']) + # 4.2 backward + avg_loss = outputs['loss'] + avg_loss.backward() + # 4.3 minimize + optimizer.step() + optimizer.clear_grad() + + # log record + record_list['lr'].update(optimizer._global_learning_rate(), + batch_size) + for name, value in outputs.items(): + if name == 'output' or name == 'label': + continue + record_list[name].update(value, batch_size) + + record_list['batch_time'].update(time.time() - tic) + tic = time.time() + + if i % cfg.get("log_interval", 10) == 0: + ips = "ips: {:.5f} instance/sec.".format( + batch_size / record_list["batch_time"].val) + log_batch(record_list, i, epoch + 1, cfg.epochs, "train", ips) + + # learning rate iter step + if cfg.OPTIMIZER.learning_rate.get("iter_step"): + lr.step() + + # learning rate epoch step + if not cfg.OPTIMIZER.learning_rate.get("iter_step"): + lr.step() + + train_PLCC, train_SROCC = Metric.accumulate_train(train_output, train_label) + logger.info("train_SROCC={}".format(train_SROCC)) + logger.info("train_PLCC={}".format(train_PLCC)) + + ips = "ips: {:.5f} instance/sec.".format( + batch_size * record_list["batch_time"].count / + record_list["batch_time"].sum) + log_epoch(record_list, epoch + 1, "train", ips) + + eval_output = [] + eval_label = [] + def evaluate(best, max_SROCC, max_PLCC): + """evaluate""" + model.eval() + record_list = build_rec_record(cfg.MODEL) + record_list.pop('lr') + tic = time.time() + + for i, data in enumerate(valid_loader): + + if parallel: + outputs = model._layers.val_step(data) + else: + outputs = model.val_step(data) + eval_output.extend(outputs['output']) + eval_label.extend(outputs['label']) + + # log_record + for name, value in outputs.items(): + if name == 'output' or name == 'label': + continue + record_list[name].update(value, batch_size) + + record_list['batch_time'].update(time.time() - tic) + tic = time.time() + + if i % cfg.get("log_interval", 10) == 0: + ips = "ips: {:.5f} instance/sec.".format( + batch_size / record_list["batch_time"].val) + log_batch(record_list, i, epoch + 1, cfg.epochs, "val", ips) + + eval_PLCC, eval_SROCC = Metric.accumulate_train(eval_output, eval_label) + logger.info("val_SROCC={}".format(eval_SROCC)) + logger.info("val_PLCC={}".format(eval_PLCC)) + + if max_SROCC <= eval_SROCC and max_PLCC <= eval_PLCC: + max_SROCC = eval_SROCC + max_PLCC = eval_PLCC + logger.info("max_SROCC={}".format(max_SROCC)) + logger.info("max_PLCC={}".format(max_PLCC)) + save(optimizer.state_dict(), + osp.join(output_dir, model_name + "_best.pdopt")) + save(model.state_dict(), + osp.join(output_dir, model_name + "_best.pdparams")) + + ips = "ips: {:.5f} instance/sec.".format( + batch_size * record_list["batch_time"].count / + record_list["batch_time"].sum) + log_epoch(record_list, epoch + 1, "val", ips) + + return best, max_SROCC, max_PLCC + + # use precise bn to improve acc + if cfg.get("PRECISEBN") and (epoch % cfg.PRECISEBN.preciseBN_interval + == 0 or epoch == cfg.epochs - 1): + do_preciseBN( + model, train_loader, parallel, + min(cfg.PRECISEBN.num_iters_preciseBN, len(train_loader))) + + # 5. Validation + if validate and (epoch % cfg.get("val_interval", 1) == 0 + or epoch == cfg.epochs - 1): + with paddle.fluid.dygraph.no_grad(): + best, max_SROCC, max_PLCC = evaluate(best, max_SROCC, max_PLCC) + + # 6. Save model + if epoch % cfg.get("save_interval", 1) == 0 or epoch == cfg.epochs - 1: + save(optimizer.state_dict(), osp.join(output_dir, model_name + "_epoch_{}.pdopt".format(epoch))) + save(model.state_dict(), osp.join(output_dir, model_name + "_epoch_{}.pdparams".format(epoch))) + + + + + logger.info('training {model_name} finished') diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/__init__.py b/applications/VideoQualityAssessment/paddlevideo/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fe58285e05a58ee33d4131ed6d73f9b527ec09fb --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/__init__.py @@ -0,0 +1,25 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from .registry import Registry +from .build_utils import build +from .config import * +from .logger import setup_logger, coloring, get_logger +from .record import AverageMeter, build_record, build_rec_record, log_batch, log_epoch +from .dist_utils import get_dist_info, main_only +from .save_load import save, load, load_ckpt, mkdir +from .precise_bn import do_preciseBN +__all__ = ['Registry', 'build'] diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/build_utils.py b/applications/VideoQualityAssessment/paddlevideo/utils/build_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c8ed1cbe40bd7bca7cc82089de09d0affbd23e7a --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/build_utils.py @@ -0,0 +1,36 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +def build(cfg, registry, key='name'): + """Build a module from config dict. + Args: + cfg (dict): Config dict. It should at least contain the key. + registry (XXX): The registry to search the type from. + key (str): the key. + Returns: + obj: The constructed object. + """ + + assert isinstance(cfg, dict) and key in cfg + + cfg_copy = cfg.copy() + obj_type = cfg_copy.pop(key) + + obj_cls = registry.get(obj_type) + if obj_cls is None: + raise KeyError('{} is not in the {} registry'.format( + obj_type, registry.name)) + return obj_cls(**cfg_copy) diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/config.py b/applications/VideoQualityAssessment/paddlevideo/utils/config.py new file mode 100644 index 0000000000000000000000000000000000000000..b98bb447c04b2ab62fa6d0d7038effa7d8717c0c --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/config.py @@ -0,0 +1,180 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os +import yaml +from paddlevideo.utils.logger import coloring, get_logger, setup_logger + +__all__ = ['get_config'] + +logger = setup_logger("./", name="paddlevideo", level="INFO") + + +class AttrDict(dict): + """Attr Dict""" + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value + + +def create_attr_dict(yaml_config): + """create attr dict""" + from ast import literal_eval + for key, value in yaml_config.items(): + if type(value) is dict: + yaml_config[key] = value = AttrDict(value) + if isinstance(value, str): + try: + value = literal_eval(value) + except BaseException: + pass + if isinstance(value, AttrDict): + create_attr_dict(yaml_config[key]) + else: + yaml_config[key] = value + + +def parse_config(cfg_file): + """Load a config file into AttrDict""" + with open(cfg_file, 'r') as fopen: + yaml_config = AttrDict(yaml.load(fopen, Loader=yaml.SafeLoader)) + create_attr_dict(yaml_config) + return yaml_config + + +def print_dict(d, delimiter=0): + """ + Recursively visualize a dict and + indenting acrrording by the relationship of keys. + """ + placeholder = "-" * 60 + for k, v in sorted(d.items()): + if isinstance(v, dict): + logger.info("{}{} : ".format(delimiter * " ", coloring(k, + "HEADER"))) + print_dict(v, delimiter + 4) + elif isinstance(v, list) and len(v) >= 1 and isinstance(v[0], dict): + logger.info("{}{} : ".format(delimiter * " ", + coloring(str(k), "HEADER"))) + for value in v: + print_dict(value, delimiter + 4) + else: + logger.info("{}{} : {}".format(delimiter * " ", + coloring(k, "HEADER"), + coloring(v, "OKGREEN"))) + + if k.isupper(): + logger.info(placeholder) + + +def print_config(config): + """ + visualize configs + Arguments: + config: configs + """ + print_dict(config) + + +def check_config(config): + """ + Check config + """ + pass + + +def override(dl, ks, v): + """ + Recursively replace dict of list + Args: + dl(dict or list): dict or list to be replaced + ks(list): list of keys + v(str): value to be replaced + """ + def str2num(v): + """str2num""" + try: + return eval(v) + except Exception: + return v + + assert isinstance(dl, (list, dict)), ("{} should be a list or a dict") + assert len(ks) > 0, ('lenght of keys should larger than 0') + if isinstance(dl, list): + k = str2num(ks[0]) + if len(ks) == 1: + assert k < len(dl), ('index({}) out of range({})'.format(k, dl)) + dl[k] = str2num(v) + else: + override(dl[k], ks[1:], v) + else: + if len(ks) == 1: + #assert ks[0] in dl, ('{} is not exist in {}'.format(ks[0], dl)) + if not ks[0] in dl: + logger.warning('A new filed ({}, {}) detected!'.format(ks[0], dl)) + dl[ks[0]] = str2num(v) + else: + assert ks[0] in dl, ( + '({}) doesn\'t exist in {}, a new dict field is invalid'.format( + ks[0], dl)) + override(dl[ks[0]], ks[1:], v) + + +def override_config(config, options=None): + """ + Recursively override the config + Args: + config(dict): dict to be replaced + options(list): list of pairs(key0.key1.idx.key2=value) + such as: [ + epochs=20', + 'PIPELINE.train.transform.1.ResizeImage.resize_short=300' + ] + Returns: + config(dict): replaced config + """ + if options is not None: + for opt in options: + assert isinstance(opt, + str), ("option({}) should be a str".format(opt)) + assert "=" in opt, ( + "option({}) should contain a =" + "to distinguish between key and value".format(opt)) + pair = opt.split('=') + assert len(pair) == 2, ("there can be only a = in the option") + key, value = pair + keys = key.split('.') + override(config, keys, value) + + return config + + +def get_config(fname, overrides=None, show=True): + """ + Read config from file + """ + assert os.path.exists(fname), ('config file({}) is not exist'.format(fname)) + config = parse_config(fname) + override_config(config, overrides) + if show: + print_config(config) + check_config(config) + return config diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/dist_utils.py b/applications/VideoQualityAssessment/paddlevideo/utils/dist_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ebfdba6530921fec8953e724eb9286fafcaaad34 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/dist_utils.py @@ -0,0 +1,36 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import functools + +import paddle +import paddle.distributed as dist + +def get_dist_info(): + """get_dist_info""" + world_size = dist.get_world_size() + rank = dist.get_rank() + return rank, world_size + +def main_only(func): + """main_only""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + """wrapper""" + rank, _ = get_dist_info() + if rank == 0: + return func(*args, **kwargs) + return wrapper diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/logger.py b/applications/VideoQualityAssessment/paddlevideo/utils/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..f4f6116c8cf81749a06e2e269e1cfd06a0e02ddf --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/logger.py @@ -0,0 +1,117 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import logging +import os +import sys +import datetime + +from paddle.distributed import ParallelEnv + + + +Color = { + 'RED': '\033[31m', + 'HEADER': '\033[35m', # deep purple + 'PURPLE': '\033[95m', # purple + 'OKBLUE': '\033[94m', + 'OKGREEN': '\033[92m', + 'WARNING': '\033[93m', + 'FAIL': '\033[91m', + 'ENDC': '\033[0m' +} + + +def coloring(message, color="OKGREEN"): + """coloring""" + assert color in Color.keys() + if os.environ.get('COLORING', True): + return Color[color] + str(message) + Color["ENDC"] + else: + return message + + +logger_initialized = [] + + +def setup_logger(output=None, name="paddlevideo", level="INFO"): + """ + Initialize the paddlevideo logger and set its verbosity level to "INFO". + Args: + output (str): a file name or a directory to save log. If None, will not save log file. + If ends with ".txt" or ".log", assumed to be a file name. + Otherwise, logs will be saved to `output/log.txt`. + name (str): the root module name of this logger + Returns: + logging.Logger: a logger + """ + def time_zone(sec, fmt): + real_time = datetime.datetime.now() + return real_time.timetuple() + logging.Formatter.converter = time_zone + + logger = logging.getLogger(name) + if level == "INFO": + logger.setLevel(logging.INFO) + elif level=="DEBUG": + logger.setLevel(logging.DEBUG) + logger.propagate = False + + if level == "DEBUG": + plain_formatter = logging.Formatter( + "[%(asctime)s] %(name)s %(levelname)s: %(message)s", + datefmt="%m/%d %H:%M:%S") + else: + plain_formatter = logging.Formatter( + "[%(asctime)s] %(message)s", + datefmt="%m/%d %H:%M:%S") + # stdout logging: master only + local_rank = ParallelEnv().local_rank + if local_rank == 0: + ch = logging.StreamHandler(stream=sys.stdout) + ch.setLevel(logging.DEBUG) + formatter = plain_formatter + ch.setFormatter(formatter) + logger.addHandler(ch) + + # file logging: all workers + if output is not None: + if output.endswith(".txt") or output.endswith(".log"): + filename = output + else: + filename = os.path.join(output, ".log.txt") + if local_rank > 0: + filename = filename + ".rank{}".format(local_rank) + + # PathManager.mkdirs(os.path.dirname(filename)) + os.makedirs(os.path.dirname(filename), exist_ok=True) + + # fh = logging.StreamHandler(_cached_log_stream(filename) + fh = logging.FileHandler(filename, mode='a') + fh.setLevel(logging.DEBUG) + fh.setFormatter(plain_formatter) + logger.addHandler(fh) + logger_initialized.append(name) + return logger + + +def get_logger(name, output=None): + """get logger""" + logger = logging.getLogger(name) + if name in logger_initialized: + return logger + + return setup_logger(name=name, output=name) diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/precise_bn.py b/applications/VideoQualityAssessment/paddlevideo/utils/precise_bn.py new file mode 100644 index 0000000000000000000000000000000000000000..3f80517f8d52f051e17e68bdae332c4b88845456 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/precise_bn.py @@ -0,0 +1,84 @@ +""" +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import paddle +import itertools + +from paddlevideo.utils import get_logger +logger = get_logger("paddlevideo") +""" +Implement precise bn, which is useful for improving accuracy. +""" + + +def do_preciseBN(model, data_loader, parallel, num_iters=200): + """ + Recompute and update the batch norm stats to make them more precise. During + training both BN stats and the weight are changing after every iteration, so + the running average can not precisely reflect the actual stats of the + current model. + In this function, the BN stats are recomputed with fixed weights, to make + the running average more precise. Specifically, it computes the true average + of per-batch mean/variance instead of the running average. + This is useful to improve validation accuracy. + Args: + model: the model whose bn stats will be recomputed + data_loader: an iterator. Produce data as input to the model + num_iters: number of iterations to compute the stats. + Return: + the model with precise mean and variance in bn layers. + """ + bn_layers_list = [ + m for m in model.sublayers() + if any((isinstance(m, bn_type) + for bn_type in (paddle.nn.BatchNorm1D, paddle.nn.BatchNorm2D, + paddle.nn.BatchNorm3D))) and m.training + ] + if len(bn_layers_list) == 0: + return + + # moving_mean=moving_mean*momentum+batch_mean*(1.−momentum) + # we set momentum=0. to get the true mean and variance during forward + momentum_actual = [bn._momentum for bn in bn_layers_list] + for bn in bn_layers_list: + bn._momentum = 0. + + running_mean = [paddle.zeros_like(bn._mean) + for bn in bn_layers_list] #pre-ignore + running_var = [paddle.zeros_like(bn._variance) for bn in bn_layers_list] + + ind = -1 + for ind, data in enumerate(itertools.islice(data_loader, num_iters)): + logger.info("doing precise BN {} / {}...".format(ind + 1, num_iters)) + if parallel: + model._layers.train_step(data) + else: + model.train_step(data) + + for i, bn in enumerate(bn_layers_list): + # Accumulates the bn stats. + running_mean[i] += (bn._mean - running_mean[i]) / (ind + 1) + running_var[i] += (bn._variance - running_var[i]) / (ind + 1) + + assert ind == num_iters - 1, ( + "update_bn_stats is meant to run for {} iterations, but the dataloader stops at {} iterations." + .format(num_iters, ind)) + + # Sets the precise bn stats. + for i, bn in enumerate(bn_layers_list): + bn._mean.set_value(running_mean[i]) + bn._variance.set_value(running_var[i]) + bn._momentum = momentum_actual[i] diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/record.py b/applications/VideoQualityAssessment/paddlevideo/utils/record.py new file mode 100644 index 0000000000000000000000000000000000000000..240a7c4b56a2866434fdc297f96581b372f48ad5 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/record.py @@ -0,0 +1,122 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from collections import OrderedDict +from .logger import get_logger, coloring +logger = get_logger("paddlevideo") + +__all__ = ['AverageMeter', 'build_record', 'build_rec_record', 'log_batch', 'log_epoch'] + + +def build_record(cfg): + framework_type = cfg.get('framework') + record_list = [ + ("loss", AverageMeter('loss', '7.5f')), + ("lr", AverageMeter('lr', 'f', need_avg=False)), + ] + if 'Recognizer1D' in cfg.framework: #TODO: required specify str in framework + record_list.append(("hit_at_one", AverageMeter("hit_at_one", '.5f'))) + record_list.append(("perr", AverageMeter("perr", '.5f'))) + record_list.append(("gap", AverageMeter("gap", '.5f'))) + elif 'Recognizer' in cfg.framework: + record_list.append(("top1", AverageMeter("top1", '.5f'))) + record_list.append(("top5", AverageMeter("top5", '.5f'))) + + record_list.append(("batch_time", AverageMeter('elapse', '.3f'))) + record_list.append(("reader_time", AverageMeter('reader', '.3f'))) + record_list = OrderedDict(record_list) + return record_list + +def build_rec_record(cfg): + """build rec record""" + framework_type = cfg.get('framework') + record_list = [ + ("loss", AverageMeter('loss', '7.5f')), + ("lr", AverageMeter('lr', 'f', need_avg=False)), + ] + if 'Recognizer1D' in cfg.framework: #TODO: required specify str in framework + record_list.append(("hit_at_one", AverageMeter("hit_at_one", '.5f'))) + record_list.append(("perr", AverageMeter("perr", '.5f'))) + record_list.append(("gap", AverageMeter("gap", '.5f'))) + + record_list.append(("batch_time", AverageMeter('elapse', '.3f'))) + record_list.append(("reader_time", AverageMeter('reader', '.3f'))) + record_list = OrderedDict(record_list) + return record_list + +class AverageMeter(object): + """ + Computes and stores the average and current value + """ + def __init__(self, name='', fmt='f', need_avg=True): + self.name = name + self.fmt = fmt + self.need_avg = need_avg + self.reset() + + def reset(self): + """ reset """ + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + """ update """ + if isinstance(val, paddle.Tensor): + val = val.numpy()[0] + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + @property + def total(self): + return '{self.name}_sum: {self.sum:{self.fmt}}'.format(self=self) + + @property + def total_minute(self): + return '{self.name}_sum: {s:{self.fmt}} min'.format(s=self.sum / 60, + self=self) + + @property + def mean(self): + return '{self.name}_avg: {self.avg:{self.fmt}}'.format( + self=self) if self.need_avg else '' + + @property + def value(self): + return '{self.name}: {self.val:{self.fmt}}'.format(self=self) + + +def log_batch(metric_list, batch_id, epoch_id, total_epoch, mode, ips): + metric_str = ' '.join([str(m.value) for m in metric_list.values()]) + epoch_str = "epoch:[{:>3d}/{:<3d}]".format(epoch_id, total_epoch) + step_str = "{:s} step:{:<4d}".format(mode, batch_id) + logger.info("{:s} {:s} {:s}s {}".format( + coloring(epoch_str, "HEADER") if batch_id == 0 else epoch_str, + coloring(step_str, "PURPLE"), coloring(metric_str, 'OKGREEN'), ips)) + + +def log_epoch(metric_list, epoch, mode, ips): + metric_avg = ' '.join([str(m.mean) for m in metric_list.values()] + + [metric_list['batch_time'].total]) + + end_epoch_str = "END epoch:{:<3d}".format(epoch) + + logger.info("{:s} {:s} {:s}s {}".format(coloring(end_epoch_str, "RED"), + coloring(mode, "PURPLE"), + coloring(metric_avg, "OKGREEN"), + ips)) diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/registry.py b/applications/VideoQualityAssessment/paddlevideo/utils/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..44c5c98d80d0443f57826978b3827509c2946e46 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/registry.py @@ -0,0 +1,98 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +class Registry(object): + """ + The registry that provides name -> object mapping, to support third-party users' custom modules. + + To register an object: + + .. code-block:: python + + BACKBONES = Registry('backbone') + @BACKBONES.register() + class ResNet: + pass + Or: + .. code-block:: python + + BACKBONES = Registry('backbone') + class ResNet: + pass + BACKBONES.register(ResNet) + + Usage: To build a module. + + .. code-block:: python + backbone_name = "ResNet" + b = BACKBONES.get(backbone_name)() + + """ + def __init__(self, name): + """ + Args: + name (str): the name of this registry + """ + self._name = name + self._obj_map = {} + + def __contains__(self, key): + return self._obj_map.get(key) is not None + + def _do_register(self, name, obj): + """do register""" + assert ( + name not in self._obj_map + ), "An object named '{}' was already registered in '{}' registry!".format( + name, self._name) + self._obj_map[name] = obj + + def register(self, obj=None, name=None): + """ + Register the given object under the the name `obj.__name__`. + Can be used as either a decorator or not. See docstring of this class for usage. + """ + if obj is None: + # used as a decorator + def deco(func_or_class, name=name): + if name is None: + name = func_or_class.__name__ + self._do_register(name, func_or_class) + return func_or_class + + return deco + + # used as a function call + if name is None: + name = obj.__name__ + self._do_register(name, obj) + + def get(self, name): + """Get the registry record. + + Args: + name (str): The class name. + + Returns: + ret: The class. + """ + ret = self._obj_map.get(name) + if ret is None: + raise KeyError( + "No object named '{}' found in '{}' registry!".format( + name, self._name)) + + return ret diff --git a/applications/VideoQualityAssessment/paddlevideo/utils/save_load.py b/applications/VideoQualityAssessment/paddlevideo/utils/save_load.py new file mode 100644 index 0000000000000000000000000000000000000000..9e52d22a19550abf38c612434eea9e9540881002 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/utils/save_load.py @@ -0,0 +1,117 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +import os +import os.path as osp +import time + +import pickle +from tqdm import tqdm +import paddle + +from paddlevideo.utils import get_logger +from paddlevideo.utils import main_only + + +#XXX(shipping): maybe need load N times because of different cards have different params. +@main_only +def load_ckpt(model, weight_path): + """ + load_ckpt + """ + #model.set_state_dict(state_dict) + + if not osp.isfile(weight_path): + raise IOError('{weight_path} is not a checkpoint file') + #state_dicts = load(weight_path) + + logger = get_logger("paddlevideo") + state_dicts = paddle.load(weight_path) + tmp = {} + total_len = len(model.state_dict()) + localkeyname = [i for i in state_dicts] + + with tqdm(total=total_len, + position=1, + bar_format='{desc}', + desc="Loading weights") as desc: + #for item in tqdm(model.state_dict(), total=total_len, position=0): + for i, item in enumerate(tqdm(model.state_dict(), total=total_len, position=0)): + name = item + desc.set_description('Loading %s' % name) + print("model name is {}, correspoding local name is {}".format(name, localkeyname[i])) + #tmp[name] = state_dicts[name] + tmp[name] = state_dicts[localkeyname[i]] + time.sleep(0.01) + ret_str = "loading {:<20d} weights completed.".format( + len(model.state_dict())) + desc.set_description(ret_str) + model.set_state_dict(tmp) + + +def mkdir(dir): + """mkdir""" + if not os.path.exists(dir): + # avoid error when train with multiple gpus + try: + os.makedirs(dir) + except: + pass + + +""" +def save(state_dicts, file_name): + def convert(state_dict): + model_dict = {} + + for k, v in state_dict.items(): + if isinstance( + v, + (paddle.fluid.framework.Variable, paddle.fluid.core.VarBase)): + model_dict[k] = v.numpy() + else: + model_dict[k] = v + + return model_dict + + final_dict = {} + for k, v in state_dicts.items(): + if isinstance( + v, + (paddle.fluid.framework.Variable, paddle.fluid.core.VarBase)): + final_dict = convert(state_dicts) + break + elif isinstance(v, dict): + final_dict[k] = convert(v) + else: + final_dict[k] = v + + with open(file_name, 'wb') as f: + pickle.dump(final_dict, f, protocol=2) +""" + + +@main_only +def save(obj, path): + """save""" + paddle.save(obj, path) + + +def load(file_name): + """load""" + if not osp.isfile(file_name): + raise IOError('{file_name} not exist') + return paddle.load(file_name) diff --git a/applications/VideoQualityAssessment/paddlevideo/version.py b/applications/VideoQualityAssessment/paddlevideo/version.py new file mode 100644 index 0000000000000000000000000000000000000000..50266f5e014782bc5bddd95c8bcc3d8e25d58971 --- /dev/null +++ b/applications/VideoQualityAssessment/paddlevideo/version.py @@ -0,0 +1,18 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +__all__ = ["paddlevideo_version"] +paddlevideo_version = "0.0.1" diff --git a/applications/VideoQualityAssessment/pretrained_dygraph/download.txt b/applications/VideoQualityAssessment/pretrained_dygraph/download.txt new file mode 100644 index 0000000000000000000000000000000000000000..22436cff58eda0a8029d020da78bc9512990c512 --- /dev/null +++ b/applications/VideoQualityAssessment/pretrained_dygraph/download.txt @@ -0,0 +1 @@ +https://bj.bcebos.com/acg-algo/PaddleVideo_application/VideoQualityAssessment/ppTSM.pdparams diff --git a/applications/VideoQualityAssessment/requirements.txt b/applications/VideoQualityAssessment/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b12d827cbb025576682702b099dc507c34146af4 --- /dev/null +++ b/applications/VideoQualityAssessment/requirements.txt @@ -0,0 +1,7 @@ +paddlepaddle-gpu>=2.0.0 +tqdm +PyYAML>=5.1 +numpy +decord +pandas +opencv-python==4.2.0.32 diff --git a/applications/VideoQualityAssessment/run.sh b/applications/VideoQualityAssessment/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..f7f5c49c5e41c3b1872dade92cdc55e916d2d241 --- /dev/null +++ b/applications/VideoQualityAssessment/run.sh @@ -0,0 +1,20 @@ +export CUDA_VISIBLE_DEVICES=0 + +# run training +python3.7 -B -m paddle.distributed.launch --gpus="0" --log_dir=log_pptsm main.py --amp --validate -c configs/recognition/tsm/pptsm_regression.yaml + +# run testing +#python3.7 -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_pptsm main.py -c configs/recognition/tsm/pptsm_regression.yaml --test --weights=output/model_name/ppTSM_best.pdparams + +#finetune +#python3 -m paddle.distributed.launch --gpus="0,1,2,3" main.py --amp -c ./configs/recognition/tsm/pptsm_regression.yaml --validate --weights=./output/model_name/ppTSM_best.pdparams + +#resume +#python3 -m paddle.distributed.launch --gpus="0,1,2,3" main.py --amp -c ./configs/recognition/tsm/pptsm_regression.yaml --validate -o resume_epoch=2 +# export_models script +# just use `example` as example, please replace to real name. +#python3.7 tools/export_model.py -c configs/example.yaml -p output/model_name/ppTSM_best.pdparams -o ./inference + +# predict script +# just use `example` as example, please replace to real name. +#python3.7 tools/predict.py -v example.avi --model_file "./inference/example.pdmodel" --params_file "./inference/example.pdiparams" --enable_benchmark=False --model="example" --num_seg=8 diff --git a/applications/VideoQualityAssessment/save_model.sh b/applications/VideoQualityAssessment/save_model.sh new file mode 100644 index 0000000000000000000000000000000000000000..5cf6fbdff9484300ea49385b58a37b5b9f278051 --- /dev/null +++ b/applications/VideoQualityAssessment/save_model.sh @@ -0,0 +1,5 @@ +python tools/export_model.py \ + -c ./configs/recognition/tsm/pptsm.yaml \ + -p ./output/ppTSM/ppTSM_best.pdparams \ + -o ./inference/ \ + --num_seg=32 diff --git a/applications/VideoQualityAssessment/setup.py b/applications/VideoQualityAssessment/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..477013b71dae2380eeb4d45a59d9b671150bd83a --- /dev/null +++ b/applications/VideoQualityAssessment/setup.py @@ -0,0 +1,57 @@ +""" +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + +from setuptools import setup +from io import open + +with open('requirements.txt', encoding="utf-8-sig") as f: + requirements = f.readlines() + +def readme(): + """readme""" + with open('docs/en/whl_en.md', encoding="utf-8-sig") as f: + README = f.read() + return README + + +setup( + name='paddlevideo', #name of .whl file + packages=['ppvideo'], #install package name + package_dir={'ppvideo': ''}, + include_package_data=True, #Accept all data files and directories matched by MANIFEST.in + install_requires=requirements, + entry_points={"console_scripts": ["ppvideo= ppvideo.tools.paddlevideo_clas:main"]}, + version='0.0.1', + license='Apache License 2.0', + description='Awesome Video toolkits based on PaddlePaddle ', + long_description=readme(), + long_description_content_type='text/markdown', + url='https://github.com/PaddlePaddle/PaddleVideo', + download_url='https://github.com/PaddlePaddle/PaddleVideo.git', + keywords=[ + 'A treasure chest for video understanding powered by PaddlePaddle.' + ], + classifiers=[ + 'Intended Audience :: Developers', 'Operating System :: OS Independent', + 'Natural Language :: Chinese (Simplified)', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Utilities' + ],) diff --git a/applications/VideoTag/FineTune.md b/applications/VideoTag/FineTune.md new file mode 100644 index 0000000000000000000000000000000000000000..f0ce9ed48aad9a700158d4d59e2f4ab0c6c0f927 --- /dev/null +++ b/applications/VideoTag/FineTune.md @@ -0,0 +1,206 @@ +# 模型微调指南 + +--- +## 内容 +参考本文档,您可以使用自己的训练数据在VideoTag预训练模型上进行fine-tune,训练出自己的模型。 + +文档内容包括: +- [原理解析](#原理解析) +- [对AttentionLSTM模型进行微调](#对AttentionLSTM模型进行微调) +- [对TSN模型进行微调](#对TSN模型进行微调) +- [扩展内容](#扩展内容) +- [参考论文](#参考论文) + + +## 原理解析 +VideoTag采用两阶段建模方式,由两个模型组成: TSN + AttentionLSTM。 + +Temporal Segment Network (TSN) 是经典的基于2D-CNN的视频分类模型。该模型通过稀疏采样视频帧的方式,在捕获视频时序信息的同时降低了计算量。详细内容请参考论文[Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859) +AttentionLSTM以视频的特征向量作为输入,采用双向长短时记忆网络(LSTM)对所有帧特征进行编码,并增加Attention层,将每个时刻的隐状态输出与自适应权重线性加权得到最终分类向量。详细内容请参考论文[AttentionCluster](https://arxiv.org/abs/1711.09550) + +VideoTag训练时分两个阶段: 第一阶段使用少量视频样本(十万级别)训练大规模视频特征提取模型(TSN);第二阶段使用千万级数据训练预测器(AttentionLSTM)。 + +VideoTag预测时也分两个阶段: 第一阶段以视频文件作为输入,经过去除了全连接层以及损失函数层的TSN网络后得到输出特征向量;第二阶段以TSN网络输出的特征向量作为输入,经过AttentionLSTM后得到最终的分类结果。 + +基于我们的预模型,您可以使用自己的训练数据进行fine-tune: + +- [对AttentionLSTM模型进行微调](#对AttentionLSTM模型进行微调) +- [对TSN模型进行微调](#对TSN模型进行微调) + + +## 对AttentionLSTM模型进行微调 +AttentionLSTM以视频特征作为输入,显存占用少,训练速度较TSN更快,因此推荐优先对AttentionLSTM模型进行微调。输入视频首先经过TSN预训练模型提取特征向量,然后将特征向量作为训练输入数据,微调AttentionLSTM模型。 + +### TSN预模型提取特征向量 + +#### 数据准备 + +- 预训练权重下载: 参考[样例代码运行指南-数据准备-预训练权重下载](./Run.md) + +- 准备训练数据: 准备好待训练的视频数据,并在video\_tag/data/TsnExtractor.list文件中指定待训练的文件路径,内容格式如下: + +``` +my_video_path/my_video_file1.mp4 +my_video_path/my_video_file2.mp4 +... +``` + +#### 特征提取 +特征提取脚本如下: + +``` +python tsn_extractor.py --model_name=TSN --config=./configs/tsn.yaml --weights=./weights/tsn.pdparams +``` + +- 通过--weights可指定TSN权重参数的存储路径,默认为video\_tag/weights/tsn.pdparams + +- 通过--save\_dir可指定特征向量保存路径,默认为video\_tag/data/tsn\_features,不同输入视频的特征向量提取结果分文件保存在不同的npy文件中,目录形式为: + +``` +video_tag + ├──data + ├──tsn_features + ├── my_feature_file1.npy + ├── my_feature_file2.npy + ... +``` +- tsn提取的特征向量维度为```帧数*特征维度```,默认为300 * 2048。 + +### AttentionLSTM模型Fine-tune + +#### 数据准备 +VideoTag中的AttentionLSTM以TSN模型提取的特征向量作为输入。在video\_tag/data/dataset/attention\_lstm/train.list文件中指定待训练的文件路径和对应的标签,内容格式如下: + +``` +my_feature_path/my_feature_file1.npy label1 label2 +my_feature_path/my_feature_file2.npy label1 +... +``` +- 一个输入视频可以有多个标签,标签索引为整型数据,文件名与标签之间、多个标签之间以一个空格分隔; + +- 标签索引与标签名称的之间的对应关系以list文件指定,可参考VideoTag用到的label_3396.txt文件构造,行索引对应标签索引; + +- 验证集、测试集以及预测数据集的构造方式同训练集类似,仅需要在video\_tag/data/attention\_lstm/目录下对应的list文件中指定相关文件路径/标签即可。 + +#### 模型训练 +使用VideoTag中的AttentionLSTM预模型进行fine-tune训练脚本如下: +``` +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +python train.py --model_name=AttentionLSTM --config=./configs/attention_lstm.yaml --pretrain=./weights/attention_lstm +``` + +- AttentionLSTM模型默认使用8卡训练,总的batch size数是1024。若使用单卡训练,请修改环境变量,脚本如下: +``` +export CUDA_VISIBLE_DEVICES=0 +python train.py --model_name=AttentionLSTM --config=./configs/attention_lstm-single.yaml --pretrain=./weights/attention_lstm +``` + +- 请确保训练样本数大于batch_size数 + +- 通过--pretrain参数可指定AttentionLSTM预训练模型的路径,默认为./weights/attention\_lstm; + +- 模型相关配置写在video_tag/configs/attention\_lstm.yaml文件中,可以方便的调节各项超参数; + +- 通过--save_dir参数可指定训练模型参数的保存路径,默认为./data/checkpoints; + +#### 模型评估 +可用如下方式进行模型评估: +``` +python eval.py --model_name=AttentionLSTM --config=./configs/attention_lstm.yaml --weights=./data/checkpoints/AttentionLSTM_epoch9.pdparams +``` +- 通过--weights参数可指定评估需要的权重,默认为./data/checkpoints/AttentionLSTM_epoch9.pdparams; + +- 评估结果以log的形式直接打印输出GAP、Hit@1等精度指标。 + +#### 模型推断 +可用如下方式进行模型推断: +``` +python predict.py --model_name=AttentionLSTM --config=./configs/attention_lstm.yaml --weights=./data/checkpoints/AttentionLSTM_epoch9.pdparams +``` + +- 通过--weights参数可指定推断需要的权重,默认为./data/checkpoints/AttentionLSTM_epoch9.pdparams; + +- 通过--label_file参数指定标签文件,请根据自己的数据修改,默认为./label_3396.txt; + +- 预测结果会以日志形式打印出来,同时也保存在json文件中,通过--save_dir参数可指定预测结果保存路径,默认为./data/predict_results/attention_lstm。 + + +## 对TSN模型进行微调 +VideoTag中使用的TSN模型以mp4文件为输入,backbone为ResNet101。 + +### 数据准备 + +准备好训练视频文件后,在video\_tag/data/dataset/tsn/train.list文件中指定待训练的文件路径和对应的标签即可,内容格式如下: + +``` +my_video_path/my_video_file1.mp4 label1 +my_video_path/my_video_file2.mp4 label2 +... +``` +- 一个输入视频只能有一个标签,标签索引为整型数据,标签索引与文件名之间以一个空格分隔; + +- 验证集、测试集以及预测数据集的构造方式同训练集类似,仅需要在video\_tag/data/dataset/tsn目录下对应的list文件中指定相关文件路径/标签即可。 + +#### 模型训练 +使用VideoTag中的TSN预模型进行fine-tune训练脚本如下: +``` +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +python train.py --model_name=TSN --config=./configs/tsn.yaml --pretrain=./weights/tsn +``` + +- TSN模型默认使用8卡训练,总的batch size数是256。若使用单卡训练,请修改环境变量,脚本如下: +``` +export CUDA_VISIBLE_DEVICES=0 +python train.py --model_name=TSN --config=./configs/tsn-single.yaml --pretrain=./weights/tsn +``` + +- 通过--pretrain参数可指定TSN预训练模型的路径,示例为./weights/tsn; + +- 模型相关配置写在video_tag/configs/tsn.yaml文件中,可以方便的调节各项超参数; + +- 通过--save_dir参数可指定训练模型参数的保存路径,默认为./data/checkpoints; + +#### 模型评估 +可用如下方式进行模型评估: +``` +python eval.py --model_name=TSN --config=./configs/tsn.yaml --weights=./data/checkpoints/TSN_epoch44.pdparams +``` + +- 通过--weights参数可指定评估需要的权重,示例为./data/checkpoints/TSN_epoch44.pdparams; + +- 评估结果以log的形式直接打印输出TOP1_ACC、TOP5_ACC等精度指标。 + +#### 模型推断 +可用如下方式进行模型推断: +``` +python predict.py --model_name=TSN --config=./configs/tsn.yaml --weights=./data/checkpoints/TSN_epoch44.pdparams --save_dir=./data/predict_results/tsn/ +``` + +- 通过--weights参数可指定推断需要的权重,示例为./data/checkpoints/TSN_epoch44.pdparams; + +- 通过--label_file参数指定标签文件,请根据自己的数据修改,默认为./label_3396.txt; + +- 预测结果会以日志形式打印出来,同时也保存在json文件中,通过--save_dir参数可指定预测结果保存路径,示例为./data/predict_results/tsn。 + +### 训练加速 +TSN模型默认以mp4的视频文件作为输入,训练时需要先对视频文件解码,再将解码后的数据送入网络进行训练,如果视频文件很大,这个过程将会很耗时。 + +为加速训练,可以先将视频解码成图片,然后保存下来,训练时直接根据索引读取帧图片作为输入,加快训练过程。 + +- 数据准备: 首先将视频解码,存成帧图片;然后生成帧图片的文件路径列表。实现过程可参考[ucf-101数据准备](../../../../dygraph/tsn/data/dataset/ucf101/README.md) + +- 修改配置文件: 修改配置文件./config/tsn.yaml,其中MODEL.format值改为"frames",不同模式下的filelist值改为对应的帧图片文件list。 + + +## 扩展内容 + +- 更多关于TSN模型的内容可参考PaddleCV视频库[TSN视频分类模型](https://github.com/PaddlePaddle/models/blob/develop/PaddleCV/video/models/tsn/README.md)。 + +- 更多关于AttentionLSTM模型的内容可参考PaddleCV视频库[AttentionLSTM视频分类模型](https://github.com/PaddlePaddle/models/tree/develop/PaddleCV/video/models/attention_lstm)。 + + +## 参考论文 + +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859), Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoou Tang, Luc Van Gool + +- [Beyond Short Snippets: Deep Networks for Video Classification](https://arxiv.org/abs/1503.08909) Joe Yue-Hei Ng, Matthew Hausknecht, Sudheendra Vijayanarasimhan, Oriol Vinyals, Rajat Monga, George Toderici diff --git a/applications/VideoTag/README.md b/applications/VideoTag/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b33c3666c966e9c2ed421473becd81fc450e9651 --- /dev/null +++ b/applications/VideoTag/README.md @@ -0,0 +1,31 @@ +# VideoTag 飞桨大规模视频分类模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [使用方法](#使用方法) + + +## 模型简介 + +飞桨大规模视频分类模型VideoTag基于百度短视频业务千万级数据,支持3000个源于产业实践的实用标签,具有良好的泛化能力,非常适用于国内大规模(千万/亿/十亿级别)短视频分类场景的应用。VideoTag采用两阶段建模方式,即图像建模和序列学习。第一阶段,使用少量视频样本(十万级别)训练大规模视频特征提取模型(Extractor);第二阶段,使用千万级数据训练预测器(Predictor),最终实现在超大规模(千万/亿/十亿级别)短视频上产业应用,其原理示意如下图所示。 + +

+
+VideoTag模型示意图 +

+ +- 数据处理:视频是按特定顺序排列的一组图像的集合,这些图像也称为帧。视频分类任务需要先对短视频进行解码,然后再将输出的图像帧序列灌入到VideoTag中进行训练和预测。 + +- 图像建模:先从训练数据中,对每个类别均匀采样少量样本数据,构成十万量级的训练视频。然后使用TSN网络进行训练,提取所有视频帧的TSN模型分类层前一层的特征数据。在这个过程中,每一帧都被转化成相应的特征向量,一段视频被转化成一个特征序列。 + +- 序列学习:采用Attention clusters、LSTM和Nextvlad对特征序列进行建模,学习各个特征之间的组合方式,进一步提高模型准确率。由于序列学习相比于图像建模耗时更短,因此可以融合多个具有互补性的序列模型。示例代码仅使用Attention\_LSTM网络进行序列特征预测。 + +- 预测结果:融合多个模型结果实现视频分类,进一步提高分类准确率。 + + +## 使用方法 +- [1. 如何运行样例代码](./Run.md) +- [2. 如何使用自己的数据进行测试](./Test.md) +- [3. 如何进行模型fine-tune](./FineTune.md) diff --git a/applications/VideoTag/Run.md b/applications/VideoTag/Run.md new file mode 100644 index 0000000000000000000000000000000000000000..3ffee7f1801547f63a5d16b11aa74b196be769c9 --- /dev/null +++ b/applications/VideoTag/Run.md @@ -0,0 +1,109 @@ +# 样例代码运行指南 + +--- +## 内容 +参考本文档,您可以快速熟悉VideoTag的使用方法,观察VideoTag的预训练模型在示例视频上的预测结果。 + +文档内容包括: +- [安装说明](#安装说明) +- [数据准备](#数据准备) +- [模型推断](#模型推断) + + +## 安装说明 + +### 环境依赖: + +``` + CUDA >= 9.0 + cudnn >= 7.5 +``` + +### 依赖安装: + +- 1.7.0 <= PaddlePaddle版本 <= 2.0.0: pip install paddlepaddle-gpu==1.8.4.post97 -i https://mirror.baidu.com/pypi/simple +- opencv版本 >= 4.1.0: pip install opencv-python==4.2.0.32 + +## 数据准备 + +### 预训练权重下载 + +我们提供了[TSN](https://videotag.bj.bcebos.com/video_tag_tsn.tar)和[AttentionLSTM](https://videotag.bj.bcebos.com/video_tag_lstm.tar)预训练权重,请在video\_tag目录下新建weights目录,并将下载解压后的参数文件放在weights目录下: + +``` + mkdir weights + cd weights + wget https://videotag.bj.bcebos.com/video_tag_tsn.tar + wget https://videotag.bj.bcebos.com/video_tag_lstm.tar + tar -zxvf video_tag_tsn.tar + tar -zxvf video_tag_lstm.tar + rm video_tag_tsn.tar -rf + rm video_tag_lstm.tar -rf + mv video_tag_tsn/* . + mv attention_lstm/* . + rm video_tag_tsn/ -rf + rm attention_lstm -rf +``` + +所得目录结构如下: + +``` +video_tag + ├──weights + ├── attention_lstm.pdmodel + ├── attention_lstm.pdopt + ├── attention_lstm.pdparams + ├── tsn.pdmodel + ├── tsn.pdopt + └── tsn.pdparams +``` + +### 示例视频下载 + +我们提供了[样例视频](https://videotag.bj.bcebos.com/mp4.tar)方便用户测试,请下载后解压,并将视频文件放置在video\_tag/data/mp4目录下: + +``` + cd data/ + wget https://videotag.bj.bcebos.com/mp4.tar + tar -zxvf mp4.tar + rm mp4.tar -rf +``` + +所得目录结构如下: + +``` +video_tag + ├──data + ├── mp4 + ├── 1.mp4 + ├── 2.mp4 + └── ... +``` + +## 模型推断 + +模型推断的启动方式如下: + + python videotag_test.py + +- 预测结果会以日志方式打印,示例如下: +``` +[========video_id [ data/mp4/1.mp4 ] , topk(20) preds: ========] +class_id: 3110, class_name: 训练 , probability: 0.97730666399 +class_id: 2159, class_name: 蹲 , probability: 0.945082366467 +... +[========video_id [ data/mp4/2.mp4 ] , topk(20) preds: ========] +class_id: 2773, class_name: 舞蹈 , probability: 0.850423932076 +class_id: 1128, class_name: 表演艺术 , probability: 0.0446354188025 +... +``` + +- 通过--save\_dir可指定预测结果存储路径,默认为video\_tag/data/VideoTag\_results,不同输入视频的预测结果分文件保存在不同的json文件中,文件的内容格式为: + +``` + [file_path, + {"class_name": class_name1, "probability": probability1, "class_id": class_id1}, + {"class_name": class_name2, "probability": probability2, "class_id": class_id2}, + ... + ] +``` diff --git a/applications/VideoTag/Test.md b/applications/VideoTag/Test.md new file mode 100644 index 0000000000000000000000000000000000000000..155f7634d47ad74d99ca2a90c10de11608d81bd0 --- /dev/null +++ b/applications/VideoTag/Test.md @@ -0,0 +1,31 @@ +# 预训练模型自测指南 + +## 内容 +参考本文档,您可以快速测试VideoTag的预训练模型在自己业务数据上的预测效果。 + +主要内容包括: +- [数据准备](#数据准备) +- [模型推断](#模型推断) + +## 数据准备 + +在数据准备阶段,您需要准备好自己的测试数据,并在video\_tag/data/VideoTag\_test.list文件中指定待推断的测试文件路径,内容格式如下: +``` +my_video_path/my_video_file1.mp4 +my_video_path/my_video_file2.mp4 +... +``` + +## 模型推断 + +模型推断的启动方式如下: + + python videotag_test.py + +- 目前支持的视频文件输入格式为:mp4、mkv和webm格式; + +- 模型会从输入的视频文件中*均匀抽取300帧*用于预测。对于较长的视频文件,建议先截取有效部分输入模型以提高预测速度; + +- 通过--use\_gpu参数可指定是否使用gpu进行推断,默认使用gpu。对于10s左右的短视频文件,gpu推断时间约为4s; + +- 通过--filelist可指定输入list文件路径,默认为video\_tag/data/VideoTag\_test.list。 diff --git a/applications/VideoTag/configs/attention_lstm-single.yaml b/applications/VideoTag/configs/attention_lstm-single.yaml new file mode 100644 index 0000000000000000000000000000000000000000..708cb835037624fcb5696ab19cad6bafb98d7ecf --- /dev/null +++ b/applications/VideoTag/configs/attention_lstm-single.yaml @@ -0,0 +1,36 @@ +MODEL: + name: "AttentionLSTM" + dataset: "YouTube-8M" #Default, don't recommand to modify it + bone_nework: None + drop_rate: 0.5 + feature_names: ['rgb'] #rbg only, without audio + feature_dims: [2048] + embedding_size: 1024 + lstm_size: 512 + num_classes: 3396 + topk: 20 + +TRAIN: + epoch: 10 + learning_rate: 0.000125 + decay_epochs: [5] + decay_gamma: 0.1 + weight_decay: 0.0008 + num_samples: 5000000 # modify it according to the number samples of your dataset + pretrain_base: None + batch_size: 128 + use_gpu: True + num_gpus: 1 + filelist: "data/dataset/attention_lstm/train.list" + +VALID: + batch_size: 128 + filelist: "data/dataset/attention_lstm/val.list" + +TEST: + batch_size: 128 + filelist: "data/dataset/attention_lstm/test.list" + +INFER: + batch_size: 1 + filelist: "data/dataset/attention_lstm/infer.list" diff --git a/applications/VideoTag/configs/attention_lstm.yaml b/applications/VideoTag/configs/attention_lstm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..75ee7381446944aca2b0b31dc93bfecfe76332d6 --- /dev/null +++ b/applications/VideoTag/configs/attention_lstm.yaml @@ -0,0 +1,36 @@ +MODEL: + name: "AttentionLSTM" + dataset: "YouTube-8M" #Default, don't recommand to modify it + bone_nework: None + drop_rate: 0.5 + feature_names: ['rgb'] #rbg only, without audio + feature_dims: [2048] + embedding_size: 1024 + lstm_size: 512 + num_classes: 3396 + topk: 20 + +TRAIN: + epoch: 10 + learning_rate: 0.001 + decay_epochs: [5] + decay_gamma: 0.1 + weight_decay: 0.0008 + num_samples: 5000000 # modify it according to the number samples of your dataset + pretrain_base: None + batch_size: 1024 + use_gpu: True + num_gpus: 8 + filelist: "data/dataset/attention_lstm/train.list" + +VALID: + batch_size: 1024 + filelist: "data/dataset/attention_lstm/val.list" + +TEST: + batch_size: 128 + filelist: "data/dataset/attention_lstm/test.list" + +INFER: + batch_size: 1 + filelist: "data/dataset/attention_lstm/infer.list" diff --git a/applications/VideoTag/configs/tsn-single.yaml b/applications/VideoTag/configs/tsn-single.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f412d619c312527605a21691a6e277fcb1fd7e8e --- /dev/null +++ b/applications/VideoTag/configs/tsn-single.yaml @@ -0,0 +1,53 @@ +MODEL: + name: "TSN" + format: "video" # ["video", "frames"] + num_classes: 400 + seglen: 1 + image_mean: [0.485, 0.456, 0.406] + image_std: [0.229, 0.224, 0.225] + num_layers: 101 + topk: 5 + +TRAIN: + seg_num: 3 # training with 3 segments + epoch: 45 + short_size: 256 + target_size: 224 + num_reader_threads: 12 + buf_size: 1024 + batch_size: 32 + use_gpu: True + num_gpus: 1 + filelist: "./data/dataset/tsn/train.list" + learning_rate: 0.00125 + learning_rate_decay: 0.1 + l2_weight_decay: 1e-4 + momentum: 0.9 + total_videos: 224684 # modify it according to the number samples of your dataset + +VALID: + seg_num: 3 + short_size: 256 + target_size: 224 + num_reader_threads: 12 + buf_size: 1024 + batch_size: 32 + filelist: "./data/dataset/tsn/val.list" + +TEST: + seg_num: 7 + short_size: 256 + target_size: 224 + num_reader_threads: 12 + buf_size: 1024 + batch_size: 16 + filelist: "./data/dataset/tsn/test.list" + +INFER: + seg_num: 300 # infer using 300 segments + short_size: 256 + target_size: 224 + num_reader_threads: 12 + buf_size: 1024 + batch_size: 1 + filelist: "./data/dataset/tsn/infer.list" diff --git a/applications/VideoTag/configs/tsn.yaml b/applications/VideoTag/configs/tsn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8eda91423e05b44421b092fd11ebf81e0cee6097 --- /dev/null +++ b/applications/VideoTag/configs/tsn.yaml @@ -0,0 +1,53 @@ +MODEL: + name: "TSN" + format: "video" # ["video", "frames"] + num_classes: 400 + seglen: 1 + image_mean: [0.485, 0.456, 0.406] + image_std: [0.229, 0.224, 0.225] + num_layers: 101 + topk: 5 + +TRAIN: + seg_num: 3 # training with 3 segments + epoch: 45 + short_size: 256 + target_size: 224 + num_reader_threads: 12 + buf_size: 1024 + batch_size: 256 + use_gpu: True + num_gpus: 8 + filelist: "./data/dataset/tsn/train.list" + learning_rate: 0.01 + learning_rate_decay: 0.1 + l2_weight_decay: 1e-4 + momentum: 0.9 + total_videos: 224684 # modify it according to the number samples of your dataset + +VALID: + seg_num: 3 + short_size: 256 + target_size: 224 + num_reader_threads: 12 + buf_size: 1024 + batch_size: 256 + filelist: "./data/dataset/tsn/val.list" + +TEST: + seg_num: 7 + short_size: 256 + target_size: 224 + num_reader_threads: 12 + buf_size: 1024 + batch_size: 16 + filelist: "./data/dataset/tsn/test.list" + +INFER: + seg_num: 300 # infer using 300 segments + short_size: 256 + target_size: 224 + num_reader_threads: 12 + buf_size: 1024 + batch_size: 1 + filelist: "./data/dataset/tsn/infer.list" diff --git a/applications/VideoTag/eval.py b/applications/VideoTag/eval.py new file mode 100644 index 0000000000000000000000000000000000000000..e53cabb1fe768a445cd46f4aaaa6856be9010a00 --- /dev/null +++ b/applications/VideoTag/eval.py @@ -0,0 +1,133 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import time +import logging +import argparse +import ast +import paddle.fluid as fluid + +from utils.config_utils import * +import models +from reader import get_reader +from metrics import get_metrics +from utils.utility import check_cuda +from utils.utility import check_version + +logging.root.handlers = [] +FORMAT = '[%(levelname)s: %(filename)s: %(lineno)4d]: %(message)s' +logging.basicConfig(level=logging.INFO, format=FORMAT, stream=sys.stdout) +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--model_name', + type=str, + default='AttentionCluster', + help='name of model to train.') + parser.add_argument('--config', + type=str, + default='configs/attention_cluster.txt', + help='path to config file of model') + parser.add_argument( + '--batch_size', + type=int, + default=None, + help='test batch size. None to use config file setting.') + parser.add_argument('--use_gpu', + type=ast.literal_eval, + default=True, + help='default use gpu.') + parser.add_argument( + '--weights', + type=str, + default='./data/checkpoints/AttentionLSTM_epoch9.pdparams', + help='weight path.') + parser.add_argument( + '--save_dir', + type=str, + default=os.path.join('data', 'evaluate_results'), + help='output dir path, default to use ./data/evaluate_results') + parser.add_argument('--log_interval', + type=int, + default=1, + help='mini-batch interval to log.') + args = parser.parse_args() + return args + + +def test(args): + # parse config + config = parse_config(args.config) + test_config = merge_configs(config, 'test', vars(args)) + print_configs(test_config, "Test") + use_dali = test_config['TEST'].get('use_dali', False) + + # build model + test_model = models.get_model(args.model_name, test_config, mode='test') + test_model.build_input(use_dataloader=False) + test_model.build_model() + test_feeds = test_model.feeds() + test_fetch_list = test_model.fetches() + + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + + exe.run(fluid.default_startup_program()) + + if args.weights: + assert os.path.exists( + args.weights), "Given weight dir {} not exist.".format(args.weights) + weights = args.weights or test_model.get_weights() + + logger.info('load test weights from {}'.format(weights)) + + test_model.load_test_weights(exe, weights, fluid.default_main_program()) + + # get reader and metrics + test_reader = get_reader(args.model_name.upper(), 'test', test_config) + test_metrics = get_metrics(args.model_name.upper(), 'test', test_config) + + test_feeder = fluid.DataFeeder(place=place, feed_list=test_feeds) + + epoch_period = [] + for test_iter, data in enumerate(test_reader()): + cur_time = time.time() + test_outs = exe.run(fetch_list=test_fetch_list, + feed=test_feeder.feed(data)) + period = time.time() - cur_time + epoch_period.append(period) + test_metrics.accumulate(test_outs) + + # metric here + if args.log_interval > 0 and test_iter % args.log_interval == 0: + info_str = '[EVAL] Batch {}'.format(test_iter) + test_metrics.calculate_and_log_out(test_outs, info_str) + + if not os.path.isdir(args.save_dir): + os.makedirs(args.save_dir) + test_metrics.finalize_and_log_out("[EVAL] eval finished. ", args.save_dir) + + +if __name__ == "__main__": + args = parse_args() + # check whether the installed paddle is compiled with GPU + check_cuda(args.use_gpu) + check_version() + logger.info(args) + + test(args) diff --git a/applications/VideoTag/images.png b/applications/VideoTag/images.png new file mode 100644 index 0000000000000000000000000000000000000000..50ada247073a8aaaf3d4004547f8772ddb31fcb2 Binary files /dev/null and b/applications/VideoTag/images.png differ diff --git a/applications/VideoTag/label_3396.txt b/applications/VideoTag/label_3396.txt new file mode 100644 index 0000000000000000000000000000000000000000..bcda50c015c15d0f0cbd129a251e4a58b1fc93bd --- /dev/null +++ b/applications/VideoTag/label_3396.txt @@ -0,0 +1,3396 @@ +胶合板 +坠楼 +空手道 +弹奏 +直升机 +罗盘 +健身 +羽毛球拍 +龙与地下城 +漆 +混合器 +学生 +安全气囊 +法庭 +游泳池 +潜艇 +穆斯林头巾 +奇葩 +绞狐大冒险 +飞行器 +演出 +喷枪 +萝莉 +暗黑血统 +彭彭丁满历险记 +出生 +嫩模 +流星雨 +超市 +StepMania +自动扶梯 +讲座 +缝纫机 +自助餐 +衣服 +翼装飞行 +手语 +可爱颂 +复合弓 +列车 +欧洲模拟卡车 +吃豆人 +队长 +僵尸 +猩红 +战争片 +通关攻略 +横梁 +机场 +引体向上 +暴力片 +橱柜 +卡车 +美人 +仙境传说 +格斗 +奇趣蛋 +健美 +新能源 +佳能 +电视 +喊麦 +信件 +双胞胎 +膳食补充剂 +胸部 +碟子 +女排 +地铁:最后的曙光 +牛肉 +激光照明 +毛巾 +面包店 +时空之轮 +泰迪 +吉他 +绿茶 +自驾游 +签名会 +酱 +抽屉 +山火 +T台 +喝醉 +马桶 +巴松管 +皇帝 +沙丘 +主播 +炖汤 +糖 +球球大作战 +彩票 +中暑 +雷达 +独木舟 +星座 +弓箭 +跑车 +大豆 +妖怪 +激光 +中秋节 +风景 +橡皮筋 +固体 +音乐会 +幽灵 +救生员 +彩虹 +政治 +眼线 +柴 +医疗 +购物中心 +舰载机 +空战 +服装 +钢模 +拖鞋 +教室 +羽毛球 +烤肉 +煎饼 +金星 +火箭 +婴儿车 +黑暗之魂 +夏目友人帐 +图像处理 +恐龙 +柔术 +剪刀 +冒险任务世界 +冰雹 +木工刨 +白金汉宫 +可丽饼 +绅士 +盖瑞模组 +滑板 +游戏网站 +套房 +动作教学 +DOTA +海盗传说 +小马慢跑 +怪物中学 +快闪 +冠军 +手风琴 +工具 +进击的巨人 +怀孕 +停车场 +舌钉 +自行车运动 +飞檐走壁 +滑雪板 +保健 +大蒜 +门 +咏春 +热火吉他手 +筷子 +饮料罐 +拳无虚发 +糗事 +豆腐 +动物园大亨 +佛兰肯斯坦 +动漫 +机长 +脱发 +石英 +医生 +母婴 +数码 +螳螂 +加仑 +核电站 +老鹰 +哑铃 +成语故事 +情景剧 +小提琴 +熊猫 +泥石流 +贴花 +合唱团 +质量效应 +东京食尸鬼 +流行音乐 +犁 +帆 +监拍 +城市 +液氮 +扳手 +卫星 +跳伞 +三维 +美味 +特种部队 +名模 +手帕 +瀑布 +教师 +风铃 +爱丽丝梦游仙境 +风光 +通用电气公司 +逗比 +豹子 +石油 +仙乐传说 +晴天 +皮革 +露台·天井 +实验室 +口琴 +驾车 +枕头 +鸡 +遥控器 +铁路运输 +瓦片 +原子弹 +偶像剧 +闯关 +西游记 +吉他音箱 +车速表 +甜品 +电源供应器 +人行道 +疲劳驾驶 +房车 +量子 +民工 +薄暮传说 +节日 +连连看 +遥控 +科学探索 +银河 +雨水沟 +小丑 +建造 +鹅 +地毯 +赛车俱乐部 +超级飞侠 +美女与野兽 +克兰娜德 +中央处理器 +儿童故事 +口罩 +警匪片 +美女直播 +海洋 +睡衣 +忍者 +烧伤 +裙子 +剪影 +生活大爆炸 +麦田怪圈 +勺子 +狮子王 +床戏 +导管 +冰雪奇缘 +彩泥 +货物 +驼铃 +牙膏 +高铁 +古风 +新娘 +深空传说 +鹰 +鹿 +铲车 +星际战甲 +怪物猎人 +转蛋 +香奈儿 +醉驾 +坦克世界 +新能源汽车 +幻想传奇 +纺织品 +超级英雄 +谍战片 +起重机 +钥匙·按键 +苹果商店 +河粉 +名侦探柯南 +蜂窝 +演唱会 +喷泉 +比基尼 +面粉 +日本食玩 +王子 +画画 +激情戏 +中国队 +帆船 +电商 +消防员 +美腿 +侏罗纪 +吃饭 +锯木机 +烤面包机 +土星 +珠子 +大头儿子 +穴位 +旅客 +演员 +短信 +擂台 +东方永夜抄 +龙之谷 +马路 +袜子 +神秘岛 +勋章 +斑马 +攻壳特工队 +激流回旋 +路易鬼屋 +飞盘 +汽车 +走秀 +异度之刃 +奥利奥 +相声 +房屋 +三国无双 +猫和老鼠 +高校 +鬼片 +维修 +巢 +煎蛋 +哪吒 +排球 +人体穿孔 +核武器 +明星 +水底 +水库 +海军陆战队 +景区 +陀螺战士 +战斗公主西娜 +教学 +火花塞 +收费站 +风力 +马里奥派对 +操作系统 +灼眼的夏娜 +古罗马 +哈士奇 +气象 +神魔之塔 +锁定:现代空战 +球接头娃娃 +神鬼寓言 +幽灵战车 +战争前线 +骡子 +出游 +早餐 +华为 +房间 +现代片 +海报 +游戏王 +咳嗽 +金丝雀 +音乐剧 +根 +灯泡 +星界边境 +视频教学 +剥 +钢铁 +星之卡比 +试驾 +车技 +剑 +树 +茄子 +轨道 +坠毁 +面团 +玩具屋 +拳击 +音乐中心 +行李 +长江 +花絮 +纯情罗曼史 +地精 +铁铲 +公园 +杠铃 +旅游团 +特斯拉线圈 +喷染术 +电子书 +猪猪侠 +骆驼 +假人挑战 +推杆 +图书馆 +洗澡 +耀西之岛 +武装突袭 +幼儿园 +印刷电路板 +头盔式相机 +金字塔 +双簧管 +养老院 +黎明杀机 +复活节兔子 +马棚 +枪杀 +二维码 +击杀 +刷子 +古筝 +财经 +武术 +影视周边 +游览车 +鳄鱼 +开箱 +水晶 +街头霸王 +恐怖袭击 +过生日 +陶瓷 +健身球 +慢镜头 +贝斯 +异形附身 +风扇 +时装秀 +海底 +奔驰小哥 +弹弓 +生化奇兵 +俱乐部 +人字拖 +推土机 +钞票 +救人 +派对 +土豆 +宿舍 +玉米 +乐动魔方 +国产剧 +柚子 +模子 +细菌 +背包 +婚礼 +菠菜 +遛狗 +东方红魔乡 +山口 +驴友 +偶像大师 +噬神者 +假面骑士 +瑞奇与叮当 +新郎 +坦克在线 +网吧 +酵母 +车手 +枪击 +杂志封面 +孩之宝 +猎人 +夜市 +黑岩射手 +王座 +雕塑粘土 +同人志 +浪客剑心 +车票 +重生娃娃 +驱逐舰 +反叛的鲁路修 +领带 +死亡空间 +幽默 +障碍技巧 +运输机 +铙钹 +条码 +采石场 +排骨 +壁橱 +高尔夫球 +恐怖主义 +圆号 +悠悠球 +科技奇趣 +陶轮 +石头 +枪战 +纸板 +斯诺克 +荒野大镖客 +吉祥物 +满月 +野蛮人柯南 +家电 +电子竞技 +但丁地狱 +天花板 +披萨 +车辆 +巨人 +风车 +高速公路 +婚房 +蛤蜊 +抢救 +兔子 +航展 +火山 +发动机 +装载机 +皮艇 +梳子 +维秘 +星际火狐 +嫦娥 +沼泽 +舞曲 +炒鸡蛋 +心灵杀手 +怪物 +中国风 +理发师 +悬崖 +铅笔 +博士 +海豚 +芥末 +磨刀 +卸妆 +黄牌 +魔法门 +飞行 +游泳 +羚羊 +自动售货机 +优惠券 +银行 +打车 +东北二人转 +演讲 +香槟酒 +油罐车 +海豹 +万智牌 +步枪 +造型师 +空间站 +大风 +鼻子 +外卖 +X战警 +田径 +外星人 +木材 +速度生活 +豪车 +鬼魂 +手榴弹 +海底隧道 +表演者 +木琴 +月饼 +活页乐谱 +红牛 +天才 +南瓜饼 +鸟 +离合器 +精灵复兴 +击倒 +农产品 +轰炸 +商家 +美貌 +狗粮 +绞盘 +虚构人物 +冰川 +怒之铁拳 +车祸 +星火 +陆战队 +太阳 +大学 +录音机 +全职猎人 +内衣 +赛车总动员 +同学会 +四重奏 +桨 +驾驶员 +健身房 +瓷器 +抢劫 +爆米花 +绿色 +蕾丝 +黑熊 +公主抱 +刀剑神域 +馒头 +圣诞礼物 +墙壁 +幼儿 +信用卡 +刀 +狂飙旧金山 +日历 +新生 +婚戒 +雪 +雨 +竹子 +美人鱼 +音乐键盘 +娃娃 +键盘 +动力火车 +骑兵·装甲兵 +立交桥 +散步 +成就 +荣誉勋章 +助攻 +沙滩 +蚯蚓 +动物 +汽车越野赛 +项链 +啤酒 +女装 +和尚 +乳清蛋白 +圣诞树 +手绘 +投篮 +大麦 +光头强 +工作会议 +苍蝇 +宝藏 +射击游戏 +粉笔 +杏仁 +碗 +神舟 +胭脂 +惊天动地 +马 +封面 +小学 +物联网 +沙子 +录音棚 +挖土机 +穿衣 +飞机 +大盘 +内涝 +恶魔 +鳄梨 +飞驰竞速 +西兰花 +实验 +录影机 +气球塔防 +跑酷 +交警 +熊 +桔梗 +解放军 +活动房屋 +相机 +数学 +特斯拉 +太空堡垒 +宅男女神 +安卓 +冰块 +鸡舍 +美妙天堂 +化石 +超时空要塞 +数字 +网球 +神秘海域 +艺考 +艺术节 +编织 +打字 +明星派 +二十一点 +护栏 +大海 +极光 +舞力全开 +广场 +神庙逃亡 +纽扣 +时装周 +西葫芦 +炊具和烤盘 +星巴克 +油炸 +划船 +创世纪 +摩托车越野赛 +星星 +金刚 +弹球 +美女 +三明治 +工艺 +冒险 +垃圾桶 +极限竞速 +加菲猫 +宝宝辅食 +首饰 +场地赛 +球 +幻想水浒 +生活剧 +希曼 +插图 +潜水 +秃鹫 +诺亚方舟 +少女 +比武 +糖果粉碎传奇 +拳皇 +墨水 +校园暴力 +引擎 +脱口秀 +路由·伐木 +牡蛎 +漂移 +熊出没 +校车 +牧羊人 +功夫 +植物大战僵尸 +朗诵 +娇妻 +镜框·画框 +百叶窗 +客流 +咖啡 +塑像 +生物学 +手电筒 +机器 +座位 +沙包·沙袋 +森林 +乐高主题公园 +视频制作 +充电器 +犬夜叉 +超级粉碎兄弟 +交通安全 +躲猫猫 +翼 +粘土动画 +山羊 +海王星 +导弹 +街头表演 +水獭 +访谈节目 +石榴 +讲解教学 +拥堵 +变形 +电饭煲 +星际公民 +猿 +头 +丝路传说 +极品飞车 +皮卡丘 +拍照 +化油器 +肥料 +鲨鱼 +星云 +冬奥会 +模拟器 +CD机 +中国梦 +捕食 +泰坦陨落 +白宫 +饺子 +光环 +火鸡 +男装 +火爆狂飙 +推钱机 +命令与征服 +大金刚国度 +古琴 +食堂 +消防站 +愤怒的小鸟 +护士 +母亲 +暗杀 +美妙旋律 +芦笋 +荷花 +弓猎 +超车 +松下 +宙斯 +生活记录 +公路 +模拟合成器 +时尚 +宾馆 +难民 +立体声扬声器 +旋转 +杯子 +模型 +坦克 +生食 +波西杰克逊 +气球 +峡谷 +锁 +粉蜡笔画 +铅笔盒 +收藏 +激光笔 +智能家居 +翻筋斗 +烤面包 +生化危机 +演奏 +百货公司 +屁股 +锯 +车站 +瓜 +极速前进 +篮子 +蹦极 +纸片马里奥 +秦时明月 +全面战争 +游乐园 +最终幻想 +水手 +水上乐园 +尾巴 +鸡蛋 +相声演员 +坚果 +硬盘驱动器 +吃货 +望远镜 +夹克 +僧侣 +山洪 +打斗 +仓库 +独奏 +毁灭战士 +牵手 +普乐路路轨道 +天鹅 +旅行社 +柔道 +景观 +古墓丽影 +蓝龙 +甜美 +拍手 +酒店 +膝盖 +歌曲 +滑翔伞 +小马宝莉 +修道院 +滑板公园 +旅馆 +云朵 +麦片 +灾区 +水槽 +卧室 +避暑 +小熊维尼 +棒球帽 +拖车 +四大名助 +铜管乐器 +沙画 +外太空 +模拟人生 +健身教练 +数字电子 +公寓 +乐迪 +枪战片 +便秘 +姑娘 +大宅门 +猪蹄 +山峰 +三国志大战 +灯 +锅炉 +火 +气球造型 +面部 +光标 +动作片 +上网本 +汽艇 +棉花 +雪橇 +热泵 +装修 +记者 +女警 +恐怖 +龙 +夜景 +民警 +算命 +手里剑 +夜晚 +笑傲江湖 +精灵 +炮弹 +表情包 +刮刮卡 +三轮车 +护目镜 +墙纸 +洗头 +红包 +星系 +运动鞋 +菌类 +冰 +拔牙 +腿 +肿瘤 +先锋 +开心农场 +迪士尼 +山体滑坡 +表格 +文物 +眉毛 +刷牙 +绝命毒师 +电子宠物 +咖啡机 +流苏花边 +素描 +超级跑跑 +搏击 +司机 +卡通 +灰姑娘 +晨练 +记号笔 +心脏 +大提琴 +卫生巾 +受灾 +任天堂 +珠宝 +英雄连 +溜冰场 +青岛大姨 +大灰熊 +骑车 +基督 +道具 +料理 +甜菜根 +鱼饵 +车床 +反曲弓 +影视 +网络直播 +车库 +波斯王子 +船厂 +捕食者 +青铜 +橄榄 +污点·着色 +咖啡屋 +水稻 +改装车 +小正太 +烧烤 +卡布奇诺 +蝴蝶结 +桥梁 +邮件 +数码宝贝 +手臂 +炉子 +学校 +霸王龙 +山 +客车 +焊接 +小车 +分裂细胞 +管道 +爱情剧 +摇滚名人堂 +游行 +完美世界 +开枪 +微波炉 +中学 +东方大头条 +香菇 +虾 +双眼皮 +椅子 +格雷少年 +相亲节目 +称重秤 +香精油 +小路 +压力清洗 +木头 +水彩画 +土豆泥 +电脑 +方舟 +乐高好友 +球体 +冷空气 +大闸蟹 +帽子 +涂料 +手提包 +战争 +水球 +汤 +西红柿 +唇妆 +商铺 +王者之剑 +腕表 +藤蔓 +钱包 +刀工 +平衡车 +奥斯卡金像奖 +抗日剧 +导游 +行星边际 +泡沫 +任务栏 +中药 +死侍 +小小大星球 +自行车 +签名 +胸肌 +太极 +儿童安全座椅 +口哨 +罗技 +休闲 +汉堡 +德军司令部 +变压器 +考拉 +动物之森 +手势 +竖琴 +椰子 +大炮 +医保 +杂技 +电影摄像机 +表演艺术 +话剧 +工作室 +黄河 +吸毒 +黄油 +无限试驾 +高空 +冬天 +酒 +洞穴 +甘薯 +流星体 +手表 +救护车 +金牌 +麦迪逊广场花园 +特技演员 +饼干 +垃圾车 +服装搭配 +出租车 +暴力 +女王 +盗墓 +手提箱 +丝巾 +化学反应 +海贼王 +淋浴 +选秀 +成型 +童话故事 +麦克风 +黑客 +无尽传说 +羊 +狙击手 +小轮车 +夺宝奇兵 +美食 +食品 +肥皂泡 +骑牛 +辫子 +重型设备 +战队 +制服诱惑 +法官 +蝎子 +小屋 +酒精灯 +青鬼 +马赛克 +南方公园 +无人机 +调酒师 +万万没想到 +粉底 +捕鱼 +初音未来 +毒贩 +矮人 +好莱坞 +六孔哨 +棺材 +猜拳 +潜水服 +搞笑 +火星 +盗窃 +DJ +沐浴类产品 +长颈鹿 +整蛊 +围攻 +教堂 +黑带 +浮桥 +单眼皮 +陷 +软件 +过山车大亨 +围巾 +幸存者 +情感剧 +洗剂 +拆除 +星际迷航 +浮子 +雪地 +安保 +黄金眼 +追尾 +岩石 +电视广告 +行窃 +会计 +鸭子 +VR显示器 +莱克斯卢瑟 +反恐精英 +蒸汽机 +球场 +游戏动漫 +玉米卷 +漫威传奇 +腾讯 +亚洲 +卫生间 +吸烟 +战争机器 +青蛙 +喜羊羊与灰太狼 +飞艇 +猎犬 +招式 +拉伸 +连帽衫 +欧美音乐 +恶魔岛 +拳击之夜 +车 +大型强子对撞机 +舰艇 +枫之谷 +真功夫 +轴 +飞碟 +生物 +魔兽争霸 +欧巴 +平底锅 +石膏 +钢琴 +海关 +剪纸 +坐垫 +镜子 +夏令营 +战争之人 +简历 +彩排 +船 +真空管 +邮轮 +法制节目 +皇室战争 +小龙斯派罗 +博览会 +舞蹈革命 +生活 +圣诞贺卡 +拥抱 +飞飞全明星 +驾考 +卫生纸 +上市 +果酱 +儿子 +教会 +艺术团 +刷卡 +信封 +军阀 +军队 +黑塔利亚 +玉米饼 +滑雪 +猕猴桃 +提拉米苏 +航天 +芭蕾 +狮子 +跑步机 +杀出重围 +忍者龙剑传 +碰撞 +使命召唤 +自拍 +火柴 +火车站 +枫树 +咖啡师 +解说 +狒狒 +终极格斗冠军 +魔法禁书目录 +消防车 +极限运动 +电脑机箱 +兵 +家畜 +墨镜 +演技派 +大长腿 +功夫片 +梯子 +夏日 +排箫 +法师 +急救 +福尔摩斯 +农场 +发型 +决战之夜 +太子妃 +华夫饼 +刺猬索尼克 +赌博 +磨砂机 +办公室 +器官 +毕业 +军训 +带子 +治愈 +船长 +砂浆 +最游记 +绿野仙踪 +炉石传说 +数字录像机 +清洁 +喷气艇 +刺猬 +恒温器 +透视装 +黑执事 +基金 +守望者 +ATM取款机 +干墙 +曲棍球 +双节棍 +明胶 +锤子 +婚宴 +街道 +甜饼怪 +上帝模式 +狂神国度 +烈火战车 +麻将 +X音素 +液压机 +水杯 +扭曲 +魔界战记 +车评 +独角兽 +特种兵 +诱饵 +活动 +面具 +九阴真经 +实况足球 +护肤品 +游戏工作室 +榴莲 +马戏团 +原油 +蚁类 +分娩 +钓鱼 +游戏手柄 +影评 +虚幻竞技场 +神枪手 +架线工 +无线遥控飞机 +轮滑 +排气系统 +水管 +电源 +星之海洋 +摄像机 +纪录片 +优雅 +闺蜜 +曼妥思 +作曲家 +锡罐 +骑行 +快递 +电影节 +车队 +犀牛 +肌肉 +纽约时代广场 +敌人 +英雄 +八路 +纹身 +留声机唱片 +家常菜 +影视原声 +撞车 +达人秀 +古玩 +吊坠手链 +旅游 +录节目 +竞技 +黄梅戏 +村民 +昆虫 +旅行车 +草原 +毛衣 +叉车 +决斗大师 +灌木 +手工 +神之浩劫 +广场舞 +工厂 +练习室 +智能硬件 +龙珠 +龙梦幻境 +模仿 +枪支 +加速处理单元 +皮卡 +踏板车 +卡丁车 +歹徒 +跳跃 +大屠杀 +阀 +霍比特人 +煤矿 +遥控车 +女仆 +眼镜 +遇难者 +足球 +英雄工厂 +种族 +武打 +皇牌空战 +曲奇饼 +蜡像 +衬衫 +平衡木 +火灾 +水果蜜饯 +孔雀 +头文字D +战国 +正手击打 +港台剧 +空中巴士 +部队 +挡风玻璃刮水器 +楼梯 +无人驾驶 +写作 +塑料袋 +灯塔 +徒步旅行 +埃菲尔铁塔 +快餐 +丛林 +怪兽 +灌篮高手 +导航 +台球 +裤子 +包子 +绘图仪 +宠物 +冲浪板 +厕所 +龙虾 +寿司 +海蜇 +赛车游戏 +下午茶 +跨栏 +图像扫描仪 +王者荣耀 +钢琴弹奏 +润肤膏 +真人快打 +橡皮泥 +二胡 +新封印传说 +衣服熨斗 +红烧肉 +除毛 +变脸 +泡菜 +酸奶 +中文 +甘蔗 +拉丁 +萨克斯 +鼓 +炸弹人 +壁炉 +球员 +角斗士 +轮缘 +病毒 +洛基 +科技数码 +梦想俱乐部 +私房菜 +平板 +灯光 +圆筒 +工人 +音乐 +灯具 +探险 +相亲 +传送门 +互联网 +喝 +鼠 +齿轮 +油脂 +旗 +糖霜酥皮 +光学错觉 +数字音频工作站 +击球 +截拳道 +指环王 +高达 +网球王子 +瘦腿 +神秘博士 +自行火炮 +向日葵 +纤维 +电视台 +羊肉 +飞行员 +电车 +按摩 +射箭 +欧洲杯 +戒指 +英雄传说 +棋牌 +魔术 +电动车 +体操 +毁灭公爵 +T恤 +宗教 +豚鼠 +精彩剪辑 +卡拉OK +护肤 +海盗 +染发 +名人采访 +锐化 +午夜俱乐部 +吃鱼 +飙车 +吸管 +肾脏 +焙烧 +跑步 +紫罗兰 +海岛奇兵 +东京喵喵 +阅兵 +偷窃 +奶茶 +辣条 +特战先锋 +蝙蝠侠 +孤岛危机 +魔法王国 +挖掘机 +U盘 +荧光棒 +图章 +女婴 +光晕 +礼品 +会议 +车展 +电音 +家具 +木雕 +台锯 +终极奇迹 +草坪 +模拟城市 +画眉 +淑女 +酒馆 +唇膏 +手机数码 +橄榄球 +锻造 +水疗 +音悦台 +反导系统 +动感 +第二人生 +星空 +园艺 +稻草人 +无头骑士 +盔甲 +舞会 +蛋 +高空抛物 +无敌浩克 +姜饼 +印刷 +帝国时代 +黄山 +鲁邦三世 +盲人 +蛇 +睡眠 +战舰世界 +蟑螂 +面包车 +缝纫针 +脂肪 +纸模型 +室内装潢 +恐怖分子 +客机 +欧美影视 +便利店 +核弹 +双面人 +厨师 +跑道 +计算机 +灾难片 +飞哥与小佛 +放牧 +文艺演出 +肖像 +红绿灯 +锥体 +喇叭 +赛道狂飙 +全家福 +麻辣烫 +包包 +身体护甲 +航空 +毒品 +天空 +针织 +魔杖 +猪肉 +砖 +松糕 +圣诞装饰 +轰炸机 +无尽的任务 +摇滚史密斯 +网页 +汽车照明系统 +小镇 +巫师 +月球 +硬汉 +机车 +面食 +手术 +海鲜 +玩具熊的五夜后宫 +巧克力 +手机 +Vox +画法 +莫妮卡的团伙 +大米 +全金属狂潮 +随声听 +旋律 +放生 +操场 +窗户 +恐怖喜剧 +大力水手 +惩罚者 +木工 +悬疑 +长方形 +木片 +电子电路 +查理与巧克力工厂 +不锈钢 +苍翼默示录 +盒子 +耐力赛 +保龄球 +海啸 +舰队收藏 +死亡岛 +歌手 +电话 +感染:幸存者故事 +真人秀 +恶魔城 +五佳球 +机械 +马里奥与路易吉 +饲养员 +滑水 +龙舟 +大理石 +港片 +葫芦娃 +武装分子 +奶油烤菜 +吓人 +斧头 +正义联盟 +超凡双生 +蜜蜂 +游艇 +头骨 +道路 +神奇四侠 +弓道 +呼啦圈 +拍客 +航空母舰 +狂热节拍 +宇宙 +美景 +健身队 +武侠 +武林高手 +测评 +薄樱鬼 +人物专访 +颈椎 +皮带 +少年泰坦 +黑色 +交响乐 +震荡 +火炉 +光盘 +喝水 +守望先锋 +烹饪 +装甲车 +棒球 +网游 +黄蜂 +安全带 +泰坦 +巴掌 +指南 +复活节彩蛋 +餐馆 +樱花 +溜冰鞋 +机甲战士 +耐克 +命运石之门 +装扮 +山水画 +耀斑 +贺卡 +日本团子 +月亮 +黑人 +科普 +钥匙扣 +甜瓜 +垃圾 +美食猎人 +头巾 +无线电遥控船 +骨牌 +单挑 +上古世纪 +覆盆子 +绳子 +海绵 +超模 +香肠 +奇观 +直线加速赛 +菜园 +雨伞 +十二生肖 +奶油 +汽车修理 +大号 +倒霉熊 +音乐节目 +唇彩 +几何冲刺 +视频游戏厅 +射击 +鬼屋 +手套 +驾驶 +青蛙军曹 +鞍 +港口 +彩灯 +广播公司 +摄影 +鞋 +我的世界 +大发 +马甲线 +模式·图案 +干衣机 +机器人战斗 +人工呼吸 +华尔兹 +水族馆 +国庆 +领奖 +巫师之怒 +火影忍者 +马克杯 +战鹰 +年会 +垂钓 +摩天大楼 +炸酱面 +企鹅 +整形 +睫毛 +暴走大事件 +教程 +钢铁侠 +日出 +国家公园 +戏剧 +折纸 +花 +说唱史诗战 +白娘子 +头盔 +威浮球 +热血无赖 +眼球 +香烟 +抗战片 +小鲜肉 +音响 +武功 +场地自行车 +稻田 +真侍魂 +海战英豪 +火焰之纹章 +婚纱摄影 +发布会 +损伤 +下水道 +雕刻 +制服 +延时摄影 +凯蒂猫 +截屏 +奇幻森林 +舞台剧 +雪糕 +飞车手罗德 +我想当爷们 +肉丸 +短号 +炮兵 +孩子 +搞怪 +军事 +对决 +战神 +菜花 +欧冠 +冰壶 +蓝莓 +帐篷 +幸运星 +化妆 +激战 +方便面 +旋转木马 +人物 +磁带 +恐怖片 +梦幻龙族 +牙齿 +海滩 +猛鬼街 +鲸 +唱片公司 +露营 +松饼 +安妮 +百乐门 +圣诞 +扬琴 +棚子 +调解 +发射 +体育 +通心粉 +热可可 +二次元 +迷人 +宇航员 +运钞车 +行车记录仪 +官员 +奥数 +玉米地 +音乐人 +彗星 +颁奖典礼 +表演 +粉丝 +军人 +堂吉诃德 +狙击枪 +减脂 +古装 +游戏机 +饥饿游戏 +撒旦 +邮票 +理发店 +网络主播 +身材火辣 +棒球 +兔八哥 +大巴车 +耳环 +数码产品 +游民星空 +泰拳 +配音秀 +机器人 +盛装舞步 +玩具人 +袋鼠 +酒吧 +蘑菇 +死亡边境 +世界杯 +驾驶舱 +海藻 +乐高 +艺术 +龙之信条 +开关 +武警 +日蚀·月蚀 +手机评测 +诛仙 +行李箱 +恐龙世界 +天宫 +滑板 +青贮饲料 +摄像头 +工程车 +阀门·龙头 +石工 +孤岛惊魂 +胫骨 +砸车 +迷你人形 +超级玛丽 +生活技巧 +武打片 +胡子 +苹果 +橙色 +灾害 +猫 +翅膀 +吵架 +唱诗班 +雷神 +扑克 +史酷比 +魔龙骑士 +人体 +拾音器 +圆圈·循环 +地狱 +运球 +游轮 +疯狂动物城 +战舰 +核反应堆 +雾霾 +版画 +真正的家庭主妇 +海龟 +烘培 +电容器 +核试验 +寒潮 +垂死之光 +橡木 +游乐场 +养生 +杀手 +魔法 +台阶·门廊 +倒塌 +法院 +硬币 +拳击比赛 +弩 +可爱 +笔记本 +花卉设计 +僵尸末日 +闹钟 +调制解调器 +狗窝 +萌妹 +部落战争 +聚会 +乐器 +劫匪 +腹语 +电动工具 +头发 +地下城与勇士 +卡牌 +卡片 +别墅 +地球冒险 +暴风雪 +瑜伽 +海狸 +安检 +绘画 +沙拉 +浴缸 +毛绒玩具 +海狮 +琵琶 +肯得基 +口红 +娱乐 +魔戒 +婴儿 +烫发器 +狂飙 +积水 +机动车 +奖 +椰奶 +芦荟 +刺客 +拖拉机 +蒙娜丽莎 +牛仔 +葡萄酒 +猴子 +潜水员 +盘式制动器 +比赛 +吸尘器 +豌豆 +拍摄现场 +帆布 +喜剧演员 +蜡笔小新 +香蕉 +全民健身 +牛排 +音响系统 +啦啦队 +街头采访 +视觉小说 +弹唱 +飞车 +装甲核心 +罐头 +哈利波特 +沉香 +举重 +纸 +拼图 +电视频道 +防护 +视频游戏 +家居 +平屋顶 +开车 +航拍 +特技 +杂货店 +拍卖 +薯条 +珍珠 +手指 +柔力球 +美少女战士 +游戏公司 +冰球 +天气预报 +充气船 +爆炒 +机油 +眼泪 +西区故事 +镶嵌 +仪表着陆系统 +鱼 +爆炸 +骑马 +礼服 +植物 +战地 +淘宝 +烟花 +求婚 +饮料 +蹲 +喜剧 +猎天使魔女 +潜行者 +船员 +汽油 +低音炮 +美甲 +无花果 +超级大金刚 +猩猩 +带锯 +国旗 +开幕式 +货运工具 +腹部 +泥潭 +秀逗魔导士 +交通 +小米 +钢琴家 +机票 +肉 +姜黄 +龙腾世纪 +杀戮空间 +婴儿吊带 +拿铁 +僵尸片 +孤儿院 +自爆 +马里奥赛车 +火锅 +冬季运动 +女巫 +大厦 +街头赛车 +快板 +驾校 +秀场 +侠盗猎车手 +杂志拍摄 +乌龟 +蜂蜜 +减肥操 +水上艇筏 +象 +播种 +单词 +偷车 +玻璃贴膜 +俄罗斯方块 +惊悚 +火车头托马斯 +净水器 +电影解说 +画家 +谷类 +机枪 +滑翔翼 +瓶子 +合唱 +超胆侠 +轮盘 +电气布线 +考古 +豆类 +集装箱 +异形 +洗碗机 +割草机 +茶 +计算器 +魔方 +宝莱坞 +辣妹 +军官 +牛人 +后备箱 +海边 +电磁线圈 +印度 +红酒 +食谱 +工地 +特技飞行 +家庭剧 +培乐多 +温泉 +钩针 +宫殿 +时装 +鹦鹉 +棕熊 +运动会 +空姐 +球星卡 +葱油饼 +洛奇 +女团 +老虎机 +记者会 +体育场 +票房 +无冬城 +浣熊 +洗衣服 +菜市场 +寂静岭 +肉汁 +大力士 +鼓棒 +金属加工 +壶铃 +德云社 +国际军事 +驾照 +面条 +手枪 +金条 +泰迪熊 +河马 +洗涤 +阁楼 +爆炸袭击 +桑拿 +踢打 +爱探险的朵拉 +葡萄园 +闪光 +妈妈 +骨头 +钓竿 +颜色 +摩托车头盔 +纱线 +驯鹿 +银魂 +独轮车 +虚拟玩家角色 +圣经 +毛笔字 +电影 +音乐影片 +西餐 +菠萝 +西湖 +清洁剂 +斗牛 +小红帽 +餐巾 +单杠 +地球 +爽肤水 +打印机 +吹风机 +记号笔 +小麦 +螺帽 +乐高都市 +白酒 +显卡 +都市 +画展 +光之美少女 +银行卡 +群星 +穿越火线 +古装剧 +单簧管 +网络 +洪水 +美容 +汤姆猫 +讲故事 +海底世界 +操作杆 +赛车方向盘 +倚天 +球赛 +海岸 +空调 +铁路 +怪物卡车大毁灭 +下巴 +票 +复仇者联盟 +新闻 +雪崩 +彩绘 +狂野飙车 +沙雕 +木偶 +轮椅 +文艺 +家电公司 +海岛 +苹果派 +降龙十八掌 +打结 +素食 +深渊传说 +骑士 +视频解说 +活塞 +小猪佩奇 +直播 +蟋蟀 +乘客 +英雄联盟 +大气污染 +硬石餐厅 +晶体管 +宝石 +奶酪 +图表 +鲜花 +背心 +反恐 +科学家 +种子 +喂食 +爪子 +火线精英 +体育用品 +照片 +军事武器 +直线 +电脑硬件 +开锁 +鼓手 +模型车 +航天器 +屏幕 +花生 +直排轮滑鞋 +军舰 +钻石 +橄榄油 +稻草 +蜡笔 +妆容 +杀手本能 +餐厅 +摔跤 +内裤 +蹦床 +樱兰高校男公关部 +跆拳道 +科幻 +豪宅 +停车 +冰淇淋 +钢盘·平底深锅 +大乱斗 +服装店 +千与千寻 +音标 +吉他英雄 +南瓜 +采访 +小吃 +漫画英雄 +最后生还者 +红薯 +镜之边缘 +燃脂 +葫芦丝 +篮球 +组装 +台球杆 +过滤器 +空翻 +壁画 +闪电 +海域 +红唇 +面试 +吊坠 +武侠剧 +睫毛膏 +香水 +舞蹈室 +资讯 +眼影 +军装 +躺骑车 +白色 +英魂之刃 +魔鬼 +饭团 +琴弦 +冰箱 +通灵王 +公交 +魔法之战 +泳装 +文本 +长号 +羊毛 +古诗 +马克思佩恩 +演习 +陀螺仪 +车牌 +静物写生 +木屋 +米饭 +萝卜 +高尔夫球 +散热器 +直播间 +星球大战 +黄金 +果汁 +疯狂橄榄球 +散打 +犰狳 +爱情故事 +决斗 +电动汽车 +缝纫 +餐饮 +魔兽世界 +设计师 +航班 +麻薯 +以撒的结合 +中提琴 +孢子 +说唱 +死神 +迷宫 +战斗 +警长 +手球 +睡袋 +镲片 +城堡 +性感 +酒精 +生化模式 +湖 +黑暗 +小小世界 +户外休闲 +球技 +同步带 +制动 +剧情片 +球鞋 +清纯 +聚餐 +刺绣 +减肥 +对唱 +睡美人 +儿童 +烤箱 +黄色 +干草 +神灵 +航空公司 +元素周期表 +电影院 +女神转生 +字典 +飞镖 +战锤 +失忆症 +死亡笔记 +亚马逊公司 +虐杀原形 +象棋 +虚幻引擎 +烧烤架 +奶粉 +悉尼歌剧院 +伐木 +草莓 +爆破 +忍者神龟 +银 +四轮车 +鬼泣 +娱乐八卦 +浴室 +鸡肉 +胡萝卜 +胎儿 +液体 +收割机 +铜 +玩具世界 +一字马 +飞船 +修剪器 +煤炭 +简笔图 +网剧 +小品 +洋葱 +便当 +百事 +蜘蛛 +警车 +马车 +尼姑 +河流 +斗牛士 +染色 +黄瓜 +跳水 +音乐大师课 +蜗牛 +钢笔 +故宫 +公益片 +渔船 +蓝色 +卷发器 +超级快递 +鞭炮 +珊瑚 +实战 +跳绳 +滑冰 +小行星 +翻车 +博物馆 +欧元 +哆啦A梦 +乐乐天使娃娃 +空难 +阴阳师 +辣椒 +青之驱魔师 +鸿雁 +SaGa +凝胶 +池塘 +节拍器 +亲子节目 +播放机 +打印 +歌迷 +荒野星球 +农业 +地震 +时政 +吴哥窟 +拉面 +音乐节 +甜甜圈 +藤球 +灾难意外 +骑马与砍杀 +柑橘 +不明飞行物 +软管 +相册 +触摸屏 +飞行表演 +圣杯神器 +紫色 +笛子 +存储卡 +鸽赛 +蔬菜 +山地自行车 +哑剧大师 +双簧 +长椅 +松弛熊 +官兵 +巧克力 +动画 +侦探 +溜冰 +拉链 +警察局 +工程师 +分屏 +牧师 +球拍 +馅饼 +马展 +蜡烛 +游戏 +舌头 +增压器 +泰拉瑞亚 +三国 +污染 +管带夹 +丫鬟 +歌剧魅影 +温室 +八卦 +晚会 +多米诺骨牌 +西瓜 +无主之地 +薯片 +降落伞 +家具装饰 +螃蟹 +模拟山羊 +麦当劳 +传感器 +粉扑 +太阳能 +裁判 +保卫萝卜 +地铁 +松鼠 +猫女 +课堂 +木星 +耳机 +耳朵 +医学 +尼尔机械纪元 +驾驶证 +婚车 +砂锅 +死海 +海绵宝宝 +模拟农场 +警官 +调酒 +龙战士 +动车 +老鼠 +辛普森一家 +蜥蜴 +和服 +女生 +影视混剪 +长毛绒 +广告牌 +撒娇 +炒锅 +萌宝 +自然 +指甲油 +灰泥 +火腿 +桌子 +月姬格斗 +塑料 +大脑 +接线盒 +攀岩 +水果忍者 +货币 +秋千 +销售 +卷轴 +化妆品 +包裹 +斑马线 +面包超人 +蛋糕 +肉桂 +寺庙 +书法 +团队套牛 +仙人掌 +餐饮 +火箭炮 +视频直播 +鬼娃回魂 +画线骑士 +宜家 +春晚 +步行 +日落 +袋子 +击剑 +理发 +地下室 +斗地主 +打针 +喝酒 +喷漆 +柯南时代 +锦鲤 +凝乳 +杀戮地带 +恶霸鲁尼 +奖牌 +猫头鹰 +赛道 +战士 +美照 +购物 +蝴蝶 +字母表 +客厅 +乌鸦 +唢呐 +反串 +潘多拉 +监控 +烤鸭 +明星大乱斗 +葡萄 +飓风 +病人 +吊车 +蝙蝠 +伪装 +益智玩具 +舞蹈 +合金装备 +跳楼 +勇者斗恶龙 +油 +网站 +厨师机 +凯恩的遗产 +钱 +食材 +外交部 +酒厂 +显示器 +主持 +羽绒服 +牛仔布 +车模 +盐 +芝麻 +痘痘 +股票 +微笑 +菜单 +地板 +烤鸡 +自动唱机 +雪貂 +涡轮 +扎染 +歌剧 +变形金刚 +失火 +门票 +雪山 +风筝 +长袍·礼服 +书柜 +家庭教师 +死亡之屋 +DarkOrbit +粮食 +公益活动 +藏獒 +渔民 +下一站巨星 +彩虹手环 +苦瓜 +冲浪 +卷心菜 +珠饰 +西贡小姐 +地铁酷跑 +训练营 +运输 +磁铁 +健康 +床垫 +摇摆 +街头恶搞 +糕点 +拳王 +肋骨 +猫 +曲艺 +加油站 +凉宫春日 +妖怪手表 +动力伞 +墓地 +工程 +民房 +胶片 +色带 +主教 +樱桃小丸子 +鸡翅 +轮子 +牛 +邻里 +萌 +音乐制作 +洛克人 +芒果 +地图 +劈木机 +勇士 +火锅店 +电梯 +吻 +弹球盘 +三角形 +粘土 +鸡尾酒 +慈善 +天天酷跑 +唱片骑师 +结婚 +家庭 +手机壳 +航线 +职业摔跤 +肥皂 +竞技场 +丧钟 +摩天轮 +天使 +台面 +外汇市场 +肉搏 +求生之路 +铜牌 +泡面 +流亡黯道 +灯笼 +谜题 +婴儿室 +捕猎 +尿布袋 +鱼鹰 +雪犁 +方块世界 +斑鸠 +建筑 +电视剧 +堆肥 +细胞 +邪恶力量 +零食 +湾岸竞速 +太鼓达人 +赛车 +金枪鱼 +司令 +皮肤 +马拉松 +末日 +垒球 +涂鸦 +充气城堡 +十字架 +食疗 +早教 +速叠杯 +纸牌 +披肩 +躲避球 +柠檬 +打牌 +抗战 +绕口令 +美容院 +惠普 +情感节目 +永恒之塔 +电脑鼠标 +虚拟现实 +特警 +吊床 +货车 +飞绑 +可乐 +运动 +双重国度 +多功能工具 +妹子 +农村 +眼睛 +干冰 +果冻 +相声小品 +电线杆 +战友 +影视配音 +孤岛生存大乱斗 +奥运 +沃尔玛 +太空 +星际之门 +装饰 +灰色 +樱桃 +电锯 +手铃 +科幻片 +身份证 +古墓 +乒乓 +溪流 +手链 +野外生存 +天线 +玻璃 +营地 +庆典 +玩具 +袭击事件 +美术 +橡皮 +加农 +镜头 +探测器 +洗发精 +彩虹岛 +武器 +装置艺术 +葱 +护理 +命运 +仓鼠 +碎石 +青蛙科密特 +螺旋桨 +七日杀 +整容 +行星 +小宝宝 +科技 +台风 +勇者前线 +皇家国教骑士团 +狂欢节 +热狗 +捉迷藏 +弦乐琴 +叶子 +床 +彼得潘 +写真 +托儿所 +设备 +冰桶挑战 +萌物 +变色龙 +花瓣 +伴郎 +打戏 +画报 +罪恶装备 +漫画 +瘫痪 +飞机失事 +奇闻趣事 +大选 +花瓶 +钢之炼金术师 +杂志 +鼠型车 +教育 +旺达与巨像 +插花 +城堡破坏者 +泵 +混音带 +字体 +超人 +倒计时 +恶作剧 +鹌鹑 +吸血鬼 +小朋友 +颤音琴 +符号 +调音台 +梦幻之星 +橘子 +奶昔 +面糊 +冬不拉 +北斗神拳 +越野 +灭火器 +水果 +婚纱 +上古卷轴 +007 +暮光之城 +蜘蛛侠 +冰沙 +下坡 +毡 +警察 +超市特工 +外套 +汉服 +女童 +筏流 +花园 +布丁 +花圈 +生菜 +新年 +清雪机 +气雾喷雾器 +暮蝉悲鸣时 +公主 +显微镜 +秋天 +模特 +收藏品 +咖喱 +空气净化器 +漫威宇宙 +混凝土 +育儿 +电子琴 +遮瑕膏 +火车 +芭比娃娃 +爵士 +音箱 +黑洞 +积木 +剑球 +奶爸 +监管 +美国队长 +爆笑 +闪电 +降世神通 +祷告 +家禽 +穿越时空 +分裂 +轮胎 +水坝 +索尼 +战斗机 +恶搞路人 +拍戏 +电池 +爆胎 +光棍 +俯卧撑 +摩斯 +饮用水 +狂热 +阅读器 +训练 +奥特曼 +王国之心 +学车 +快递员 +住宅 +袋狼大冒险 +悟空 +面包 +雷曼疯狂兔子 +杀手 +赛马 +啄木鸟伍迪 +国务院 +拖把 +壁虎 +铁拳 +高跟鞋 +动物园 +唱片 +金鹰节 +棒球公园 +宠物小精灵 +手游 +部落冲突 +兽人 +魔术师 +谷仓 +圣剑传说 +商场 +起火 +内饰 +暴龙 +鲸 +上课 +油画 +剧本 +武士 +村庄 +脖子 +卷饼 +蚊子 +狩猎 +保健品 +红毯 +总统 +塔罗牌 +偶像活动 +涂层 +合金弹头 +黑白 +沙漠 +白头鹰 +芝士 +宅男 +战利品 +军营 +围棋 +洗衣店 +教育部 +模糊 +国画 +菲比娃娃 +雕塑 +施工 +书呆子 +冬季 +F-Zero +核桃 +狱警 +游戏人物 +旗袍 +笑话 +衣柜 +综艺 +迫击炮 +梨 +圣斗士 +媒体 +辩论 +健美操 +速降 +男团 +杀人 +圣诞老人 +圆顶 +海豚音 +特技表演 +耙 +探索 +僵尸围城 +银河战士 +长城 +雪人 +作画 +狼 +星际争霸 +立方体 +武装·装备 +被子 +自行车赛 +吃东西 +金属 +交易 +铲屎官 +培根 +档案 +飞去来器 +歌舞表演 +报纸 +仙女 +舞蹈中心 +亚瑟王传奇 +浏览器 +钟 +狗 +露营车 +艺术品 +洗衣机 +睡姿 +打野 +西装 +管风琴 +半机械人 +U型场地 +光 +鸽子 +窗帘 +练习生 +刺客信条 +黑道圣徒 +农民 +煤气灶 +播放器 +塞尔达传说 +消防 +黄铜 +胶带 +挡泥板 +越战越勇 +糖浆 +武装部队 +录像带 +倒车 +牛奶 +冰棍 +阳台 +饮品 +番茄 +灵异事件 +屋顶 +角色扮演 +大富翁 +饿狼传说 +玫瑰 +猪 +海马 +防汛抗洪 +水井 +书 +土地 +村长 +权力的游戏 +东方妖妖梦 +半条命 +国家队 +木瓜 +绿箭 +滑翔 +视频艺术 +人猿泰山 +国防部 +报警装置 +吉尼斯 +厢型布景 +突袭 +狐狸 +倒立 +搅拌机 +腹肌 +飙酷车神 +电子键盘 +惩罚 +失落的星球 +乐队 +丝绸 +冲突 +豆芽 +交通工具 +滑翔机 +亲子 +拳击手 +少儿 +厨房 +花栗鼠 +楼市 +卡通城 +夜店 +洗车 +广告 +饭店 +合气道 +雪地车 +留声机 +全民枪战 +毛皮 +迷你四驱车 +钻头 +生活常识 +少林 +校园 +拔河 +事故 +菊花 +小蛮腰 +过山车 +鸡腿 +暗黑破坏神 +炸鸡 +排版 +拼贴画 +制造业 +艺人 +选美 +猛兽 +英语 +手 +酥皮 +运动员 +卡士达酱 +内衣秀 +护照 +民航 +土匪 +监狱 +靴子 +积雪草 +沙发 +加勒比海盗 +咱们穿越吧 +极度恐慌 +拉力赛 +背部 +伴娘 +投影机 +面膜 +水 +玉·翡翠 +易拉罐 +度假村 +益智 +吻戏 +丈夫 +吊扇 +模具 +水泥 +火柴人 +公安部 +泥土 +地铁站 +打火机 +小小宠物店 +橙子 +子弹 +猴子岛 +闪电十一人 +雪碧 +指甲 +摩托车 +摄影师 +角色 +电人 +老虎 +音乐合奏 +塑料瓶 +发带 +标签·商标 +肉排 +桃子 +指板 +狼人 +分解动作 +读书 +志愿者 +灵魂能力 +星际宝贝 diff --git a/applications/VideoTag/metrics/__init__.py b/applications/VideoTag/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0d1df762bdf3d3b920fc1e00d15a3a2ecdcdbe55 --- /dev/null +++ b/applications/VideoTag/metrics/__init__.py @@ -0,0 +1 @@ +from .metrics_util import get_metrics diff --git a/applications/VideoTag/metrics/kinetics/__init__.py b/applications/VideoTag/metrics/kinetics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/VideoTag/metrics/kinetics/accuracy_metrics.py b/applications/VideoTag/metrics/kinetics/accuracy_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..fd187db5b04d4865ed8a5f65b665ae317dc0fe96 --- /dev/null +++ b/applications/VideoTag/metrics/kinetics/accuracy_metrics.py @@ -0,0 +1,107 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +from __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division + +import numpy as np +import datetime +import logging + +logger = logging.getLogger(__name__) + + +class MetricsCalculator(): + def __init__(self, name, mode): + self.name = name + self.mode = mode # 'train', 'val', 'test' + self.reset() + + def reset(self): + logger.info('Resetting {} metrics...'.format(self.mode)) + self.aggr_acc1 = 0.0 + self.aggr_acc5 = 0.0 + self.aggr_loss = 0.0 + self.aggr_batch_size = 0 + + def finalize_metrics(self): + self.avg_acc1 = self.aggr_acc1 / self.aggr_batch_size + self.avg_acc5 = self.aggr_acc5 / self.aggr_batch_size + self.avg_loss = self.aggr_loss / self.aggr_batch_size + + def get_computed_metrics(self): + json_stats = {} + json_stats['avg_loss'] = self.avg_loss + json_stats['avg_acc1'] = self.avg_acc1 + json_stats['avg_acc5'] = self.avg_acc5 + return json_stats + + def calculate_metrics(self, loss, softmax, labels): + accuracy1 = compute_topk_accuracy(softmax, labels, top_k=1) * 100. + accuracy5 = compute_topk_accuracy(softmax, labels, top_k=5) * 100. + return accuracy1, accuracy5 + + def accumulate(self, loss, softmax, labels): + cur_batch_size = softmax.shape[0] + # if returned loss is None for e.g. test, just set loss to be 0. + if loss is None: + cur_loss = 0. + else: + cur_loss = np.mean(np.array(loss)) # + self.aggr_batch_size += cur_batch_size + self.aggr_loss += cur_loss * cur_batch_size + + accuracy1 = compute_topk_accuracy(softmax, labels, top_k=1) * 100. + accuracy5 = compute_topk_accuracy(softmax, labels, top_k=5) * 100. + self.aggr_acc1 += accuracy1 * cur_batch_size + self.aggr_acc5 += accuracy5 * cur_batch_size + + return + + +# ---------------------------------------------- +# other utils +# ---------------------------------------------- +def compute_topk_correct_hits(top_k, preds, labels): + '''Compute the number of corret hits''' + batch_size = preds.shape[0] + + top_k_preds = np.zeros((batch_size, top_k), dtype=np.float32) + for i in range(batch_size): + top_k_preds[i, :] = np.argsort(-preds[i, :])[:top_k] + + correctness = np.zeros(batch_size, dtype=np.int32) + for i in range(batch_size): + if labels[i] in top_k_preds[i, :].astype(np.int32).tolist(): + correctness[i] = 1 + correct_hits = sum(correctness) + + return correct_hits + + +def compute_topk_accuracy(softmax, labels, top_k): + + computed_metrics = {} + + assert labels.shape[0] == softmax.shape[0], "Batch size mismatch." + aggr_batch_size = labels.shape[0] + aggr_top_k_correct_hits = compute_topk_correct_hits(top_k, softmax, labels) + + # normalize results + computed_metrics = \ + float(aggr_top_k_correct_hits) / aggr_batch_size + + return computed_metrics diff --git a/applications/VideoTag/metrics/metrics_util.py b/applications/VideoTag/metrics/metrics_util.py new file mode 100644 index 0000000000000000000000000000000000000000..2e264ed93027452313d3c56cb7888f7fbf2a45d8 --- /dev/null +++ b/applications/VideoTag/metrics/metrics_util.py @@ -0,0 +1,279 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +from __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division + +import logging + +import os +import io +import numpy as np +import json +from metrics.youtube8m import eval_util as youtube8m_metrics +from metrics.kinetics import accuracy_metrics as kinetics_metrics + +logger = logging.getLogger(__name__) + + +class Metrics(object): + def __init__(self, name, mode, metrics_args): + """Not implemented""" + pass + + def calculate_and_log_out(self, fetch_list, info=''): + """Not implemented""" + pass + + def accumulate(self, fetch_list, info=''): + """Not implemented""" + pass + + def finalize_and_log_out(self, info='', savedir='./'): + """Not implemented""" + pass + + def reset(self): + """Not implemented""" + pass + + +class Youtube8mMetrics(Metrics): + def __init__(self, name, mode, metrics_args): + self.name = name + self.mode = mode + self.num_classes = metrics_args['MODEL']['num_classes'] + self.topk = metrics_args['MODEL']['topk'] + self.calculator = youtube8m_metrics.EvaluationMetrics( + self.num_classes, self.topk) + if self.mode == 'infer': + self.infer_results = [] + + def calculate_and_log_out(self, fetch_list, info=''): + loss = np.mean(np.array(fetch_list[0])) + pred = np.array(fetch_list[1]) + label = np.array(fetch_list[2]) + hit_at_one = youtube8m_metrics.calculate_hit_at_one(pred, label) + perr = youtube8m_metrics.calculate_precision_at_equal_recall_rate( + pred, label) + gap = youtube8m_metrics.calculate_gap(pred, label) + logger.info(info + ' , loss = {0}, Hit@1 = {1}, PERR = {2}, GAP = {3}'.format(\ + '%.6f' % loss, '%.2f' % hit_at_one, '%.2f' % perr, '%.2f' % gap)) + + def accumulate(self, fetch_list, info=''): + if self.mode == 'infer': + predictions = np.array(fetch_list[0]) + video_id = fetch_list[1] + for i in range(len(predictions)): + topk_inds = predictions[i].argsort()[0 - self.topk:] + topk_inds = topk_inds[::-1] + preds = predictions[i][topk_inds] + self.infer_results.append( + (video_id[i], topk_inds.tolist(), preds.tolist())) + else: + loss = np.array(fetch_list[0]) + pred = np.array(fetch_list[1]) + label = np.array(fetch_list[2]) + self.calculator.accumulate(loss, pred, label) + + def finalize_and_log_out(self, + info='', + savedir='./data/results', + label_file='./label_3396.txt'): + if self.mode == 'infer': + for index, item in enumerate(self.infer_results): + video_id = item[0] + print('[========video_id [ {} ] , topk({}) preds: ========]\n'. + format(video_id, self.topk)) + + f = io.open(label_file, "r", encoding="utf-8") + fl = f.readlines() + res_list = [] + res_list.append(video_id) + for i in range(len(item[1])): + class_id = item[1][i] + class_prob = item[2][i] + class_name = fl[class_id].split('\n')[0] + print('class_id: {},'.format(class_id), 'class_name:', + class_name, + ', probability: {} \n'.format(class_prob)) + save_dict = { + "'class_id": class_id, + "class_name": class_name, + "probability": class_prob + } + res_list.append(save_dict) + + # save infer result into output dir + with io.open(os.path.join(savedir, + 'result' + str(index) + '.json'), + 'w', + encoding='utf-8') as f: + f.write(json.dumps(res_list, ensure_ascii=False)) + else: + epoch_info_dict = self.calculator.get() + logger.info(info + '\tavg_hit_at_one: {0},\tavg_perr: {1},\tavg_loss :{2},\taps: {3},\tgap:{4}'\ + .format(epoch_info_dict['avg_hit_at_one'], epoch_info_dict['avg_perr'], \ + epoch_info_dict['avg_loss'], epoch_info_dict['aps'], epoch_info_dict['gap'])) + + def reset(self): + self.calculator.clear() + if self.mode == 'infer': + self.infer_results = [] + + +class Kinetics400Metrics(Metrics): + def __init__(self, name, mode, metrics_args): + self.name = name + self.mode = mode + self.topk = metrics_args['MODEL']['topk'] + self.calculator = kinetics_metrics.MetricsCalculator(name, mode.lower()) + if self.mode == 'infer': + self.infer_results = [] + + def calculate_and_log_out(self, fetch_list, info=''): + if len(fetch_list) == 3: + loss = fetch_list[0] + loss = np.mean(np.array(loss)) + pred = np.array(fetch_list[1]) + label = np.array(fetch_list[2]) + else: + loss = 0. + pred = np.array(fetch_list[0]) + label = np.array(fetch_list[1]) + acc1, acc5 = self.calculator.calculate_metrics(loss, pred, label) + logger.info(info + '\tLoss: {},\ttop1_acc: {}, \ttop5_acc: {}'.format('%.6f' % loss, \ + '%.2f' % acc1, '%.2f' % acc5)) + return loss + + def accumulate(self, fetch_list, info=''): + if self.mode == 'infer': + predictions = np.array(fetch_list[0]) + video_id = fetch_list[1] + for i in range(len(predictions)): + topk_inds = predictions[i].argsort()[0 - self.topk:] + topk_inds = topk_inds[::-1] + preds = predictions[i][topk_inds] + self.infer_results.append( + (video_id[i], topk_inds.tolist(), preds.tolist())) + else: + if len(fetch_list) == 3: + loss = fetch_list[0] + loss = np.mean(np.array(loss)) + pred = np.array(fetch_list[1]) + label = np.array(fetch_list[2]) + else: + loss = 0. + pred = np.array(fetch_list[0]) + label = np.array(fetch_list[1]) + self.calculator.accumulate(loss, pred, label) + + def finalize_and_log_out(self, + info='', + savedir='./data/results', + label_file='./label_3396.txt'): + if self.mode == 'infer': + for index, item in enumerate(self.infer_results): + video_id = item[0] + print('[========video_id [ {} ] , topk({}) preds: ========]\n'. + format(video_id, self.topk)) + + f = io.open(label_file, "r", encoding="utf-8") + fl = f.readlines() + res_list = [] + res_list.append(video_id) + for i in range(len(item[1])): + class_id = item[1][i] + class_prob = item[2][i] + class_name = fl[class_id].split('\n')[0] + print('class_id: {},'.format(class_id), 'class_name:', + class_name, + ', probability: {} \n'.format(class_prob)) + save_dict = { + "'class_id": class_id, + "class_name": class_name, + "probability": class_prob + } + res_list.append(save_dict) + + # save infer result into output dir + with io.open(os.path.join(savedir, + 'result' + str(index) + '.json'), + 'w', + encoding='utf-8') as f: + f.write(json.dumps(res_list, ensure_ascii=False)) + else: + self.calculator.finalize_metrics() + metrics_dict = self.calculator.get_computed_metrics() + loss = metrics_dict['avg_loss'] + acc1 = metrics_dict['avg_acc1'] + acc5 = metrics_dict['avg_acc5'] + logger.info(info + '\tLoss: {},\ttop1_acc: {}, \ttop5_acc: {}'.format('%.6f' % loss, \ + '%.2f' % acc1, '%.2f' % acc5)) + + def reset(self): + self.calculator.reset() + if self.mode == 'infer': + self.infer_results = [] + + +class MetricsNotFoundError(Exception): + "Error: metrics not found" + + def __init__(self, metrics_name, avail_metrics): + super(MetricsNotFoundError, self).__init__() + self.metrics_name = metrics_name + self.avail_metrics = avail_metrics + + def __str__(self): + msg = "Metrics {} Not Found.\nAvailiable metrics:\n".format( + self.metrics_name) + for metric in self.avail_metrics: + msg += " {}\n".format(metric) + return msg + + +class MetricsZoo(object): + def __init__(self): + self.metrics_zoo = {} + + def regist(self, name, metrics): + assert metrics.__base__ == Metrics, "Unknow model type {}".format( + type(metrics)) + self.metrics_zoo[name] = metrics + + def get(self, name, mode, cfg): + for k, v in self.metrics_zoo.items(): + if k == name: + return v(name, mode, cfg) + raise MetricsNotFoundError(name, self.metrics_zoo.keys()) + + +# singleton metrics_zoo +metrics_zoo = MetricsZoo() + + +def regist_metrics(name, metrics): + metrics_zoo.regist(name, metrics) + + +def get_metrics(name, mode, cfg): + return metrics_zoo.get(name, mode, cfg) + + +# sort by alphabet +regist_metrics("ATTENTIONLSTM", Youtube8mMetrics) +regist_metrics("TSN", Kinetics400Metrics) diff --git a/applications/VideoTag/metrics/youtube8m/__init__.py b/applications/VideoTag/metrics/youtube8m/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/VideoTag/metrics/youtube8m/average_precision_calculator.py b/applications/VideoTag/metrics/youtube8m/average_precision_calculator.py new file mode 100644 index 0000000000000000000000000000000000000000..5e8a71cae6b7fe83745f4462e5104f5933178734 --- /dev/null +++ b/applications/VideoTag/metrics/youtube8m/average_precision_calculator.py @@ -0,0 +1,274 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Calculate or keep track of the interpolated average precision. + +It provides an interface for calculating interpolated average precision for an +entire list or the top-n ranked items. For the definition of the +(non-)interpolated average precision: +http://trec.nist.gov/pubs/trec15/appendices/CE.MEASURES06.pdf + +Example usages: +1) Use it as a static function call to directly calculate average precision for +a short ranked list in the memory. + +``` +import random + +p = np.array([random.random() for _ in xrange(10)]) +a = np.array([random.choice([0, 1]) for _ in xrange(10)]) + +ap = average_precision_calculator.AveragePrecisionCalculator.ap(p, a) +``` + +2) Use it as an object for long ranked list that cannot be stored in memory or +the case where partial predictions can be observed at a time (Tensorflow +predictions). In this case, we first call the function accumulate many times +to process parts of the ranked list. After processing all the parts, we call +peek_interpolated_ap_at_n. +``` +p1 = np.array([random.random() for _ in xrange(5)]) +a1 = np.array([random.choice([0, 1]) for _ in xrange(5)]) +p2 = np.array([random.random() for _ in xrange(5)]) +a2 = np.array([random.choice([0, 1]) for _ in xrange(5)]) + +# interpolated average precision at 10 using 1000 break points +calculator = average_precision_calculator.AveragePrecisionCalculator(10) +calculator.accumulate(p1, a1) +calculator.accumulate(p2, a2) +ap3 = calculator.peek_ap_at_n() +``` +""" + +import heapq +import random +import numbers + +import numpy + + +class AveragePrecisionCalculator(object): + """Calculate the average precision and average precision at n.""" + def __init__(self, top_n=None): + """Construct an AveragePrecisionCalculator to calculate average precision. + + This class is used to calculate the average precision for a single label. + + Args: + top_n: A positive Integer specifying the average precision at n, or + None to use all provided data points. + + Raises: + ValueError: An error occurred when the top_n is not a positive integer. + """ + if not ((isinstance(top_n, int) and top_n >= 0) or top_n is None): + raise ValueError("top_n must be a positive integer or None.") + + self._top_n = top_n # average precision at n + self._total_positives = 0 # total number of positives have seen + self._heap = [] # max heap of (prediction, actual) + + @property + def heap_size(self): + """Gets the heap size maintained in the class.""" + return len(self._heap) + + @property + def num_accumulated_positives(self): + """Gets the number of positive samples that have been accumulated.""" + return self._total_positives + + def accumulate(self, predictions, actuals, num_positives=None): + """Accumulate the predictions and their ground truth labels. + + After the function call, we may call peek_ap_at_n to actually calculate + the average precision. + Note predictions and actuals must have the same shape. + + Args: + predictions: a list storing the prediction scores. + actuals: a list storing the ground truth labels. Any value + larger than 0 will be treated as positives, otherwise as negatives. + num_positives = If the 'predictions' and 'actuals' inputs aren't complete, + then it's possible some true positives were missed in them. In that case, + you can provide 'num_positives' in order to accurately track recall. + + Raises: + ValueError: An error occurred when the format of the input is not the + numpy 1-D array or the shape of predictions and actuals does not match. + """ + if len(predictions) != len(actuals): + raise ValueError( + "the shape of predictions and actuals does not match.") + + if not num_positives is None: + if not isinstance(num_positives, + numbers.Number) or num_positives < 0: + raise ValueError( + "'num_positives' was provided but it wan't a nonzero number." + ) + + if not num_positives is None: + self._total_positives += num_positives + else: + self._total_positives += numpy.size(numpy.where(actuals > 0)) + topk = self._top_n + heap = self._heap + + for i in range(numpy.size(predictions)): + if topk is None or len(heap) < topk: + heapq.heappush(heap, (predictions[i], actuals[i])) + else: + if predictions[i] > heap[0][0]: # heap[0] is the smallest + heapq.heappop(heap) + heapq.heappush(heap, (predictions[i], actuals[i])) + + def clear(self): + """Clear the accumulated predictions.""" + self._heap = [] + self._total_positives = 0 + + def peek_ap_at_n(self): + """Peek the non-interpolated average precision at n. + + Returns: + The non-interpolated average precision at n (default 0). + If n is larger than the length of the ranked list, + the average precision will be returned. + """ + if self.heap_size <= 0: + return 0 + predlists = numpy.array(list(zip(*self._heap))) + + ap = self.ap_at_n(predlists[0], + predlists[1], + n=self._top_n, + total_num_positives=self._total_positives) + return ap + + @staticmethod + def ap(predictions, actuals): + """Calculate the non-interpolated average precision. + + Args: + predictions: a numpy 1-D array storing the sparse prediction scores. + actuals: a numpy 1-D array storing the ground truth labels. Any value + larger than 0 will be treated as positives, otherwise as negatives. + + Returns: + The non-interpolated average precision at n. + If n is larger than the length of the ranked list, + the average precision will be returned. + + Raises: + ValueError: An error occurred when the format of the input is not the + numpy 1-D array or the shape of predictions and actuals does not match. + """ + return AveragePrecisionCalculator.ap_at_n(predictions, actuals, n=None) + + @staticmethod + def ap_at_n(predictions, actuals, n=20, total_num_positives=None): + """Calculate the non-interpolated average precision. + + Args: + predictions: a numpy 1-D array storing the sparse prediction scores. + actuals: a numpy 1-D array storing the ground truth labels. Any value + larger than 0 will be treated as positives, otherwise as negatives. + n: the top n items to be considered in ap@n. + total_num_positives : (optionally) you can specify the number of total + positive + in the list. If specified, it will be used in calculation. + + Returns: + The non-interpolated average precision at n. + If n is larger than the length of the ranked list, + the average precision will be returned. + + Raises: + ValueError: An error occurred when + 1) the format of the input is not the numpy 1-D array; + 2) the shape of predictions and actuals does not match; + 3) the input n is not a positive integer. + """ + if len(predictions) != len(actuals): + raise ValueError( + "the shape of predictions and actuals does not match.") + + if n is not None: + if not isinstance(n, int) or n <= 0: + raise ValueError("n must be 'None' or a positive integer." + " It was '%s'." % n) + + ap = 0.0 + + predictions = numpy.array(predictions) + actuals = numpy.array(actuals) + + # add a shuffler to avoid overestimating the ap + predictions, actuals = AveragePrecisionCalculator._shuffle( + predictions, actuals) + sortidx = sorted(range(len(predictions)), + key=lambda k: predictions[k], + reverse=True) + + if total_num_positives is None: + numpos = numpy.size(numpy.where(actuals > 0)) + else: + numpos = total_num_positives + + if numpos == 0: + return 0 + + if n is not None: + numpos = min(numpos, n) + delta_recall = 1.0 / numpos + poscount = 0.0 + + # calculate the ap + r = len(sortidx) + if n is not None: + r = min(r, n) + for i in range(r): + if actuals[sortidx[i]] > 0: + poscount += 1 + ap += poscount / (i + 1) * delta_recall + return ap + + @staticmethod + def _shuffle(predictions, actuals): + random.seed(0) + suffidx = random.sample(range(len(predictions)), len(predictions)) + predictions = predictions[suffidx] + actuals = actuals[suffidx] + return predictions, actuals + + @staticmethod + def _zero_one_normalize(predictions, epsilon=1e-7): + """Normalize the predictions to the range between 0.0 and 1.0. + + For some predictions like SVM predictions, we need to normalize them before + calculate the interpolated average precision. The normalization will not + change the rank in the original list and thus won't change the average + precision. + + Args: + predictions: a numpy 1-D array storing the sparse prediction scores. + epsilon: a small constant to avoid denominator being zero. + + Returns: + The normalized prediction. + """ + denominator = numpy.max(predictions) - numpy.min(predictions) + ret = (predictions - numpy.min(predictions)) / numpy.max( + denominator, epsilon) + return ret diff --git a/applications/VideoTag/metrics/youtube8m/eval_util.py b/applications/VideoTag/metrics/youtube8m/eval_util.py new file mode 100644 index 0000000000000000000000000000000000000000..5a78d1f3b08bf8701588e5cec8bf5aff20052c64 --- /dev/null +++ b/applications/VideoTag/metrics/youtube8m/eval_util.py @@ -0,0 +1,244 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Provides functions to help with evaluating models.""" +import datetime +import numpy + +from . import mean_average_precision_calculator as map_calculator +from . import average_precision_calculator as ap_calculator + + +def flatten(l): + """ Merges a list of lists into a single list. """ + return [item for sublist in l for item in sublist] + + +def calculate_hit_at_one(predictions, actuals): + """Performs a local (numpy) calculation of the hit at one. + + Args: + predictions: Matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + actuals: Matrix containing the ground truth labels. + Dimensions are 'batch' x 'num_classes'. + + Returns: + float: The average hit at one across the entire batch. + """ + top_prediction = numpy.argmax(predictions, 1) + hits = actuals[numpy.arange(actuals.shape[0]), top_prediction] + return numpy.average(hits) + + +def calculate_precision_at_equal_recall_rate(predictions, actuals): + """Performs a local (numpy) calculation of the PERR. + + Args: + predictions: Matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + actuals: Matrix containing the ground truth labels. + Dimensions are 'batch' x 'num_classes'. + + Returns: + float: The average precision at equal recall rate across the entire batch. + """ + aggregated_precision = 0.0 + num_videos = actuals.shape[0] + for row in numpy.arange(num_videos): + num_labels = int(numpy.sum(actuals[row])) + top_indices = numpy.argpartition(predictions[row], + -num_labels)[-num_labels:] + item_precision = 0.0 + for label_index in top_indices: + if predictions[row][label_index] > 0: + item_precision += actuals[row][label_index] + item_precision /= top_indices.size + aggregated_precision += item_precision + aggregated_precision /= num_videos + return aggregated_precision + + +def calculate_gap(predictions, actuals, top_k=20): + """Performs a local (numpy) calculation of the global average precision. + + Only the top_k predictions are taken for each of the videos. + + Args: + predictions: Matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + actuals: Matrix containing the ground truth labels. + Dimensions are 'batch' x 'num_classes'. + top_k: How many predictions to use per video. + + Returns: + float: The global average precision. + """ + gap_calculator = ap_calculator.AveragePrecisionCalculator() + sparse_predictions, sparse_labels, num_positives = top_k_by_class( + predictions, actuals, top_k) + gap_calculator.accumulate(flatten(sparse_predictions), + flatten(sparse_labels), sum(num_positives)) + return gap_calculator.peek_ap_at_n() + + +def top_k_by_class(predictions, labels, k=20): + """Extracts the top k predictions for each video, sorted by class. + + Args: + predictions: A numpy matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + k: the top k non-zero entries to preserve in each prediction. + + Returns: + A tuple (predictions,labels, true_positives). 'predictions' and 'labels' + are lists of lists of floats. 'true_positives' is a list of scalars. The + length of the lists are equal to the number of classes. The entries in the + predictions variable are probability predictions, and + the corresponding entries in the labels variable are the ground truth for + those predictions. The entries in 'true_positives' are the number of true + positives for each class in the ground truth. + + Raises: + ValueError: An error occurred when the k is not a positive integer. + """ + if k <= 0: + raise ValueError("k must be a positive integer.") + k = min(k, predictions.shape[1]) + num_classes = predictions.shape[1] + prediction_triplets = [] + for video_index in range(predictions.shape[0]): + prediction_triplets.extend( + top_k_triplets(predictions[video_index], labels[video_index], k)) + out_predictions = [[] for v in range(num_classes)] + out_labels = [[] for v in range(num_classes)] + for triplet in prediction_triplets: + out_predictions[triplet[0]].append(triplet[1]) + out_labels[triplet[0]].append(triplet[2]) + out_true_positives = [numpy.sum(labels[:, i]) for i in range(num_classes)] + + return out_predictions, out_labels, out_true_positives + + +def top_k_triplets(predictions, labels, k=20): + """Get the top_k for a 1-d numpy array. Returns a sparse list of tuples in + (prediction, class) format""" + m = len(predictions) + k = min(k, m) + indices = numpy.argpartition(predictions, -k)[-k:] + return [(index, predictions[index], labels[index]) for index in indices] + + +class EvaluationMetrics(object): + """A class to store the evaluation metrics.""" + def __init__(self, num_class, top_k): + """Construct an EvaluationMetrics object to store the evaluation metrics. + + Args: + num_class: A positive integer specifying the number of classes. + top_k: A positive integer specifying how many predictions are considered per video. + + Raises: + ValueError: An error occurred when MeanAveragePrecisionCalculator cannot + not be constructed. + """ + self.sum_hit_at_one = 0.0 + self.sum_perr = 0.0 + self.sum_loss = 0.0 + self.map_calculator = map_calculator.MeanAveragePrecisionCalculator( + num_class) + self.global_ap_calculator = ap_calculator.AveragePrecisionCalculator() + self.top_k = top_k + self.num_examples = 0 + + #def accumulate(self, predictions, labels, loss): + def accumulate(self, loss, predictions, labels): + """Accumulate the metrics calculated locally for this mini-batch. + + Args: + predictions: A numpy matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + labels: A numpy matrix containing the ground truth labels. + Dimensions are 'batch' x 'num_classes'. + loss: A numpy array containing the loss for each sample. + + Returns: + dictionary: A dictionary storing the metrics for the mini-batch. + + Raises: + ValueError: An error occurred when the shape of predictions and actuals + does not match. + """ + batch_size = labels.shape[0] + mean_hit_at_one = calculate_hit_at_one(predictions, labels) + mean_perr = calculate_precision_at_equal_recall_rate( + predictions, labels) + mean_loss = numpy.mean(loss) + + # Take the top 20 predictions. + sparse_predictions, sparse_labels, num_positives = top_k_by_class( + predictions, labels, self.top_k) + self.map_calculator.accumulate(sparse_predictions, sparse_labels, + num_positives) + self.global_ap_calculator.accumulate(flatten(sparse_predictions), + flatten(sparse_labels), + sum(num_positives)) + + self.num_examples += batch_size + self.sum_hit_at_one += mean_hit_at_one * batch_size + self.sum_perr += mean_perr * batch_size + self.sum_loss += mean_loss * batch_size + + return { + "hit_at_one": mean_hit_at_one, + "perr": mean_perr, + "loss": mean_loss + } + + def get(self): + """Calculate the evaluation metrics for the whole epoch. + + Raises: + ValueError: If no examples were accumulated. + + Returns: + dictionary: a dictionary storing the evaluation metrics for the epoch. The + dictionary has the fields: avg_hit_at_one, avg_perr, avg_loss, and + aps (default nan). + """ + if self.num_examples <= 0: + raise ValueError("total_sample must be positive.") + avg_hit_at_one = self.sum_hit_at_one / self.num_examples + avg_perr = self.sum_perr / self.num_examples + avg_loss = self.sum_loss / self.num_examples + + aps = self.map_calculator.peek_map_at_n() + gap = self.global_ap_calculator.peek_ap_at_n() + + epoch_info_dict = {} + return { + "avg_hit_at_one": avg_hit_at_one, + "avg_perr": avg_perr, + "avg_loss": avg_loss, + "aps": aps, + "gap": gap + } + + def clear(self): + """Clear the evaluation metrics and reset the EvaluationMetrics object.""" + self.sum_hit_at_one = 0.0 + self.sum_perr = 0.0 + self.sum_loss = 0.0 + self.map_calculator.clear() + self.global_ap_calculator.clear() + self.num_examples = 0 diff --git a/applications/VideoTag/metrics/youtube8m/mean_average_precision_calculator.py b/applications/VideoTag/metrics/youtube8m/mean_average_precision_calculator.py new file mode 100644 index 0000000000000000000000000000000000000000..bf26db25ec35fb5d106f2942a9470a9101c225f3 --- /dev/null +++ b/applications/VideoTag/metrics/youtube8m/mean_average_precision_calculator.py @@ -0,0 +1,113 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Calculate the mean average precision. + +It provides an interface for calculating mean average precision +for an entire list or the top-n ranked items. + +Example usages: +We first call the function accumulate many times to process parts of the ranked +list. After processing all the parts, we call peek_map_at_n +to calculate the mean average precision. + +``` +import random + +p = np.array([[random.random() for _ in xrange(50)] for _ in xrange(1000)]) +a = np.array([[random.choice([0, 1]) for _ in xrange(50)] + for _ in xrange(1000)]) + +# mean average precision for 50 classes. +calculator = mean_average_precision_calculator.MeanAveragePrecisionCalculator( + num_class=50) +calculator.accumulate(p, a) +aps = calculator.peek_map_at_n() +``` +""" + +import numpy +from . import average_precision_calculator + + +class MeanAveragePrecisionCalculator(object): + """This class is to calculate mean average precision. + """ + def __init__(self, num_class): + """Construct a calculator to calculate the (macro) average precision. + + Args: + num_class: A positive Integer specifying the number of classes. + top_n_array: A list of positive integers specifying the top n for each + class. The top n in each class will be used to calculate its average + precision at n. + The size of the array must be num_class. + + Raises: + ValueError: An error occurred when num_class is not a positive integer; + or the top_n_array is not a list of positive integers. + """ + if not isinstance(num_class, int) or num_class <= 1: + raise ValueError("num_class must be a positive integer.") + + self._ap_calculators = [] # member of AveragePrecisionCalculator + self._num_class = num_class # total number of classes + for i in range(num_class): + self._ap_calculators.append( + average_precision_calculator.AveragePrecisionCalculator()) + + def accumulate(self, predictions, actuals, num_positives=None): + """Accumulate the predictions and their ground truth labels. + + Args: + predictions: A list of lists storing the prediction scores. The outer + dimension corresponds to classes. + actuals: A list of lists storing the ground truth labels. The dimensions + should correspond to the predictions input. Any value + larger than 0 will be treated as positives, otherwise as negatives. + num_positives: If provided, it is a list of numbers representing the + number of true positives for each class. If not provided, the number of + true positives will be inferred from the 'actuals' array. + + Raises: + ValueError: An error occurred when the shape of predictions and actuals + does not match. + """ + if not num_positives: + num_positives = [None for i in predictions.shape[1]] + + calculators = self._ap_calculators + for i in range(len(predictions)): + calculators[i].accumulate(predictions[i], actuals[i], + num_positives[i]) + + def clear(self): + for calculator in self._ap_calculators: + calculator.clear() + + def is_empty(self): + return ([calculator.heap_size for calculator in self._ap_calculators + ] == [0 for _ in range(self._num_class)]) + + def peek_map_at_n(self): + """Peek the non-interpolated mean average precision at n. + + Returns: + An array of non-interpolated average precision at n (default 0) for each + class. + """ + aps = [ + self._ap_calculators[i].peek_ap_at_n() + for i in range(self._num_class) + ] + return aps diff --git a/applications/VideoTag/models/__init__.py b/applications/VideoTag/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4a3adbbfb4ee895e532f03bb2d392ef88dcd4dcf --- /dev/null +++ b/applications/VideoTag/models/__init__.py @@ -0,0 +1,7 @@ +from .model import regist_model, get_model +from .attention_lstm import AttentionLSTM +from .tsn import TSN + +# regist models, sort by alphabet +regist_model("AttentionLSTM", AttentionLSTM) +regist_model("TSN", TSN) diff --git a/applications/VideoTag/models/attention_lstm/__init__.py b/applications/VideoTag/models/attention_lstm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cb872f0e43ab52054b42970896e5791a0eeb691d --- /dev/null +++ b/applications/VideoTag/models/attention_lstm/__init__.py @@ -0,0 +1 @@ +from .attention_lstm import * diff --git a/applications/VideoTag/models/attention_lstm/attention_lstm.py b/applications/VideoTag/models/attention_lstm/attention_lstm.py new file mode 100644 index 0000000000000000000000000000000000000000..e0feea61750d61ed8652fff71f71d39308c6fced --- /dev/null +++ b/applications/VideoTag/models/attention_lstm/attention_lstm.py @@ -0,0 +1,180 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import paddle.fluid as fluid +from paddle.fluid import ParamAttr + +from ..model import ModelBase +from .lstm_attention import LSTMAttentionModel + +import logging +logger = logging.getLogger(__name__) + +__all__ = ["AttentionLSTM"] + + +class AttentionLSTM(ModelBase): + def __init__(self, name, cfg, mode='train', is_videotag=False): + super(AttentionLSTM, self).__init__(name, cfg, mode) + self.is_videotag = is_videotag + self.get_config() + + def get_config(self): + # get model configs + self.feature_names = self.cfg.MODEL.feature_names + self.feature_dims = self.cfg.MODEL.feature_dims + self.num_classes = self.cfg.MODEL.num_classes + self.embedding_size = self.cfg.MODEL.embedding_size + self.lstm_size = self.cfg.MODEL.lstm_size + self.drop_rate = self.cfg.MODEL.drop_rate + + # get mode configs + self.batch_size = self.get_config_from_sec(self.mode, 'batch_size', 1) + self.num_gpus = self.get_config_from_sec(self.mode, 'num_gpus', 1) + + if self.mode == 'train': + self.learning_rate = self.get_config_from_sec( + 'train', 'learning_rate', 1e-3) + self.weight_decay = self.get_config_from_sec( + 'train', 'weight_decay', 8e-4) + self.num_samples = self.get_config_from_sec('train', 'num_samples', + 5000000) + self.decay_epochs = self.get_config_from_sec( + 'train', 'decay_epochs', [5]) + self.decay_gamma = self.get_config_from_sec('train', 'decay_gamma', + 0.1) + + def build_input(self, use_dataloader): + self.feature_input = [] + for name, dim in zip(self.feature_names, self.feature_dims): + self.feature_input.append( + fluid.data(shape=[None, dim], + lod_level=1, + dtype='float32', + name=name)) + if self.mode != 'infer': + self.label_input = fluid.data(shape=[None, self.num_classes], + dtype='float32', + name='label') + else: + self.label_input = None + if use_dataloader: + assert self.mode != 'infer', \ + 'dataloader is not recommendated when infer, please set use_dataloader to be false.' + self.dataloader = fluid.io.DataLoader.from_generator( + feed_list=self.feature_input + [self.label_input], + capacity=8, + iterable=True) + + def build_model(self): + att_outs = [] + for i, (input_dim, + feature) in enumerate(zip(self.feature_dims, + self.feature_input)): + att = LSTMAttentionModel(input_dim, self.embedding_size, + self.lstm_size, self.drop_rate) + att_out = att.forward(feature, is_training=(self.mode == 'train')) + att_outs.append(att_out) + if len(att_outs) > 1: + out = fluid.layers.concat(att_outs, axis=1) + else: + out = att_outs[0] # video only, without audio in videoTag + + fc1 = fluid.layers.fc( + input=out, + size=8192, + act='relu', + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0)), + name='fc1') + fc2 = fluid.layers.fc( + input=fc1, + size=4096, + act='tanh', + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0)), + name='fc2') + + self.logit = fluid.layers.fc(input=fc2, size=self.num_classes, act=None, \ + bias_attr=ParamAttr(regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0)), name='output') + + self.output = fluid.layers.sigmoid(self.logit) + + def optimizer(self): + assert self.mode == 'train', "optimizer only can be get in train mode" + values = [ + self.learning_rate * (self.decay_gamma**i) + for i in range(len(self.decay_epochs) + 1) + ] + iter_per_epoch = self.num_samples / self.batch_size + boundaries = [e * iter_per_epoch for e in self.decay_epochs] + return fluid.optimizer.RMSProp( + learning_rate=fluid.layers.piecewise_decay(values=values, + boundaries=boundaries), + centered=True, + regularization=fluid.regularizer.L2Decay(self.weight_decay)) + + def loss(self): + assert self.mode != 'infer', "invalid loss calculationg in infer mode" + cost = fluid.layers.sigmoid_cross_entropy_with_logits( + x=self.logit, label=self.label_input) + cost = fluid.layers.reduce_sum(cost, dim=-1) + sum_cost = fluid.layers.reduce_sum(cost) + self.loss_ = fluid.layers.scale(sum_cost, + scale=self.num_gpus, + bias_after_scale=False) + return self.loss_ + + def outputs(self): + return [self.output, self.logit] + + def feeds(self): + return self.feature_input if self.mode == 'infer' else self.feature_input + [ + self.label_input + ] + + def fetches(self): + if self.mode == 'train' or self.mode == 'valid': + losses = self.loss() + fetch_list = [losses, self.output, self.label_input] + elif self.mode == 'test': + losses = self.loss() + fetch_list = [losses, self.output, self.label_input] + elif self.mode == 'infer': + fetch_list = [self.output] + else: + raise NotImplementedError('mode {} not implemented'.format( + self.mode)) + + return fetch_list + + def weights_info(self): + return None, None + + def load_pretrain_params(self, exe, pretrain, prog): + logger.info( + "Load pretrain weights from {}, exclude fc layer.".format(pretrain)) + + state_dict = fluid.load_program_state(pretrain) + dict_keys = list(state_dict.keys()) + for name in dict_keys: + if "fc_0" in name: + del state_dict[name] + logger.info( + 'Delete {} from pretrained parameters. Do not load it'. + format(name)) + fluid.set_program_state(prog, state_dict) diff --git a/applications/VideoTag/models/attention_lstm/lstm_attention.py b/applications/VideoTag/models/attention_lstm/lstm_attention.py new file mode 100644 index 0000000000000000000000000000000000000000..7430d03da5adc414a755df727ccf202baadd02ab --- /dev/null +++ b/applications/VideoTag/models/attention_lstm/lstm_attention.py @@ -0,0 +1,84 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import paddle.fluid as fluid +from paddle.fluid import ParamAttr +import numpy as np + + +class LSTMAttentionModel(object): + """LSTM Attention Model""" + def __init__(self, + bias_attr, + embedding_size=512, + lstm_size=1024, + drop_rate=0.5): + self.lstm_size = lstm_size + self.embedding_size = embedding_size + self.drop_rate = drop_rate + + def forward(self, input, is_training): + input_fc = fluid.layers.fc( + input=input, + size=self.embedding_size, + act='tanh', + bias_attr=ParamAttr( + regularizer=fluid.regularizer.L2Decay(0.0), + initializer=fluid.initializer.NormalInitializer(scale=0.0)), + name='rgb_fc') + + lstm_forward_fc = fluid.layers.fc( + input=input_fc, + size=self.lstm_size * 4, + act=None, + bias_attr=False, # video_tag + name='rgb_fc_forward') + + lstm_forward, _ = fluid.layers.dynamic_lstm(input=lstm_forward_fc, + size=self.lstm_size * 4, + is_reverse=False, + name='rgb_lstm_forward') + + lsmt_backward_fc = fluid.layers.fc( + input=input_fc, + size=self.lstm_size * 4, + act=None, + bias_attr=False, #video_tag + name='rgb_fc_backward') + + lstm_backward, _ = fluid.layers.dynamic_lstm(input=lsmt_backward_fc, + size=self.lstm_size * 4, + is_reverse=True, + name='rgb_lstm_backward') + + lstm_concat = fluid.layers.concat(input=[lstm_forward, lstm_backward], + axis=1) + + lstm_dropout = fluid.layers.dropout(x=lstm_concat, + dropout_prob=self.drop_rate, + is_test=(not is_training)) + + lstm_weight = fluid.layers.fc( + input=lstm_dropout, + size=1, + act='sequence_softmax', + bias_attr=False, #video_tag + name='rgb_weight') + + scaled = fluid.layers.elementwise_mul(x=lstm_dropout, + y=lstm_weight, + axis=0) + lstm_pool = fluid.layers.sequence_pool(input=scaled, pool_type='sum') + + return lstm_pool diff --git a/applications/VideoTag/models/model.py b/applications/VideoTag/models/model.py new file mode 100644 index 0000000000000000000000000000000000000000..55d51222dc336bc853a52107f7ff35abe874bccf --- /dev/null +++ b/applications/VideoTag/models/model.py @@ -0,0 +1,191 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import wget +import logging +try: + from configparser import ConfigParser +except: + from ConfigParser import ConfigParser + +import paddle.fluid as fluid +from .utils import download, AttrDict + +WEIGHT_DIR = os.path.join(os.path.expanduser('~'), '.paddle', 'weights') + +logger = logging.getLogger(__name__) + + +def is_parameter(var): + return isinstance(var, fluid.framework.Parameter) + + +class NotImplementError(Exception): + "Error: model function not implement" + + def __init__(self, model, function): + super(NotImplementError, self).__init__() + self.model = model.__class__.__name__ + self.function = function.__name__ + + def __str__(self): + return "Function {}() is not implemented in model {}".format( + self.function, self.model) + + +class ModelNotFoundError(Exception): + "Error: model not found" + + def __init__(self, model_name, avail_models): + super(ModelNotFoundError, self).__init__() + self.model_name = model_name + self.avail_models = avail_models + + def __str__(self): + msg = "Model {} Not Found.\nAvailiable models:\n".format( + self.model_name) + for model in self.avail_models: + msg += " {}\n".format(model) + return msg + + +class ModelBase(object): + def __init__(self, name, cfg, mode='train'): + assert mode in ['train', 'valid', 'test', 'infer'], \ + "Unknown mode type {}".format(mode) + self.name = name + self.is_training = (mode == 'train') + self.mode = mode + self.cfg = cfg + self.dataloader = None + + def build_model(self): + "build model struct" + raise NotImplementError(self, self.build_model) + + def build_input(self, use_dataloader): + "build input Variable" + raise NotImplementError(self, self.build_input) + + def optimizer(self): + "get model optimizer" + raise NotImplementError(self, self.optimizer) + + def outputs(self): + "get output variable" + raise NotImplementError(self, self.outputs) + + def loss(self): + "get loss variable" + raise NotImplementError(self, self.loss) + + def feeds(self): + "get feed inputs list" + raise NotImplementError(self, self.feeds) + + def fetches(self): + "get fetch list of model" + raise NotImplementError(self, self.fetches) + + def weights_info(self): + "get model weight default path and download url" + raise NotImplementError(self, self.weights_info) + + def get_weights(self): + "get model weight file path, download weight from Paddle if not exist" + path, url = self.weights_info() + path = os.path.join(WEIGHT_DIR, path) + if not os.path.isdir(WEIGHT_DIR): + logger.info('{} not exists, will be created automatically.'.format( + WEIGHT_DIR)) + os.makedirs(WEIGHT_DIR) + if os.path.exists(path): + return path + + logger.info("Download weights of {} from {}".format(self.name, url)) + wget.download(url, path) + return path + + def dataloader(self): + return self.dataloader + + def epoch_num(self): + "get train epoch num" + return self.cfg.TRAIN.epoch + + def pretrain_info(self): + "get pretrain base model directory" + return (None, None) + + def get_pretrain_weights(self): + "get model weight file path, download weight from Paddle if not exist" + path, url = self.pretrain_info() + if not path: + return None + + path = os.path.join(WEIGHT_DIR, path) + if not os.path.isdir(WEIGHT_DIR): + logger.info('{} not exists, will be created automatically.'.format( + WEIGHT_DIR)) + os.makedirs(WEIGHT_DIR) + if os.path.exists(path): + return path + + logger.info("Download pretrain weights of {} from {}".format( + self.name, url)) + download(url, path) + return path + + def load_pretrain_params(self, exe, pretrain, prog): + logger.info("Load pretrain weights from {}".format(pretrain)) + state_dict = fluid.load_program_state(pretrain) + fluid.set_program_state(prog, state_dict) + + def load_test_weights(self, exe, weights, prog): + params_list = list(filter(is_parameter, prog.list_vars())) + fluid.load(prog, weights, executor=exe, var_list=params_list) + + def get_config_from_sec(self, sec, item, default=None): + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + +class ModelZoo(object): + def __init__(self): + self.model_zoo = {} + + def regist(self, name, model): + assert model.__base__ == ModelBase, "Unknow model type {}".format( + type(model)) + self.model_zoo[name] = model + + def get(self, name, cfg, mode='train', is_videotag=False): + for k, v in self.model_zoo.items(): + if k.upper() == name.upper(): + return v(name, cfg, mode, is_videotag) + raise ModelNotFoundError(name, self.model_zoo.keys()) + + +# singleton model_zoo +model_zoo = ModelZoo() + + +def regist_model(name, model): + model_zoo.regist(name, model) + + +def get_model(name, cfg, mode='train', is_videotag=False): + return model_zoo.get(name, cfg, mode, is_videotag) diff --git a/applications/VideoTag/models/tsn/__init__.py b/applications/VideoTag/models/tsn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bd57d2687bc948e63dd88306e9d435bbbb5a7978 --- /dev/null +++ b/applications/VideoTag/models/tsn/__init__.py @@ -0,0 +1 @@ +from .tsn import * diff --git a/applications/VideoTag/models/tsn/tsn.py b/applications/VideoTag/models/tsn/tsn.py new file mode 100644 index 0000000000000000000000000000000000000000..a1a7f35c435fe51b603155fa315057c694914827 --- /dev/null +++ b/applications/VideoTag/models/tsn/tsn.py @@ -0,0 +1,165 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import paddle.fluid as fluid +from paddle.fluid import ParamAttr + +from ..model import ModelBase +from .tsn_res_model import TSN_ResNet + +import logging +logger = logging.getLogger(__name__) + +__all__ = ["TSN"] + + +class TSN(ModelBase): + def __init__(self, name, cfg, mode='train', is_videotag=False): + super(TSN, self).__init__(name, cfg, mode=mode) + self.is_videotag = is_videotag + self.get_config() + + def get_config(self): + self.num_classes = self.get_config_from_sec('model', 'num_classes') + self.seg_num = self.get_config_from_sec('model', 'seg_num') + self.seglen = self.get_config_from_sec('model', 'seglen') + self.image_mean = self.get_config_from_sec('model', 'image_mean') + self.image_std = self.get_config_from_sec('model', 'image_std') + self.num_layers = self.get_config_from_sec('model', 'num_layers') + + self.num_epochs = self.get_config_from_sec('train', 'epoch') + self.total_videos = self.get_config_from_sec('train', 'total_videos') + self.base_learning_rate = self.get_config_from_sec( + 'train', 'learning_rate') + self.learning_rate_decay = self.get_config_from_sec( + 'train', 'learning_rate_decay') + self.l2_weight_decay = self.get_config_from_sec('train', + 'l2_weight_decay') + self.momentum = self.get_config_from_sec('train', 'momentum') + + self.seg_num = self.get_config_from_sec(self.mode, 'seg_num', + self.seg_num) + self.target_size = self.get_config_from_sec(self.mode, 'target_size') + self.batch_size = self.get_config_from_sec(self.mode, 'batch_size') + + def build_input(self, use_dataloader=True): + image_shape = [3, self.target_size, self.target_size] + image_shape[0] = image_shape[0] * self.seglen + image_shape = [None, self.seg_num] + image_shape + self.use_dataloader = use_dataloader + + image = fluid.data(name='image', shape=image_shape, dtype='float32') + if self.mode != 'infer': + label = fluid.data(name='label', shape=[None, 1], dtype='int64') + else: + label = None + + if use_dataloader: + assert self.mode != 'infer', \ + 'dataloader is not recommendated when infer, please set use_dataloader to be false.' + self.dataloader = fluid.io.DataLoader.from_generator( + feed_list=[image, label], capacity=4, iterable=True) + + self.feature_input = [image] + self.label_input = label + + def create_model_args(self): + cfg = {} + cfg['layers'] = self.num_layers + cfg['class_dim'] = self.num_classes + cfg['seg_num'] = self.seg_num + return cfg + + def build_model(self): + cfg = self.create_model_args() + videomodel = TSN_ResNet(layers=cfg['layers'], + seg_num=cfg['seg_num'], + is_training=(self.mode == 'train'), + is_extractor=self.is_videotag) + out = videomodel.net(input=self.feature_input[0], + class_dim=cfg['class_dim']) + self.network_outputs = [out] + + def optimizer(self): + assert self.mode == 'train', "optimizer only can be get in train mode" + epoch_points = [self.num_epochs / 3, self.num_epochs * 2 / 3] + total_videos = self.total_videos + step = int(total_videos / self.batch_size + 1) + bd = [e * step for e in epoch_points] + base_lr = self.base_learning_rate + lr_decay = self.learning_rate_decay + lr = [base_lr, base_lr * lr_decay, base_lr * lr_decay * lr_decay] + l2_weight_decay = self.l2_weight_decay + momentum = self.momentum + optimizer = fluid.optimizer.Momentum( + learning_rate=fluid.layers.piecewise_decay(boundaries=bd, + values=lr), + momentum=momentum, + regularization=fluid.regularizer.L2Decay(l2_weight_decay)) + + return optimizer + + def loss(self): + assert self.mode != 'infer', "invalid loss calculationg in infer mode" + cost = fluid.layers.cross_entropy(input=self.network_outputs[0], \ + label=self.label_input, ignore_index=-1) + self.loss_ = fluid.layers.mean(x=cost) + return self.loss_ + + def outputs(self): + return self.network_outputs + + def feeds(self): + return self.feature_input if self.mode == 'infer' else self.feature_input + [ + self.label_input + ] + + def fetches(self): + if self.mode == 'train' or self.mode == 'valid': + losses = self.loss() + fetch_list = [losses, self.network_outputs[0], self.label_input] + elif self.mode == 'test': + losses = self.loss() + fetch_list = [losses, self.network_outputs[0], self.label_input] + elif self.mode == 'infer': + fetch_list = self.network_outputs + else: + raise NotImplementedError('mode {} not implemented'.format( + self.mode)) + + return fetch_list + + def pretrain_info(self): + return None, None + + def weights_info(self): + return None + + def load_pretrain_params(self, exe, pretrain, prog): + def is_parameter(var): + return isinstance(var, fluid.framework.Parameter) + + logger.info( + "Load pretrain weights from {}, exclude fc layer.".format(pretrain)) + + print("===pretrain===", pretrain) + state_dict = fluid.load_program_state(pretrain) + dict_keys = list(state_dict.keys()) + # remove fc layer when pretrain, because the number of classes in final fc may not match + for name in dict_keys: + if "fc_0" in name: + del state_dict[name] + print('Delete {} from pretrained parameters. Do not load it'. + format(name)) + fluid.set_program_state(prog, state_dict) diff --git a/applications/VideoTag/models/tsn/tsn_res_model.py b/applications/VideoTag/models/tsn/tsn_res_model.py new file mode 100644 index 0000000000000000000000000000000000000000..d5477db9ccd706d3d4e7060288a5a02fa287d60a --- /dev/null +++ b/applications/VideoTag/models/tsn/tsn_res_model.py @@ -0,0 +1,165 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import time +import sys +import paddle.fluid as fluid +import math + + +class TSN_ResNet(): + def __init__(self, + layers=50, + seg_num=7, + is_training=True, + is_extractor=False): + self.layers = layers + self.seg_num = seg_num + self.is_training = is_training + self.is_extractor = is_extractor + + def conv_bn_layer(self, + input, + num_filters, + filter_size, + stride=1, + groups=1, + act=None, + name=None): + conv = fluid.layers.conv2d( + input=input, + num_filters=num_filters, + filter_size=filter_size, + stride=stride, + padding=(filter_size - 1) // 2, + groups=groups, + act=None, + param_attr=fluid.param_attr.ParamAttr(name=name + "_weights"), + bias_attr=False) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + + return fluid.layers.batch_norm( + input=conv, + act=act, + is_test=(not self.is_training), + param_attr=fluid.param_attr.ParamAttr(name=bn_name + "_scale"), + bias_attr=fluid.param_attr.ParamAttr(bn_name + '_offset'), + moving_mean_name=bn_name + "_mean", + moving_variance_name=bn_name + '_variance') + + def shortcut(self, input, ch_out, stride, name): + ch_in = input.shape[1] + if ch_in != ch_out or stride != 1: + return self.conv_bn_layer(input, ch_out, 1, stride, name=name) + else: + return input + + def bottleneck_block(self, input, num_filters, stride, name): + conv0 = self.conv_bn_layer(input=input, + num_filters=num_filters, + filter_size=1, + act='relu', + name=name + "_branch2a") + conv1 = self.conv_bn_layer(input=conv0, + num_filters=num_filters, + filter_size=3, + stride=stride, + act='relu', + name=name + "_branch2b") + conv2 = self.conv_bn_layer(input=conv1, + num_filters=num_filters * 4, + filter_size=1, + act=None, + name=name + "_branch2c") + + short = self.shortcut(input, + num_filters * 4, + stride, + name=name + "_branch1") + + return fluid.layers.elementwise_add(x=short, y=conv2, act='relu') + + def net(self, input, class_dim=101): + layers = self.layers + seg_num = self.seg_num + supported_layers = [50, 101, 152] + assert layers in supported_layers, \ + "supported layers are {} but input layer is {}".format(supported_layers, layers) + + # reshape input + channels = input.shape[2] + short_size = input.shape[3] + input = fluid.layers.reshape( + x=input, shape=[-1, channels, short_size, short_size]) + + if layers == 50: + depth = [3, 4, 6, 3] + elif layers == 101: + depth = [3, 4, 23, 3] + elif layers == 152: + depth = [3, 8, 36, 3] + num_filters = [64, 128, 256, 512] + + conv = self.conv_bn_layer(input=input, + num_filters=64, + filter_size=7, + stride=2, + act='relu', + name='conv1') + conv = fluid.layers.pool2d(input=conv, + pool_size=3, + pool_stride=2, + pool_padding=1, + pool_type='max') + + for block in range(len(depth)): + for i in range(depth[block]): + if layers in [101, 152] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + + conv = self.bottleneck_block( + input=conv, + num_filters=num_filters[block], + stride=2 if i == 0 and block != 0 else 1, + name=conv_name) + + pool = fluid.layers.pool2d(input=conv, + pool_size=7, + pool_type='avg', + global_pooling=True) + + feature = fluid.layers.reshape(x=pool, + shape=[-1, seg_num, pool.shape[1]]) + if self.is_extractor: + out = feature + else: + out = fluid.layers.reduce_mean(feature, dim=1) + + stdv = 1.0 / math.sqrt(pool.shape[1] * 1.0) + out = fluid.layers.fc( + input=out, + size=class_dim, + act='softmax', + param_attr=fluid.param_attr.ParamAttr( + initializer=fluid.initializer.Uniform(-stdv, stdv))) + return out diff --git a/applications/VideoTag/models/utils.py b/applications/VideoTag/models/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..3480794285d0b2da3832c25ff3512c5678e2b0e1 --- /dev/null +++ b/applications/VideoTag/models/utils.py @@ -0,0 +1,47 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import wget +import tarfile + +__all__ = ['decompress', 'download', 'AttrDict'] + + +def decompress(path): + t = tarfile.open(path) + t.extractall(path=os.path.split(path)[0]) + t.close() + os.remove(path) + + +def download(url, path): + weight_dir = os.path.split(path)[0] + if not os.path.exists(weight_dir): + os.makedirs(weight_dir) + + path = path + ".tar.gz" + wget.download(url, path) + decompress(path) + + +class AttrDict(dict): + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value diff --git a/applications/VideoTag/predict.py b/applications/VideoTag/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..aa5f0e05dd3300a5ca0b31fd951e21c41c068ad9 --- /dev/null +++ b/applications/VideoTag/predict.py @@ -0,0 +1,170 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import time +import logging +import argparse +import ast +import numpy as np +try: + import cPickle as pickle +except: + import pickle +import paddle.fluid as fluid + +from utils.config_utils import * +import models +from reader import get_reader +from metrics import get_metrics +from utils.utility import check_cuda +from utils.utility import check_version + +logging.root.handlers = [] +FORMAT = '[%(levelname)s: %(filename)s: %(lineno)4d]: %(message)s' +logging.basicConfig(level=logging.DEBUG, format=FORMAT, stream=sys.stdout) +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--model_name', + type=str, + default='AttentionCluster', + help='name of model to train.') + parser.add_argument('--config', + type=str, + default='configs/attention_cluster.txt', + help='path to config file of model') + parser.add_argument('--use_gpu', + type=ast.literal_eval, + default=True, + help='default use gpu.') + parser.add_argument( + '--weights', + type=str, + default='./data/checkpoints/AttentionLSTM_epoch9.pdparams', + help='weight path.') + parser.add_argument('--batch_size', + type=int, + default=1, + help='sample number in a batch for inference.') + parser.add_argument('--filelist', + type=str, + default=None, + help='path to inferenece data file lists file.') + parser.add_argument('--log_interval', + type=int, + default=1, + help='mini-batch interval to log.') + parser.add_argument('--infer_topk', + type=int, + default=20, + help='topk predictions to restore.') + parser.add_argument('--save_dir', + type=str, + default=os.path.join('data', 'predict_results', + 'attention_lstm'), + help='directory to store results') + parser.add_argument('--video_path', + type=str, + default=None, + help='directory to store results') + parser.add_argument('--label_file', + type=str, + default='label_3396.txt', + help='chinese label file path') + args = parser.parse_args() + return args + + +def infer(args): + # parse config + config = parse_config(args.config) + infer_config = merge_configs(config, 'infer', vars(args)) + print_configs(infer_config, "Infer") + infer_model = models.get_model(args.model_name, infer_config, mode='infer') + infer_model.build_input(use_dataloader=False) + infer_model.build_model() + infer_feeds = infer_model.feeds() + infer_outputs = infer_model.outputs() + + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + + exe.run(fluid.default_startup_program()) + + filelist = args.filelist or infer_config.INFER.filelist + filepath = args.video_path or infer_config.INFER.get('filepath', '') + if filepath != '': + assert os.path.exists(filepath), "{} not exist.".format(filepath) + else: + assert os.path.exists(filelist), "{} not exist.".format(filelist) + + # get infer reader + infer_reader = get_reader(args.model_name.upper(), 'infer', infer_config) + + if args.weights: + assert os.path.exists( + args.weights), "Given weight dir {} not exist.".format(args.weights) + # if no weight files specified, download weights from paddle + weights = args.weights or infer_model.get_weights() + + infer_model.load_test_weights(exe, weights, fluid.default_main_program()) + + infer_feeder = fluid.DataFeeder(place=place, feed_list=infer_feeds) + fetch_list = infer_model.fetches() + + infer_metrics = get_metrics(args.model_name.upper(), 'infer', infer_config) + infer_metrics.reset() + + periods = [] + cur_time = time.time() + for infer_iter, data in enumerate(infer_reader()): + data_feed_in = [items[:-1] for items in data] + video_id = [items[-1] for items in data] + infer_outs = exe.run(fetch_list=fetch_list, + feed=infer_feeder.feed(data_feed_in)) + infer_result_list = [item for item in infer_outs] + [video_id] + + prev_time = cur_time + cur_time = time.time() + period = cur_time - prev_time + periods.append(period) + + infer_metrics.accumulate(infer_result_list) + + if args.log_interval > 0 and infer_iter % args.log_interval == 0: + logger.info('Processed {} samples'.format( + (infer_iter + 1) * len(video_id))) + + logger.info('[INFER] infer finished. average time: {}'.format( + np.mean(periods))) + + if not os.path.isdir(args.save_dir): + os.makedirs(args.save_dir) + + infer_metrics.finalize_and_log_out(savedir=args.save_dir, + label_file=args.label_file) + + +if __name__ == "__main__": + args = parse_args() + # check whether the installed paddle is compiled with GPU + check_cuda(args.use_gpu) + check_version() + logger.info(args) + + infer(args) diff --git a/applications/VideoTag/reader/__init__.py b/applications/VideoTag/reader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3d814a62dac113faa97668d6d6cff6586237435c --- /dev/null +++ b/applications/VideoTag/reader/__init__.py @@ -0,0 +1,7 @@ +from .reader_utils import regist_reader, get_reader +from .feature_reader import FeatureReader +from .kinetics_reader import KineticsReader + +# regist reader, sort by alphabet +regist_reader("ATTENTIONLSTM", FeatureReader) +regist_reader("TSN", KineticsReader) diff --git a/applications/VideoTag/reader/feature_reader.py b/applications/VideoTag/reader/feature_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..3be921f6d3ce73a49d098381b6df09f3bec202fb --- /dev/null +++ b/applications/VideoTag/reader/feature_reader.py @@ -0,0 +1,80 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import sys +from .reader_utils import DataReader +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle + from io import BytesIO +import numpy as np +import random + +python_ver = sys.version_info + + +class FeatureReader(DataReader): + """ + Data reader for youtube-8M dataset, which was stored as features extracted by prior networks + This is for the three models: lstm + + dataset cfg: num_classes + batch_size + list + """ + def __init__(self, name, mode, cfg): + self.name = name + self.mode = mode + self.num_classes = cfg.MODEL.num_classes + + # set batch size and file list + self.batch_size = cfg[mode.upper()]['batch_size'] + self.filelist = cfg[mode.upper()]['filelist'] + self.seg_num = cfg.MODEL.get('seg_num', None) + + def create_reader(self): + fl = open(self.filelist).readlines() + fl = [line.strip() for line in fl if line.strip() != ''] + if self.mode == 'train': + random.shuffle(fl) + + def reader(): + batch_out = [] + for item in fl: + fileinfo = item.split(' ') + filepath = fileinfo[0] + rgb = np.load(filepath, allow_pickle=True) + nframes = rgb.shape[0] + label = [int(i) for i in fileinfo[1:]] + one_hot_label = make_one_hot(label, self.num_classes) + + if self.mode != 'infer': + batch_out.append((rgb, one_hot_label)) + else: + batch_out.append((rgb, filepath.split('/')[-1])) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + + return reader + + +def make_one_hot(label, dim=3862): + one_hot_label = np.zeros(dim) + one_hot_label = one_hot_label.astype(float) + for ind in label: + one_hot_label[int(ind)] = 1 + return one_hot_label diff --git a/applications/VideoTag/reader/kinetics_reader.py b/applications/VideoTag/reader/kinetics_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..92c42490ebd45423ebd48fe5b8e30b11fd7e4916 --- /dev/null +++ b/applications/VideoTag/reader/kinetics_reader.py @@ -0,0 +1,368 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import cv2 +import math +import random +import functools +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle + from io import BytesIO +import numpy as np +import paddle +import paddle.fluid as fluid + +from PIL import Image, ImageEnhance +import logging + +from .reader_utils import DataReader + +logger = logging.getLogger(__name__) +python_ver = sys.version_info + + +class VideoRecord(object): + ''' + define a class method which used to describe the frames information of videos + 1. self._data[0] is the frames' path + 2. self._data[1] is the number of frames + 3. self._data[2] is the label of frames + ''' + def __init__(self, row): + self._data = row + + @property + def path(self): + return self._data[0] + + @property + def num_frames(self): + return int(self._data[1]) + + @property + def label(self): + return int(self._data[2]) + + +class KineticsReader(DataReader): + """ + Data reader for kinetics dataset of two format mp4 and pkl. + 1. mp4, the original format of kinetics400 + 2. pkl, the mp4 was decoded previously and stored as pkl + In both case, load the data, and then get the frame data in the form of numpy and label as an integer. + dataset cfg: format + num_classes + seg_num + short_size + target_size + num_reader_threads + buf_size + image_mean + image_std + batch_size + list + """ + def __init__(self, name, mode, cfg): + super(KineticsReader, self).__init__(name, mode, cfg) + self.format = cfg.MODEL.format + self.num_classes = self.get_config_from_sec('model', 'num_classes') + self.seg_num = self.get_config_from_sec('model', 'seg_num') + self.seglen = self.get_config_from_sec('model', 'seglen') + + self.seg_num = self.get_config_from_sec(mode, 'seg_num', self.seg_num) + self.short_size = self.get_config_from_sec(mode, 'short_size') + self.target_size = self.get_config_from_sec(mode, 'target_size') + self.num_reader_threads = self.get_config_from_sec( + mode, 'num_reader_threads') + self.buf_size = self.get_config_from_sec(mode, 'buf_size') + self.fix_random_seed = self.get_config_from_sec(mode, 'fix_random_seed') + + self.img_mean = np.array(cfg.MODEL.image_mean).reshape( + [3, 1, 1]).astype(np.float32) + self.img_std = np.array(cfg.MODEL.image_std).reshape([3, 1, 1]).astype( + np.float32) + # set batch size and file list + self.batch_size = cfg[mode.upper()]['batch_size'] + self.filelist = cfg[mode.upper()]['filelist'] + + if self.fix_random_seed: + random.seed(0) + np.random.seed(0) + self.num_reader_threads = 1 + + def create_reader(self): + assert os.path.exists(self.filelist), \ + '{} not exist, please check the data list'.format(self.filelist) + _reader = self._reader_creator(self.filelist, self.mode, seg_num=self.seg_num, seglen = self.seglen, \ + short_size = self.short_size, target_size = self.target_size, \ + img_mean = self.img_mean, img_std = self.img_std, \ + shuffle = (self.mode == 'train'), \ + num_threads = self.num_reader_threads, \ + buf_size = self.buf_size, format = self.format) + + def _batch_reader(): + batch_out = [] + for imgs, label in _reader(): + if imgs is None: + continue + batch_out.append((imgs, label)) + if len(batch_out) == self.batch_size: + yield batch_out + batch_out = [] + + return _batch_reader + + def _reader_creator(self, + file_list, + mode, + seg_num, + seglen, + short_size, + target_size, + img_mean, + img_std, + shuffle=False, + num_threads=1, + buf_size=1024, + format='frames'): + def decode_mp4(sample, mode, seg_num, seglen, short_size, target_size, + img_mean, img_std): + sample = sample[0].split(' ') + mp4_path = sample[0] + if mode == "infer": + label = mp4_path.split('/')[-1] + else: + label = int(sample[1]) + try: + imgs = mp4_loader(mp4_path, seg_num, seglen, mode) + if len(imgs) < 1: + logger.error('{} frame length {} less than 1.'.format( + mp4_path, len(imgs))) + return None, None + except: + logger.error('Error when loading {}'.format(mp4_path)) + return None, None + + return imgs_transform(imgs, mode, seg_num, seglen, \ + short_size, target_size, img_mean, img_std, name = self.name), label + + def decode_frames(sample, mode, seg_num, seglen, short_size, + target_size, img_mean, img_std): + recode = VideoRecord(sample[0].split(' ')) + frames_dir_path = recode.path + if mode == "infer": + label = frames_dir_path + else: + label = recode.label + + try: + imgs = frames_loader(recode, seg_num, seglen, mode) + if len(imgs) < 1: + logger.error('{} frame length {} less than 1.'.format( + frames_dir_path, len(imgs))) + return None, None + except: + logger.error('Error when loading {}'.format(frames_dir_path)) + return None, None + + return imgs_transform(imgs, + mode, + seg_num, + seglen, + short_size, + target_size, + img_mean, + img_std, + name=self.name), label + + def reader_(): + with open(file_list) as flist: + lines = [line.strip() for line in flist] + if shuffle: + random.shuffle(lines) + for line in lines: + file_path = line.strip() + yield [file_path] + + if format == 'frames': + decode_func = decode_frames + elif format == 'video': + decode_func = decode_mp4 + else: + raise ("Not implemented format {}".format(format)) + + mapper = functools.partial(decode_func, + mode=mode, + seg_num=seg_num, + seglen=seglen, + short_size=short_size, + target_size=target_size, + img_mean=img_mean, + img_std=img_std) + + return fluid.io.xmap_readers(mapper, + reader_, + num_threads, + buf_size, + order=True) + + +def imgs_transform(imgs, + mode, + seg_num, + seglen, + short_size, + target_size, + img_mean, + img_std, + name=''): + imgs = group_scale(imgs, short_size) + + np_imgs = np.array([np.array(img).astype('float32') for img in imgs]) #dhwc + + if mode == 'train': + np_imgs = group_crop(np_imgs, target_size) + np_imgs = group_random_flip(np_imgs) + else: + np_imgs = group_crop(np_imgs, target_size, is_center=True) + + np_imgs = np_imgs.transpose(0, 3, 1, 2) / 255 #dchw + np_imgs -= img_mean + np_imgs /= img_std + + return np_imgs + + +def group_crop(np_imgs, target_size, is_center=True): + d, h, w, c = np_imgs.shape + th, tw = target_size, target_size + assert (w >= target_size) and (h >= target_size), \ + "image width({}) and height({}) should be larger than crop size".format(w, h, target_size) + + if is_center: + h_off = int(round((h - th) / 2.)) + w_off = int(round((w - tw) / 2.)) + else: + w_off = random.randint(0, w - tw) + h_off = random.randint(0, h - th) + + img_crop = np_imgs[:, h_off:h_off + target_size, + w_off:w_off + target_size, :] + return img_crop + + +def group_random_flip(np_imgs): + prob = random.random() + if prob < 0.5: + ret = np_imgs[:, :, ::-1, :] + return ret + else: + return np_imgs + + +def group_scale(imgs, target_size): + resized_imgs = [] + for i in range(len(imgs)): + img = imgs[i] + w, h = img.size + if (w <= h and w == target_size) or (h <= w and h == target_size): + resized_imgs.append(img) + continue + + if w < h: + ow = target_size + oh = int(target_size * 4.0 / 3.0) + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + else: + oh = target_size + ow = int(target_size * 4.0 / 3.0) + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + + return resized_imgs + + +def mp4_loader(filepath, nsample, seglen, mode): + cap = cv2.VideoCapture(filepath) + videolen = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + sampledFrames = [] + for i in range(videolen): + ret, frame = cap.read() + # maybe first frame is empty + if ret == False: + continue + img = frame[:, :, ::-1] + sampledFrames.append(img) + average_dur = int(len(sampledFrames) / nsample) + imgs = [] + for i in range(nsample): + idx = 0 + if mode == 'train': + if average_dur >= seglen: + idx = random.randint(0, average_dur - seglen) + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + else: + if average_dur >= seglen: + idx = (average_dur - 1) // 2 + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + + for jj in range(idx, idx + seglen): + imgbuf = sampledFrames[int(jj % len(sampledFrames))] + img = Image.fromarray(imgbuf, mode='RGB') + imgs.append(img) + + return imgs + + +def frames_loader(recode, nsample, seglen, mode): + imgpath, num_frames = recode.path, recode.num_frames + average_dur = int(num_frames / nsample) + imgs = [] + for i in range(nsample): + idx = 0 + if mode == 'train': + if average_dur >= seglen: + idx = random.randint(0, average_dur - seglen) + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + else: + if average_dur >= seglen: + idx = (average_dur - 1) // 2 + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + + for jj in range(idx, idx + seglen): + img = Image.open( + os.path.join(imgpath, + 'img_{:05d}.jpg'.format(jj + 1))).convert('RGB') + imgs.append(img) + return imgs diff --git a/applications/VideoTag/reader/reader_utils.py b/applications/VideoTag/reader/reader_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f6a9ef5eb7b4565b7ac72d28457af592f7b8c182 --- /dev/null +++ b/applications/VideoTag/reader/reader_utils.py @@ -0,0 +1,80 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import pickle +import cv2 +import numpy as np +import random + + +class ReaderNotFoundError(Exception): + "Error: reader not found" + + def __init__(self, reader_name, avail_readers): + super(ReaderNotFoundError, self).__init__() + self.reader_name = reader_name + self.avail_readers = avail_readers + + def __str__(self): + msg = "Reader {} Not Found.\nAvailiable readers:\n".format( + self.reader_name) + for reader in self.avail_readers: + msg += " {}\n".format(reader) + return msg + + +class DataReader(object): + """data reader for video input""" + def __init__(self, model_name, mode, cfg): + self.name = model_name + self.mode = mode + self.cfg = cfg + + def create_reader(self): + """Not implemented""" + pass + + def get_config_from_sec(self, sec, item, default=None): + if sec.upper() not in self.cfg: + return default + return self.cfg[sec.upper()].get(item, default) + + +class ReaderZoo(object): + def __init__(self): + self.reader_zoo = {} + + def regist(self, name, reader): + assert reader.__base__ == DataReader, "Unknow model type {}".format( + type(reader)) + self.reader_zoo[name] = reader + + def get(self, name, mode, cfg): + for k, v in self.reader_zoo.items(): + if k == name: + return v(name, mode, cfg) + raise ReaderNotFoundError(name, self.reader_zoo.keys()) + + +# singleton reader_zoo +reader_zoo = ReaderZoo() + + +def regist_reader(name, reader): + reader_zoo.regist(name, reader) + + +def get_reader(name, mode, cfg): + reader_model = reader_zoo.get(name, mode, cfg) + return reader_model.create_reader() diff --git a/applications/VideoTag/train.py b/applications/VideoTag/train.py new file mode 100644 index 0000000000000000000000000000000000000000..b88be931ba356089bfaf0a33249046b3d3d4c198 --- /dev/null +++ b/applications/VideoTag/train.py @@ -0,0 +1,211 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import argparse +import ast +import logging +import paddle.fluid as fluid + +from utils.train_utils import train_with_dataloader +import models +from utils.config_utils import * +from reader import get_reader +from metrics import get_metrics +from utils.utility import check_cuda +from utils.utility import check_version + +logging.root.handlers = [] +FORMAT = '[%(levelname)s: %(filename)s: %(lineno)4d]: %(message)s' +logging.basicConfig(level=logging.INFO, format=FORMAT, stream=sys.stdout) +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser("Paddle Video train script") + parser.add_argument('--model_name', + type=str, + default='AttentionCluster', + help='name of model to train.') + parser.add_argument('--config', + type=str, + default='configs/attention_cluster.txt', + help='path to config file of model') + parser.add_argument( + '--batch_size', + type=int, + default=None, + help='training batch size. None to use config file setting.') + parser.add_argument( + '--learning_rate', + type=float, + default=None, + help='learning rate use for training. None to use config file setting.') + parser.add_argument('--pretrain', + type=str, + default=None, + help='path to pretrain weights.') + parser.add_argument('--use_gpu', + type=ast.literal_eval, + default=True, + help='default use gpu.') + parser.add_argument('--no_memory_optimize', + action='store_true', + default=False, + help='whether to use memory optimize in train') + parser.add_argument('--epoch', + type=int, + default=None, + help='epoch number, 0 for read from config file') + parser.add_argument('--valid_interval', + type=int, + default=1, + help='validation epoch interval, 0 for no validation.') + parser.add_argument('--save_dir', + type=str, + default=os.path.join('data', 'checkpoints'), + help='directory name to save train snapshoot') + parser.add_argument('--log_interval', + type=int, + default=1, + help='mini-batch interval to log.') + parser.add_argument('--fix_random_seed', + type=ast.literal_eval, + default=False, + help='If set True, enable continuous evaluation job.') + args = parser.parse_args() + return args + + +def train(args): + # parse config + config = parse_config(args.config) + train_config = merge_configs(config, 'train', vars(args)) + valid_config = merge_configs(config, 'valid', vars(args)) + print_configs(train_config, 'Train') + train_model = models.get_model(args.model_name, train_config, mode='train') + valid_model = models.get_model(args.model_name, valid_config, mode='valid') + + # build model + startup = fluid.Program() + train_prog = fluid.Program() + if args.fix_random_seed: + startup.random_seed = 1000 + train_prog.random_seed = 1000 + with fluid.program_guard(train_prog, startup): + with fluid.unique_name.guard(): + train_model.build_input(use_dataloader=True) + train_model.build_model() + # for the input, has the form [data1, data2,..., label], so train_feeds[-1] is label + train_feeds = train_model.feeds() + train_fetch_list = train_model.fetches() + train_loss = train_fetch_list[0] + optimizer = train_model.optimizer() + optimizer.minimize(train_loss) + train_dataloader = train_model.dataloader() + + valid_prog = fluid.Program() + with fluid.program_guard(valid_prog, startup): + with fluid.unique_name.guard(): + valid_model.build_input(use_dataloader=True) + valid_model.build_model() + valid_feeds = valid_model.feeds() + valid_fetch_list = valid_model.fetches() + valid_dataloader = valid_model.dataloader() + + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + exe.run(startup) + + if args.pretrain: + train_model.load_pretrain_params(exe, args.pretrain, train_prog) + + build_strategy = fluid.BuildStrategy() + build_strategy.enable_inplace = True + + exec_strategy = fluid.ExecutionStrategy() + + compiled_train_prog = fluid.compiler.CompiledProgram( + train_prog).with_data_parallel(loss_name=train_loss.name, + build_strategy=build_strategy, + exec_strategy=exec_strategy) + compiled_valid_prog = fluid.compiler.CompiledProgram( + valid_prog).with_data_parallel(share_vars_from=compiled_train_prog, + build_strategy=build_strategy, + exec_strategy=exec_strategy) + + # get reader + bs_denominator = 1 + if args.use_gpu: + # check number of GPUs + gpus = os.getenv("CUDA_VISIBLE_DEVICES", "") + if gpus == "": + pass + else: + gpus = gpus.split(",") + num_gpus = len(gpus) + assert num_gpus == train_config.TRAIN.num_gpus, \ + "num_gpus({}) set by CUDA_VISIBLE_DEVICES " \ + "shoud be the same as that " \ + "set in {}({})".format( + num_gpus, args.config, train_config.TRAIN.num_gpus) + bs_denominator = train_config.TRAIN.num_gpus + + train_config.TRAIN.batch_size = int(train_config.TRAIN.batch_size / + bs_denominator) + valid_config.VALID.batch_size = int(valid_config.VALID.batch_size / + bs_denominator) + train_reader = get_reader(args.model_name.upper(), 'train', train_config) + valid_reader = get_reader(args.model_name.upper(), 'valid', valid_config) + + # get metrics + train_metrics = get_metrics(args.model_name.upper(), 'train', train_config) + valid_metrics = get_metrics(args.model_name.upper(), 'valid', valid_config) + + epochs = args.epoch or train_model.epoch_num() + + exe_places = fluid.cuda_places() if args.use_gpu else fluid.cpu_places() + train_dataloader.set_sample_list_generator(train_reader, places=exe_places) + valid_dataloader.set_sample_list_generator(valid_reader, places=exe_places) + + train_with_dataloader(exe, + train_prog, + compiled_train_prog, + train_dataloader, + train_fetch_list, + train_metrics, + epochs=epochs, + log_interval=args.log_interval, + valid_interval=args.valid_interval, + save_dir=args.save_dir, + save_model_name=args.model_name, + fix_random_seed=args.fix_random_seed, + compiled_test_prog=compiled_valid_prog, + test_dataloader=valid_dataloader, + test_fetch_list=valid_fetch_list, + test_metrics=valid_metrics) + + +if __name__ == "__main__": + args = parse_args() + # check whether the installed paddle is compiled with GPU + check_cuda(args.use_gpu) + check_version() + logger.info(args) + + if not os.path.exists(args.save_dir): + os.makedirs(args.save_dir) + + train(args) diff --git a/applications/VideoTag/tsn_extractor.py b/applications/VideoTag/tsn_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..361d6f7f4b5a13dd57b6f820dfc9616f76f024af --- /dev/null +++ b/applications/VideoTag/tsn_extractor.py @@ -0,0 +1,157 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import time +import logging +import argparse +import ast +import numpy as np +try: + import cPickle as pickle +except: + import pickle +import paddle.fluid as fluid + +from utils.config_utils import * +import models +from reader import get_reader +from metrics import get_metrics +from utils.utility import check_cuda +from utils.utility import check_version + +logging.root.handlers = [] +FORMAT = '[%(levelname)s: %(filename)s: %(lineno)4d]: %(message)s' +logging.basicConfig(level=logging.DEBUG, format=FORMAT, stream=sys.stdout) +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--model_name', + type=str, + default='AttentionCluster', + help='name of model to train.') + parser.add_argument('--config', + type=str, + default='configs/attention_cluster.txt', + help='path to config file of model') + parser.add_argument('--use_gpu', + type=ast.literal_eval, + default=True, + help='default use gpu.') + parser.add_argument( + '--weights', + type=str, + default=None, + help= + 'weight path, None to automatically download weights provided by Paddle.' + ) + parser.add_argument('--batch_size', + type=int, + default=1, + help='sample number in a batch for inference.') + parser.add_argument('--filelist', + type=str, + default='./data/TsnExtractor.list', + help='path to inferenece data file lists file.') + parser.add_argument('--log_interval', + type=int, + default=1, + help='mini-batch interval to log.') + parser.add_argument('--infer_topk', + type=int, + default=20, + help='topk predictions to restore.') + parser.add_argument('--save_dir', + type=str, + default=os.path.join('data', 'tsn_features'), + help='directory to store tsn feature results') + parser.add_argument('--video_path', + type=str, + default=None, + help='directory to store results') + args = parser.parse_args() + return args + + +def infer(args): + # parse config + config = parse_config(args.config) + infer_config = merge_configs(config, 'infer', vars(args)) + print_configs(infer_config, "Infer") + infer_model = models.get_model(args.model_name, + infer_config, + mode='infer', + is_videotag=True) + infer_model.build_input(use_dataloader=False) + infer_model.build_model() + infer_feeds = infer_model.feeds() + infer_outputs = infer_model.outputs() + + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + + exe.run(fluid.default_startup_program()) + + filelist = args.filelist or infer_config.INFER.filelist + filepath = args.video_path or infer_config.INFER.get('filepath', '') + if filepath != '': + assert os.path.exists(filepath), "{} not exist.".format(filepath) + else: + assert os.path.exists(filelist), "{} not exist.".format(filelist) + + # get infer reader + infer_reader = get_reader(args.model_name.upper(), 'infer', infer_config) + + if args.weights: + assert os.path.exists( + args.weights), "Given weight dir {} not exist.".format(args.weights) + # if no weight files specified, download weights from paddle + weights = args.weights or infer_model.get_weights() + + infer_model.load_test_weights(exe, weights, fluid.default_main_program()) + + infer_feeder = fluid.DataFeeder(place=place, feed_list=infer_feeds) + fetch_list = infer_model.fetches() + + infer_metrics = get_metrics(args.model_name.upper(), 'infer', infer_config) + infer_metrics.reset() + + if not os.path.isdir(args.save_dir): + os.makedirs(args.save_dir) + + for infer_iter, data in enumerate(infer_reader()): + data_feed_in = [items[:-1] for items in data] + video_id = [items[-1] for items in data] + bs = len(video_id) + feature_outs = exe.run(fetch_list=fetch_list, + feed=infer_feeder.feed(data_feed_in)) + for i in range(bs): + filename = video_id[i].split('/')[-1][:-4] + np.save(os.path.join(args.save_dir, filename + '.npy'), + feature_outs[0][i]) #shape: seg_num*feature_dim + + logger.info("Feature extraction End~") + + +if __name__ == "__main__": + args = parse_args() + # check whether the installed paddle is compiled with GPU + check_cuda(args.use_gpu) + check_version() + logger.info(args) + + infer(args) diff --git a/applications/VideoTag/utils/__init__.py b/applications/VideoTag/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/applications/VideoTag/utils/config_utils.py b/applications/VideoTag/utils/config_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..647e541d63e507900402b8702029186ca647b310 --- /dev/null +++ b/applications/VideoTag/utils/config_utils.py @@ -0,0 +1,75 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import yaml +from .utility import AttrDict +import logging +logger = logging.getLogger(__name__) + +CONFIG_SECS = [ + 'train', + 'valid', + 'test', + 'infer', +] + + +def parse_config(cfg_file): + """Load a config file into AttrDict""" + import yaml + with open(cfg_file, 'r') as fopen: + yaml_config = AttrDict(yaml.load(fopen, Loader=yaml.Loader)) + create_attr_dict(yaml_config) + return yaml_config + + +def create_attr_dict(yaml_config): + from ast import literal_eval + for key, value in yaml_config.items(): + if type(value) is dict: + yaml_config[key] = value = AttrDict(value) + if isinstance(value, str): + try: + value = literal_eval(value) + except BaseException: + pass + if isinstance(value, AttrDict): + create_attr_dict(yaml_config[key]) + else: + yaml_config[key] = value + return + + +def merge_configs(cfg, sec, args_dict): + assert sec in CONFIG_SECS, "invalid config section {}".format(sec) + sec_dict = getattr(cfg, sec.upper()) + for k, v in args_dict.items(): + if v is None: + continue + try: + if hasattr(sec_dict, k): + setattr(sec_dict, k, v) + except: + pass + return cfg + + +def print_configs(cfg, mode): + logger.info( + "---------------- {:>5} Arguments ----------------".format(mode)) + for sec, sec_items in cfg.items(): + logger.info("{}:".format(sec)) + for k, v in sec_items.items(): + logger.info(" {}:{}".format(k, v)) + logger.info("-------------------------------------------------") diff --git a/applications/VideoTag/utils/train_utils.py b/applications/VideoTag/utils/train_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..3f33a070a54da971513a8867061a3b864a6ec0c4 --- /dev/null +++ b/applications/VideoTag/utils/train_utils.py @@ -0,0 +1,154 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import time +import numpy as np +import paddle +import paddle.fluid as fluid +from paddle.fluid import profiler +import logging +import shutil + +logger = logging.getLogger(__name__) + + +def log_lr_and_step(): + try: + # In optimizers, if learning_rate is set as constant, lr_var + # name is 'learning_rate_0', and iteration counter is not + # recorded. If learning_rate is set as decayed values from + # learning_rate_scheduler, lr_var name is 'learning_rate', + # and iteration counter is recorded with name '@LR_DECAY_COUNTER@', + # better impliment is required here + lr_var = fluid.global_scope().find_var("learning_rate") + if not lr_var: + lr_var = fluid.global_scope().find_var("learning_rate_0") + lr = np.array(lr_var.get_tensor()) + + lr_count = '[-]' + lr_count_var = fluid.global_scope().find_var("@LR_DECAY_COUNTER@") + if lr_count_var: + lr_count = np.array(lr_count_var.get_tensor()) + logger.info( + "------- learning rate {}, learning rate counter {} -----".format( + np.array(lr), np.array(lr_count))) + except: + logger.warn("Unable to get learning_rate and LR_DECAY_COUNTER.") + + +def test_with_dataloader(exe, + compiled_test_prog, + test_dataloader, + test_fetch_list, + test_metrics, + log_interval=0, + save_model_name=''): + if not test_dataloader: + logger.error("[TEST] get dataloader failed.") + test_metrics.reset() + test_iter = 0 + + for data in test_dataloader(): + test_outs = exe.run(compiled_test_prog, + fetch_list=test_fetch_list, + feed=data) + test_metrics.accumulate(test_outs) + if log_interval > 0 and test_iter % log_interval == 0: + test_metrics.calculate_and_log_out(test_outs, \ + info = '[TEST] test_iter {} '.format(test_iter)) + test_iter += 1 + test_metrics.finalize_and_log_out("[TEST] Finish") + + +def train_with_dataloader(exe, train_prog, compiled_train_prog, train_dataloader, \ + train_fetch_list, train_metrics, epochs = 10, \ + log_interval = 0, valid_interval = 0, save_dir = './', \ + num_trainers = 1, trainer_id = 0, \ + save_model_name = 'model', fix_random_seed = False, \ + compiled_test_prog = None, test_dataloader = None, \ + test_fetch_list = None, test_metrics = None, \ + is_profiler = None, profiler_path = None): + if not train_dataloader: + logger.error("[TRAIN] get dataloader failed.") + epoch_periods = [] + train_loss = 0 + for epoch in range(epochs): + log_lr_and_step() + + train_iter = 0 + epoch_periods = [] + + cur_time = time.time() + for data in train_dataloader(): + train_outs = exe.run(compiled_train_prog, + fetch_list=train_fetch_list, + feed=data) + period = time.time() - cur_time + epoch_periods.append(period) + timeStamp = time.time() + localTime = time.localtime(timeStamp) + strTime = time.strftime("%Y-%m-%d %H:%M:%S", localTime) + if log_interval > 0 and (train_iter % log_interval == 0): + train_metrics.calculate_and_log_out(train_outs, \ + info = '[TRAIN {}] Epoch {}, iter {}, time {}, '.format(strTime, epoch, train_iter, period)) + train_iter += 1 + cur_time = time.time() + + # NOTE: profiler tools, used for benchmark + if is_profiler and epoch == 0 and train_iter == log_interval: + profiler.start_profiler("All") + elif is_profiler and epoch == 0 and train_iter == log_interval + 5: + profiler.stop_profiler("total", profiler_path) + return + + if len(epoch_periods) < 1: + logger.info( + 'No iteration was executed, please check the data reader') + sys.exit(1) + + logger.info( + '[TRAIN] Epoch {} training finished, average time: {}'.format( + epoch, np.mean(epoch_periods[1:]))) + + if trainer_id == 0: + save_model(exe, train_prog, save_dir, save_model_name, + "_epoch{}".format(epoch)) + if compiled_test_prog and valid_interval > 0 and ( + epoch + 1) % valid_interval == 0: + test_with_dataloader(exe, compiled_test_prog, test_dataloader, + test_fetch_list, test_metrics, log_interval, + save_model_name) + + if trainer_id == 0: + save_model(exe, train_prog, save_dir, save_model_name) + #when fix_random seed for debug + if fix_random_seed: + cards = os.environ.get('CUDA_VISIBLE_DEVICES') + gpu_num = len(cards.split(",")) + print("kpis\ttrain_cost_card{}\t{}".format(gpu_num, train_loss)) + print("kpis\ttrain_speed_card{}\t{}".format(gpu_num, + np.mean(epoch_periods))) + + +def save_model(exe, program, save_dir, model_name, postfix=''): + """save paramters and optimizer related varaibles""" + if not os.path.isdir(save_dir): + os.makedirs(save_dir) + saved_model_name = model_name + postfix + + fluid.save(program, os.path.join(save_dir, saved_model_name)) + + return diff --git a/applications/VideoTag/utils/utility.py b/applications/VideoTag/utils/utility.py new file mode 100644 index 0000000000000000000000000000000000000000..fa94c0ddc4296100b206a4b4529774bd1c75c773 --- /dev/null +++ b/applications/VideoTag/utils/utility.py @@ -0,0 +1,71 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import signal +import logging +import paddle +import paddle.fluid as fluid + +__all__ = ['AttrDict'] + +logger = logging.getLogger(__name__) + + +def _term(sig_num, addition): + print('current pid is %s, group id is %s' % (os.getpid(), os.getpgrp())) + os.killpg(os.getpgid(os.getpid()), signal.SIGKILL) + + +signal.signal(signal.SIGTERM, _term) +signal.signal(signal.SIGINT, _term) + + +class AttrDict(dict): + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value + +def check_cuda(use_cuda, err = \ + "\nYou can not set use_gpu = True in the model because you are using paddlepaddle-cpu.\n \ + Please: 1. Install paddlepaddle-gpu to run your models on GPU or 2. Set use_gpu = False to run models on CPU.\n" + ): + try: + if use_cuda == True and fluid.is_compiled_with_cuda() == False: + print(err) + sys.exit(1) + except Exception as e: + pass + + +def check_version(): + """ + Log error and exit when the installed version of paddlepaddle is + not satisfied. + """ + err = "PaddlePaddle version 1.6 or higher is required, " \ + "or a suitable develop version is satisfied as well. \n" \ + "Please make sure the version is good with your code." \ + + try: + fluid.require_version('1.6.0') + except Exception as e: + logger.error(err) + sys.exit(1) diff --git a/applications/VideoTag/videotag_test.py b/applications/VideoTag/videotag_test.py new file mode 100644 index 0000000000000000000000000000000000000000..b83e19ca3b7be63d2c264da63956082857b32542 --- /dev/null +++ b/applications/VideoTag/videotag_test.py @@ -0,0 +1,235 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import os +import sys +import time +import logging +import argparse +import ast +import numpy as np +import paddle.fluid as fluid + +from utils.config_utils import * +import models +from reader import get_reader +from metrics import get_metrics +from utils.utility import check_cuda +from utils.utility import check_version + +logging.root.handlers = [] +FORMAT = '[%(levelname)s: %(filename)s: %(lineno)4d]: %(message)s' +logging.basicConfig(level=logging.INFO, format=FORMAT, stream=sys.stdout) +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--extractor_config', + type=str, + default='configs/tsn.yaml', + help='path to config file of model') + parser.add_argument('--extractor_name', + type=str, + default='TSN', + help='extractor model name, default TSN') + parser.add_argument('--predictor_config', + '--pconfig', + type=str, + default='configs/attention_lstm.yaml', + help='path to config file of model') + parser.add_argument( + '--predictor_name', + '--pname', + type=str, + default='AttentionLSTM', + help='predictor model name, as AttentionLSTM, AttentionCluster, NEXTVLAD' + ) + parser.add_argument('--use_gpu', + type=ast.literal_eval, + default=True, + help='default use gpu.') + parser.add_argument('--extractor_weights', + type=str, + default='weights/tsn', + help='extractor weight path') + parser.add_argument('--predictor_weights', + '--pweights', + type=str, + default='weights/attention_lstm', + help='predictor weight path') + parser.add_argument('--filelist', + type=str, + default='./data/VideoTag_test.list', + help='path of video data, multiple video') + parser.add_argument('--save_dir', + type=str, + default='data/VideoTag_results', + help='output file path') + parser.add_argument('--label_file', + type=str, + default='label_3396.txt', + help='chinese label file path') + + args = parser.parse_args() + return args + + +def main(): + """ + Video classification model of 3000 Chinese tags. + videotag_extractor_prdictor (as videotag_TSN_AttentionLSTM) + two stages in our model: + 1. extract feature from input video(mp4 format) using extractor + 2. predict classification results from extracted feature using predictor + we implement this using two name scopes, ie. extractor_scope and predictor_scope. + """ + + if not os.path.isdir(args.save_dir): + os.makedirs(args.save_dir) + extractor_config = parse_config(args.extractor_config) + extractor_infer_config = merge_configs(extractor_config, 'infer', + vars(args)) + extractor_start_time = time.time() + extractor_scope = fluid.Scope() + with fluid.scope_guard(extractor_scope): + extractor_startup_prog = fluid.Program() + extractor_main_prog = fluid.Program() + with fluid.program_guard(extractor_main_prog, extractor_startup_prog): + with fluid.unique_name.guard(): + # build model + extractor_model = models.get_model(args.extractor_name, + extractor_infer_config, + mode='infer', + is_videotag=True) + extractor_model.build_input(use_dataloader=False) + extractor_model.build_model() + extractor_feeds = extractor_model.feeds() + extractor_fetch_list = extractor_model.fetches() + + place = fluid.CUDAPlace(0) if args.use_gpu else fluid.CPUPlace() + exe = fluid.Executor(place) + + exe.run(extractor_startup_prog) + + logger.info('load extractor weights from {}'.format( + args.extractor_weights)) + + extractor_model.load_pretrain_params(exe, + args.extractor_weights, + extractor_main_prog) + + # get reader and metrics + extractor_reader = get_reader(args.extractor_name, 'infer', + extractor_infer_config) + extractor_feeder = fluid.DataFeeder(place=place, + feed_list=extractor_feeds) + + feature_list = [] + file_list = [] + for idx, data in enumerate(extractor_reader()): + file_id = [item[-1] for item in data] + feed_data = [item[:-1] for item in data] + feature_out = exe.run(fetch_list=extractor_fetch_list, + feed=extractor_feeder.feed(feed_data)) + feature_list.append(feature_out[0]) #get out from list + file_list.append(file_id) + logger.info( + '========[Stage 1 Sample {} ] Extractor finished======'. + format(idx)) + extractor_end_time = time.time() + print('extractor_time', extractor_end_time - extractor_start_time) + + predictor_config = parse_config(args.predictor_config) + predictor_infer_config = merge_configs(predictor_config, 'infer', + vars(args)) + + # get Predictor input from Extractor output + predictor_feed_list = [] + for i in range(len(feature_list)): + feature_out = feature_list[i] + if args.predictor_name == "AttentionCluster": + extractor_seg_num = extractor_infer_config.INFER.seg_num + predictor_seg_num = predictor_infer_config.MODEL.seg_num + idxs = [] + stride = float(extractor_seg_num) / predictor_seg_num + for j in range(predictor_seg_num): + pos = (j + np.random.random()) * stride + idxs.append(min(extractor_seg_num - 1, int(pos))) + extractor_feature = feature_out[:, idxs, :].astype( + float) # get from bs dim + else: + extractor_feature = feature_out.astype(float) + predictor_feed_data = [extractor_feature] + predictor_feed_list.append((predictor_feed_data, file_list[i])) + + predictor_start_time = time.time() + predictor_scope = fluid.Scope() + with fluid.scope_guard(predictor_scope): + predictor_startup_prog = fluid.Program() + predictor_main_prog = fluid.Program() + with fluid.program_guard(predictor_main_prog, predictor_startup_prog): + with fluid.unique_name.guard(): + # parse config + predictor_model = models.get_model(args.predictor_name, + predictor_infer_config, + mode='infer') + predictor_model.build_input(use_dataloader=False) + predictor_model.build_model() + predictor_feeds = predictor_model.feeds() + + exe.run(predictor_startup_prog) + + logger.info('load predictor weights from {}'.format( + args.predictor_weights)) + predictor_model.load_test_weights(exe, args.predictor_weights, + predictor_main_prog) + + predictor_feeder = fluid.DataFeeder(place=place, + feed_list=predictor_feeds) + predictor_fetch_list = predictor_model.fetches() + predictor_metrics = get_metrics(args.predictor_name.upper(), + 'infer', predictor_infer_config) + predictor_metrics.reset() + + for idx, data in enumerate(predictor_feed_list): + file_id = data[1] + predictor_feed_data = data[0] + final_outs = exe.run( + fetch_list=predictor_fetch_list, + feed=predictor_feeder.feed(predictor_feed_data)) + logger.info( + '=======[Stage 2 Sample {} ] Predictor finished========' + .format(idx)) + final_result_list = [item + for item in final_outs] + [file_id] + + predictor_metrics.accumulate(final_result_list) + predictor_metrics.finalize_and_log_out( + savedir=args.save_dir, label_file=args.label_file) + predictor_end_time = time.time() + print('predictor_time', predictor_end_time - predictor_start_time) + + +if __name__ == '__main__': + start_time = time.time() + args = parse_args() + print(args) + check_cuda(args.use_gpu) + check_version() + logger.info(args) + main() + end_time = time.time() + period = end_time - start_time + print('[INFER] infer finished. cost time: {}'.format(period)) diff --git a/benchmark/TimeSformer/README.md b/benchmark/TimeSformer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d2bb20204eb10a9aa01e9d41927e34ee8252a246 --- /dev/null +++ b/benchmark/TimeSformer/README.md @@ -0,0 +1,14 @@ +执行 +```bash +bash ./run_all.sh down_data +``` +即可运行. + +run_all.sh内部的执行步骤: +1. cd 到 ../../ (也就是 PaddleVideo 目录) +2. 切换到benchmark_dev分支 +3. 安装 PaddleVideo 所需依赖 +4. cd 回PaddleVideo/data/ucf101 +5. wget下载数据集并解压缩,并下载预训练权重放到data目录下 +6. 再次cd 回到 ../../ (也就是 PaddleVideo 目录) +8. 按照不同的参数执行 run_benchmark.sh 脚本 diff --git a/benchmark/TimeSformer/run_all.sh b/benchmark/TimeSformer/run_all.sh new file mode 100644 index 0000000000000000000000000000000000000000..c7646f361151bee843af382775c041f6cb9fa685 --- /dev/null +++ b/benchmark/TimeSformer/run_all.sh @@ -0,0 +1,57 @@ +# 提供可稳定复现性能的脚本,默认在标准docker环境内py37执行: paddlepaddle/paddle:latest-gpu-cuda10.2-cudnn7 paddle=2.1.2 py=37 +# 执行目录:需说明 +sed -i '/set\ -xe/d' run_benchmark.sh +cd ../../ # cd到PaddleVideo项目根目录下 +git checkout benchmark_dev +log_path=${LOG_PATH_INDEX_DIR:-$(pwd)} # benchmark系统指定该参数,不需要跑profile时,log_path指向存speed的目录 + +# 1 安装该模型需要的依赖 (如需开启优化策略请注明) +python -m pip install -r requirements.txt + +# 2 拷贝该模型需要数据、预训练模型 +unalias cp +cp -f benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs1.yaml configs/recognition/timesformer/ +cp -f benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs1_mp.yaml configs/recognition/timesformer/ +cp -f benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs14.yaml configs/recognition/timesformer/ +cp -f benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs14_mp.yaml configs/recognition/timesformer/ +if [ ! -f "data/ucf101/trainlist_benchmark_mp.txt" ]; then + wget -P data/ucf101/ https://videotag.bj.bcebos.com/PaddleVideo-release2.2/trainlist_benchmark_mp.txt +fi +wget -P data/ https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams +alias cp='cp -i' + +cd data/ucf101 # 进入PaddleVideo/data/ucf101 +if [ $1 = "down_data" ];then + wget --no-check-certificate "https://www.crcv.ucf.edu/data/UCF101/UCF101.rar" # 下载训练数据 + unrar x UCF101.rar # 解压 + mv ./UCF-101 ./videos # 重命名文件夹为./videos + rm -rf ./UCF101.rar +else # 使用本地数据 + rm -rf videos + ln -s ${data_path}/dygraph_data/TSM/ucf101/videos ./videos +fi +cd ../../ # 返回PaddleVideo + +# 3 批量运行(如不方便批量,1,2需放到单个模型中) + +model_mode_list=(TimeSformer) +fp_item_list=(fp32 fp16) +bs_item_list=(1) # 14 +for model_mode in ${model_mode_list[@]}; do + for fp_item in ${fp_item_list[@]}; do + for bs_item in ${bs_item_list[@]} + do + run_mode=sp + log_name=video_${model_mode}_${run_mode}_bs${bs_item}_${fp_item} # 如:clas_MobileNetv1_mp_bs32_fp32_8 + echo "index is speed, 1gpus, begin, ${log_name}" + CUDA_VISIBLE_DEVICES=0 bash benchmark/${model_mode}/run_benchmark.sh ${run_mode} ${bs_item} ${fp_item} ${model_mode} | tee ${log_path}/${log_name}_speed_1gpus 2>&1 + sleep 60 + + run_mode=mp + log_name=video_${model_mode}_${run_mode}_bs${bs_item}_${fp_item} # 如:clas_MobileNetv1_mp_bs32_fp32_8 + echo "index is speed, 8gpus, run_mode is multi_process, begin, ${log_name}" + CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 bash benchmark/${model_mode}/run_benchmark.sh ${run_mode} ${bs_item} ${fp_item} ${model_mode} | tee ${log_path}/${log_name}_speed_8gpus8p 2>&1 + sleep 60 + done + done +done diff --git a/benchmark/TimeSformer/run_benchmark.sh b/benchmark/TimeSformer/run_benchmark.sh new file mode 100644 index 0000000000000000000000000000000000000000..fd50dbde10f60be8d7de80ceb4632d999aff7d69 --- /dev/null +++ b/benchmark/TimeSformer/run_benchmark.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -xe +# 运行示例:CUDA_VISIBLE_DEVICES=0 bash run_benchmark.sh ${run_mode} ${bs_item} ${fp_item} 500 ${model_mode} +# 参数说明 +function _set_params(){ + run_mode=${1:-"sp"} # 单卡sp|多卡mp + batch_size=${2:-"1"} + fp_item=${3:-"fp32"} # fp32|fp16 + model_item=${4:-"model_item"} + run_log_path=${TRAIN_LOG_DIR:-$(pwd)} # TRAIN_LOG_DIR 后续QA设置该参数 +# 添加benchmark日志解析所需参数 + base_batch_size=${batch_size} + mission_name="视频分类" + direction_id="0" + ips_unit="instance/sec" + skip_steps=10 # 解析日志,有些模型前几个step耗时长,需要跳过 (必填) + keyword="ips:" # 解析日志,筛选出数据所在行的关键字 (必填) + index="1" + model_name=${model_item}_bs${batch_size}_${fp_item} + +# 以下不用修改 + device=${CUDA_VISIBLE_DEVICES//,/ } + arr=(${device}) + num_gpu_devices=${#arr[*]} + log_file=${run_log_path}/${model_item}_${run_mode}_bs${batch_size}_${fp_item}_${num_gpu_devices} +} +function _train(){ + echo "Train on ${num_gpu_devices} GPUs" + echo "current CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES, gpus=$num_gpu_devices, batch_size=$batch_size" + + case ${run_mode} in + sp) + if [ ${fp_item} == 'fp32' ]; then + train_cmd="python -u main.py -c configs/recognition/timesformer/timesformer_ucf101_videos_benchmark_bs${batch_size}.yaml" + elif [ ${fp_item} == 'fp16' ]; then + train_cmd="python -u main.py --amp -c configs/recognition/timesformer/timesformer_ucf101_videos_benchmark_bs${batch_size}.yaml" + else + echo "choose fp_item(fp32 or fp16)" + exit 1 + fi;; + mp) + rm -rf ./mylog + if [ ${fp_item} == 'fp32' ]; then + train_cmd="python -u -B -m paddle.distributed.launch --gpus=$CUDA_VISIBLE_DEVICES --log_dir=./mylog main.py \ + -c configs/recognition/timesformer/timesformer_ucf101_videos_benchmark_bs${batch_size}_mp.yaml" + log_parse_file="mylog/workerlog.0" + elif [ ${fp_item} == 'fp16' ]; then + train_cmd="python -u -B -m paddle.distributed.launch --gpus=$CUDA_VISIBLE_DEVICES --log_dir=./mylog main.py --amp \ + -c configs/recognition/timesformer/timesformer_ucf101_videos_benchmark_bs${batch_size}_mp.yaml" + log_parse_file="mylog/workerlog.0" + else + echo "choose fp_item(fp32 or fp16)" + exit 1 + fi;; + *) echo "choose run_mode(sp or mp)"; exit 1; + esac +# 以下不用修改 + timeout 15m ${train_cmd} > ${log_file} 2>&1 + if [ $? -ne 0 ];then + echo -e "${model_name}, FAIL" + export job_fail_flag=1 + else + echo -e "${model_name}, SUCCESS" + export job_fail_flag=0 + fi + kill -9 `ps -ef|grep 'python'|awk '{print $2}'` + + if [ $run_mode = "mp" -a -d mylog ]; then + rm ${log_file} + cp mylog/workerlog.0 ${log_file} + fi +} + +source ${BENCHMARK_ROOT}/scripts/run_model.sh # 在该脚本中会对符合benchmark规范的log使用analysis.py 脚本进行性能数据解析;该脚本在连调时可从benchmark repo中下载https://github.com/PaddlePaddle/benchmark/blob/master/scripts/run_model.sh;如果不联调只想要产出训练log可以注掉本行,提交时需打开 +_set_params $@ +# _train # 如果只想产出训练log,不解析,可取消注释 +_run # 该函数在run_model.sh中,执行时会调用_train; 如果不联调只想要产出训练log可以注掉本行,提交时需打开 + diff --git a/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs1.yaml b/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs1.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5f47693be5e088c3cda4b8b905643fca01d72460 --- /dev/null +++ b/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs1.yaml @@ -0,0 +1,145 @@ +MODEL: #MODEL field + framework: "RecognizerTransformer" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "VisionTransformer" #Mandatory, The name of backbone. + pretrained: "data/ViT_base_patch16_224_pretrained.pdparams" #Optional, pretrained model path. + img_size: 224 + patch_size: 16 + in_channels: 3 + embed_dim: 768 + depth: 12 + num_heads: 12 + mlp_ratio: 4 + qkv_bias: True + epsilon: 1e-6 + num_seg: 8 + attention_type: 'divided_space_time' + head: + name: "TimeSformerHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 101 #Optional, the number of classes to be classified. + in_channels: 768 #input channel of the extracted feature. + std: 0.02 #std value in params initialization + +DATASET: #DATASET field + batch_size: 1 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 8 + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + +PIPELINE: #PIPELINE field TODO..... + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'train' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + linspace_sample: True + transform: #Mandotary, image transform operator. + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'valid' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False # It is indeed False when verifying + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + test: + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'test' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 224 + max_size: 224 + - UniformCrop: + target_size: 224 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + learning_rate: 0.0025 # Applicable when global batch size=64 + name: 'MultiStepDecay' + milestones: [11, 14] + gamma: 0.1 + weight_decay: + name: 'L2' + value: 0.0001 + use_nesterov: True + +GRADIENT_ACCUMULATION: + global_batch_size: 64 # Specify the sum of batches to be calculated by all GPUs + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'TimeSformer_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TimeSformer_ucf101" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 20 +epochs: 1 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs14.yaml b/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs14.yaml new file mode 100644 index 0000000000000000000000000000000000000000..250fbc157c3ce199023ca0b8096ad45a72003dfc --- /dev/null +++ b/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs14.yaml @@ -0,0 +1,145 @@ +MODEL: #MODEL field + framework: "RecognizerTransformer" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "VisionTransformer" #Mandatory, The name of backbone. + pretrained: "data/ViT_base_patch16_224_pretrained.pdparams" #Optional, pretrained model path. + img_size: 224 + patch_size: 16 + in_channels: 3 + embed_dim: 768 + depth: 12 + num_heads: 12 + mlp_ratio: 4 + qkv_bias: True + epsilon: 1e-6 + num_seg: 8 + attention_type: 'divided_space_time' + head: + name: "TimeSformerHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 101 #Optional, the number of classes to be classified. + in_channels: 768 #input channel of the extracted feature. + std: 0.02 #std value in params initialization + +DATASET: #DATASET field + batch_size: 14 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 8 + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + +PIPELINE: #PIPELINE field TODO..... + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'train' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + linspace_sample: True + transform: #Mandotary, image transform operator. + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'valid' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False # It is indeed False when verifying + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + test: + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'test' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 224 + max_size: 224 + - UniformCrop: + target_size: 224 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + learning_rate: 0.0025 # Applicable when global batch size=64 + name: 'MultiStepDecay' + milestones: [11, 14] + gamma: 0.1 + weight_decay: + name: 'L2' + value: 0.0001 + use_nesterov: True + +GRADIENT_ACCUMULATION: + global_batch_size: 14 # Specify the sum of batches to be calculated by all GPUs + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'TimeSformer_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TimeSformer_ucf101" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 20 +epochs: 1 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs14_mp.yaml b/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs14_mp.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6f59028babe7dc0bc936fb4b931d79d374d477ac --- /dev/null +++ b/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs14_mp.yaml @@ -0,0 +1,145 @@ +MODEL: #MODEL field + framework: "RecognizerTransformer" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "VisionTransformer" #Mandatory, The name of backbone. + pretrained: "data/ViT_base_patch16_224_pretrained.pdparams" #Optional, pretrained model path. + img_size: 224 + patch_size: 16 + in_channels: 3 + embed_dim: 768 + depth: 12 + num_heads: 12 + mlp_ratio: 4 + qkv_bias: True + epsilon: 1e-6 + num_seg: 8 + attention_type: 'divided_space_time' + head: + name: "TimeSformerHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 101 #Optional, the number of classes to be classified. + in_channels: 768 #input channel of the extracted feature. + std: 0.02 #std value in params initialization + +DATASET: #DATASET field + batch_size: 14 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 8 + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + +PIPELINE: #PIPELINE field TODO..... + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'train' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + linspace_sample: True + transform: #Mandotary, image transform operator. + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'valid' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False # It is indeed False when verifying + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + test: + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'test' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 224 + max_size: 224 + - UniformCrop: + target_size: 224 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + learning_rate: 0.0025 # Applicable when global batch size=64 + name: 'MultiStepDecay' + milestones: [11, 14] + gamma: 0.1 + weight_decay: + name: 'L2' + value: 0.0001 + use_nesterov: True + +GRADIENT_ACCUMULATION: + global_batch_size: 112 # Specify the sum of batches to be calculated by all GPUs + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'TimeSformer_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TimeSformer_ucf101" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 20 +epochs: 1 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs1_mp.yaml b/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs1_mp.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e52bc7dfb5f7297f924e2c0543d03dbe851eafb5 --- /dev/null +++ b/benchmark/TimeSformer/timesformer_ucf101_videos_benchmark_bs1_mp.yaml @@ -0,0 +1,145 @@ +MODEL: #MODEL field + framework: "RecognizerTransformer" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "VisionTransformer" #Mandatory, The name of backbone. + pretrained: "data/ViT_base_patch16_224_pretrained.pdparams" #Optional, pretrained model path. + img_size: 224 + patch_size: 16 + in_channels: 3 + embed_dim: 768 + depth: 12 + num_heads: 12 + mlp_ratio: 4 + qkv_bias: True + epsilon: 1e-6 + num_seg: 8 + attention_type: 'divided_space_time' + head: + name: "TimeSformerHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 101 #Optional, the number of classes to be classified. + in_channels: 768 #input channel of the extracted feature. + std: 0.02 #std value in params initialization + +DATASET: #DATASET field + batch_size: 1 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 8 + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, valid data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/ucf101/videos" + file_path: "data/ucf101/trainlist_benchmark_mp.txt" #Mandatory, valid data index file path + +PIPELINE: #PIPELINE field TODO..... + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'train' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + linspace_sample: True + transform: #Mandotary, image transform operator. + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'valid' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False # It is indeed False when verifying + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + test: + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'test' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 224 + max_size: 224 + - UniformCrop: + target_size: 224 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + learning_rate: 0.0025 # Applicable when global batch size=64 + name: 'MultiStepDecay' + milestones: [11, 14] + gamma: 0.1 + weight_decay: + name: 'L2' + value: 0.0001 + use_nesterov: True + +GRADIENT_ACCUMULATION: + global_batch_size: 64 # Specify the sum of batches to be calculated by all GPUs + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'TimeSformer_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TimeSformer_ucf101" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 20 +epochs: 1 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/detection/ava/ava.yaml b/configs/detection/ava/ava.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2fd09ab5c13dc95157215e0f5943e6f0639475a0 --- /dev/null +++ b/configs/detection/ava/ava.yaml @@ -0,0 +1,174 @@ +MODEL: #MODEL field + framework: "FastRCNN" + backbone: + name: "ResNetSlowFast" + depth: 50 + alpha: 4 + beta: 8 + width_per_group: 64 + fusion_kernel_sz: 5 + fuse_bn_relu: 0 + spatial_strides: [[1, 1], [2, 2], [2, 2], [1, 1]] + use_pool_af_s2: 0 + head: + name: "AVARoIHead" + bbox_roi_extractor: + name: "SingleRoIExtractor3D" + roi_layer_type: "RoIAlign" + output_size: 8 + with_temporal_pool: True + bbox_head: + name: 'BBoxHeadAVA' + in_channels : 2304 + num_classes : 81 + multilabel: True + dropout_ratio: 0.5 + assigner: + name: 'MaxIoUAssignerAVA' + pos_iou_thr: 0.9 + neg_iou_thr: 0.9 + min_pos_iou: 0.9 + sampler: + name: 'RandomSampler' + num: 32 + pos_fraction: 1 + neg_pos_ub: -1 + add_gt_as_proposals: True + pos_weight: 1.0 + action_thr: 0.0 + +DATASET: #DATASET field + batch_size: 10 #single card bacth size + valid_batch_size: 1 + test_batch_size: 1 #Optional, test batch size per gpu. + num_workers: 1 + train: + format: "AVADataset" #the Class name of the dataset used by youerself + data_prefix: 'data/ava/rawframes' #Mandatory, train data root path + file_path: "data/ava/annotations/ava_train_v2.1.csv" #Mandatory, train data index file path + exclude_file: "data/ava/annotations/ava_train_excluded_timestamps_v2.1.csv" + proposal_file: "data/ava/annotations/ava_dense_proposals_train.FAIR.recall_93.9.pkl" + label_file: "data/ava/annotations/ava_action_list_v2.1_for_activitynet_2018.pbtxt" + person_det_score_thr: 0.9 + num_max_proposals: 1000 + timestamp_start: 900 + timestamp_end: 1800 + valid: + format: "AVADataset" + data_prefix: 'data/ava/rawframes' #Mandatory, train data root path + file_path: "data/ava/annotations/ava_val_v2.1.csv" + exclude_file: "data/ava/annotations/ava_val_excluded_timestamps_v2.1.csv" + proposal_file: "data/ava/annotations/ava_dense_proposals_val.FAIR.recall_93.9.pkl" + label_file: "data/ava/annotations/ava_action_list_v2.1_for_activitynet_2018.pbtxt" + person_det_score_thr: 0.9 + num_max_proposals: 1000 + timestamp_start: 900 + timestamp_end: 1800 + test_mode: True + test: + format: "AVADataset" #Mandotary, indicate the type of test dataset, please refer to the 'paddlevideo/loader/dataset'. + data_prefix: "data/ava/rawframes" #Optional, test data root path. + file_path: "data/ava/annotations/ava_val_v2.1.csv" #Mandotary, test data index file path. + exclude_file: "./data/ava_test_excluded_timestamps_v2.1.csv" + proposal_file: "data/ava/annotations/ava_dense_proposals_val.FAIR.recall_93.9.pkl" + label_file: "data/ava/annotations/ava_action_list_v2.1_for_activitynet_2018.pbtxt" + suffix: "{:05}.jpg" + person_det_score_thr: 0.9 + num_max_proposals: 1000 + timestamp_start: 900 + timestamp_end: 1800 + test_mode: True + +PIPELINE: + train: + sampler: + name: "SampleAVAFrames" + clip_len: 32 + frame_interval: 2 + decoder: + name: "RawFrameDecode" + transform: #Mandotary, image transfrom operator + - RandomRescale: + scale_range: (256,320) + - RandomCrop_v2: + size: 256 + - Flip: + flip_ratio: 0.5 + - Normalize: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + to_bgr: False + - PackOutput: + alpha: 4 + valid: + sampler: + name: "SampleAVAFrames" + clip_len: 32 + frame_interval: 2 + decoder: + name: "RawFrameDecode" + transform: #Mandotary, image transfrom operator + - Rescale: + scale_range: (-1, 256) + - Normalize: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + to_bgr: False + - PackOutput: + alpha: 4 + test: #Mandatory, indicate the pipeline to deal with the validing data. please refer to the 'paddlevideo/loader/pipelines/' + sample: + name: "SampleAVAFrames" #Sampler type. + clip_len: 32 + frame_interval: 2 + decoder: + name: "RawFrameDecode" + transform: #Mandatory, image transform operator. + - Rescale: + scale_range: (-1, 256) + - Normalize: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + to_bgr: False + - PackOutput: + alpha: 4 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + iter_step: True + name: 'CustomWarmupPiecewiseDecay' + warmup_epochs: 5 + warmup_start_lr: 0.0075 + step_base_lr: 0.075 + lrs: [1, 0.1, 0.01, 0.001, 0.0001, 0.00001] + gamma: 0.1 + steps: [0, 10, 15] + max_epoch: 20 + weight_decay: + name: 'L2' + value: 1e-5 + +METRIC: + name: 'AVAMetric' + file_path: "data/ava/annotations/ava_val_v2.1.csv" #Mandotary, test data index file path. + exclude_file: "data/ava/annotations/ava_val_excluded_timestamps_v2.1.csv" + label_file: "data/ava/annotations/ava_action_list_v2.1_for_activitynet_2018.pbtxt" + custom_classes: 81 + +INFERENCE: + name: 'AVA_SlowFast_FastRCNN_Inference_helper' + config_file_path: 'configs/detection/ava/ava.yaml' + detection_model_name: 'faster_rcnn/faster_rcnn_r50_fpn_1x_coco' + detection_model_weights: 'faster_rcnn_r50_fpn_1x_coco.pdparams' + predict_stepsize: 4 + num_frames: 32 + alpha: 4 + target_size: 256 + +model_name: AVA_SlowFast_FastRcnn +save_interval: 10 +val_interval: 1 +epochs: 10 #Mandatory, total epoch +log_level: "INFO" diff --git a/configs/detection/ava/ava_part.yaml b/configs/detection/ava/ava_part.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2026e08fd3253b90b146209287ba9b2ac72f0fb6 --- /dev/null +++ b/configs/detection/ava/ava_part.yaml @@ -0,0 +1,123 @@ +MODEL: #MODEL field + framework: "FastRCNN" + backbone: + name: "ResNetSlowFast" + depth: 50 + alpha: 4 + beta: 8 + width_per_group: 64 + fusion_kernel_sz: 5 + fuse_bn_relu: 0 + spatial_strides: [[1, 1], [2, 2], [2, 2], [1, 1]] + use_pool_af_s2: 0 + head: + name: "AVARoIHead" + bbox_roi_extractor: + name: "SingleRoIExtractor3D" + roi_layer_type: "RoIAlign" + output_size: 8 + with_temporal_pool: True + bbox_head: + name: 'BBoxHeadAVA' + in_channels : 2304 + num_classes : 81 + multilabel: True + dropout_ratio: 0.5 + assigner: + name: 'MaxIoUAssignerAVA' + pos_iou_thr: 0.9 + neg_iou_thr: 0.9 + min_pos_iou: 0.9 + sampler: + name: 'RandomSampler' + num: 32 + pos_fraction: 1 + neg_pos_ub: -1 + add_gt_as_proposals: True + pos_weight: 1.0 + action_thr: 0.0 +DATASET: #DATASET field + batch_size: 10 #single card bacth size + valid_batch_size: 1 + num_workers: 1 + train: + format: "AVADataset" #the Class name of the dataset used by youerself + data_prefix: 'PATH_TO_AVA_FRAMES_OF_15min_VIDEOS' #Mandatory, train data root path + file_path: "./data/ava/annotations/ava_train_v2.1_99320.csv" #Mandatory, train data index file path + exclude_file: "./data/ava/annotations/ava_train_excluded_timestamps_v2.1.csv" + proposal_file: "./data/ava/annotations/ava_dense_proposals_train.FAIR.recall_93.9.pkl" + label_file: "./data/ava/annotations/ava_action_list_v2.1_for_activitynet_2018.pbtxt" + person_det_score_thr: 0.9 + num_max_proposals: 1000 + timestamp_start: 900 + timestamp_end: 1800 + valid: + format: "AVADataset" + data_prefix: 'PATH_TO_AVA_FRAMES_OF_15min_VIDEOS' #Mandatory, train data root path + file_path: "./data/ava/annotations/ava_val_v2.1_head27163.csv" + exclude_file: "./data/ava/annotations/ava_val_excluded_timestamps_v2.1.csv" + proposal_file: "./data/ava/annotations/ava_dense_proposals_val.FAIR.recall_93.9.pkl" + label_file: "./data/ava/annotations/ava_action_list_v2.1_for_activitynet_2018.pbtxt" + person_det_score_thr: 0.9 + num_max_proposals: 1000 + timestamp_start: 900 + timestamp_end: 1800 + test_mode: True +PIPELINE: + train: + sampler: + name: "SampleAVAFrames" + clip_len: 32 + frame_interval: 2 + decoder: + name: "RawFrameDecode" + transform: #Mandotary, image transfrom operator + - RandomRescale: + scale_range: (256,320) + - RandomCrop_v2: + size: 256 + - Flip: + flip_ratio: 0.5 + - Normalize: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + to_bgr: False + - PackOutput: + alpha: 4 + valid: + sampler: + name: "SampleAVAFrames" + clip_len: 32 + frame_interval: 2 + decoder: + name: "RawFrameDecode" + transform: #Mandotary, image transfrom operator + - Rescale: + scale_range: (-1, 256) + - Normalize: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + to_bgr: False + - PackOutput: + alpha: 4 +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + iter_step: True + name: 'CustomWarmupPiecewiseDecay' + warmup_epochs: 5 + warmup_start_lr: 0.0075 + step_base_lr: 0.075 + lrs: [1, 0.1, 0.01, 0.001, 0.0001, 0.00001] + gamma: 0.1 + steps: [0, 10, 15] + max_epoch: 20 + weight_decay: + name: 'L2' + value: 1e-5 +model_name: AVA_SlowFast_FastRcnn +save_interval: 10 +val_interval: 1 +epochs: 10 #Mandatory, total epoch +log_level: "INFO" diff --git a/configs/estimation/adds/adds.yaml b/configs/estimation/adds/adds.yaml new file mode 100644 index 0000000000000000000000000000000000000000..43bd259cb89815c41894c9ea259b88d3a41b8ac9 --- /dev/null +++ b/configs/estimation/adds/adds.yaml @@ -0,0 +1,135 @@ +MODEL: #MODEL field + framework: "DepthEstimator" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: 'ADDS_DepthNet' + pretrained: "data/Resnet18_Imagenet.pdparams" + num_layers: 18 + height: 256 + width: 512 + batch_size: 6 + frame_ids: [0, -1, 1] + pose_model_input: "pairs" + use_stereo: False + only_depth_encoder: False + scales: [0,1,2,3] + pose_model_type: 'separate_resnet' + min_depth: 0.1 + max_depth: 100.0 + v1_multiscale: False + predictive_mask: False + disable_automasking: False + head: + name: 'AddsHead' + avg_reprojection: False + disparity_smoothness: 0.001 + no_ssim: False + pred_depth_scale_factor: 1 + max_gt_depth: 40 + + +DATASET: #DATASET field + batch_size: 6 #Mandatory, bacth size + valid_batch_size: 1 + test_batch_size: 1 + num_workers: 2 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "MonoDataset" + file_path: "data/oxford/splits/oxford_day/train_files.txt" + data_prefix: "data/oxford/oxford_processing_forADDS" + valid: + format: "MonoDataset" + file_path: "data/oxford/splits/oxford_day/val_day_files.txt" + data_prefix: "data/oxford/oxford_processing_forADDS" + test: + format: "MonoDataset" + file_path: "data/oxford/splits/oxford_day/val_night_files.txt" + data_prefix: "data/oxford/oxford_processing_forADDS" + +PIPELINE: #PIPELINE field TODO..... + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "ImageDecoder" + backend: 'pil' + dataset: 'kitti' + frame_idxs: [0, -1, 1] + num_scales: 4 + side_map: {"2": 2, "3": 3, "l": 2, "r": 3} + full_res_shape: (640, 1280) + img_ext: ".png" + transform: + - GroupRandomFlip: + - GroupResize: + height: 256 + width: 512 + scale: 4 + K: [[0.768, 0, 0.5, 0], [0, 1.024, 0.5, 0], [0, 0, 1, 0], [0, 0, 0, 1]] + - ColorJitter: + brightness: 0.2 + contrast: 0.2 + saturation: 0.2 + hue: 0.1 + mode: "train" + - ToArray: + valid: + decode: + name: "ImageDecoder" + backend: 'pil' + dataset: 'kitti' + frame_idxs: [0] + num_scales: 4 + side_map: { "2": 2, "3": 3, "l": 2, "r": 3 } + full_res_shape: (1280, 640) + img_ext: ".png" + transform: + - GroupResize: + height: 256 + width: 512 + scale: 4 + K: [ [ 0.768, 0, 0.5, 0 ],[ 0, 1.024, 0.5, 0 ],[ 0, 0, 1, 0 ],[ 0, 0, 0, 1 ] ] + - ToArray: + test: + decode: + name: "ImageDecoder" + backend: 'pil' + dataset: 'kitti' + frame_idxs: [0] + num_scales: 4 + side_map: { "2": 2, "3": 3, "l": 2, "r": 3 } + full_res_shape: (1280, 640) + img_ext: ".png" + transform: + - GroupResize: + height: 256 + width: 512 + scale: 4 + K: [ [ 0.768, 0, 0.5, 0 ],[ 0, 1.024, 0.5, 0 ],[ 0, 0, 1, 0 ],[ 0, 0, 0, 1 ] ] + - ToArray: + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + learning_rate: + name : 'StepDecay' + learning_rate: 0.0001 + step_size: 14 + gamma: 0.1 + +METRIC: + name: 'DepthMetric' + +INFERENCE: + name: 'ADDS_Inference_helper' + frame_idxs: [0] + num_scales: 4 + side_map: { "2": 2, "3": 3, "l": 2, "r": 3 } + full_res_shape: (1280, 640) + img_ext: ".png" + height: 256 + width: 512 + num_channels: 3 + K: [[0.768, 0, 0.5, 0], [0, 1.024, 0.5, 0], [0, 0, 1, 0], [0, 0, 0, 1]] + +model_name: "ADDS" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 1 +epochs: 20 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/example.yaml b/configs/example.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9aa199a61c9317834e780f03dcf88521935e4c68 --- /dev/null +++ b/configs/example.yaml @@ -0,0 +1,114 @@ +# PaddleVieo Example Configuration, please refer to "docs/en/config.md" for more information. +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory ["Recognizer1D", "Recognizer2D", "Recognizer3D", "BMNLocalizer"], indicate the type of network, please refer to the 'paddlevideo/modeling/framework/'. + backbone: + name: "ResNet" #Optional, indicate the type of backbone, please refer to the 'paddlevideo/modeling/backbones/'. + pretrained: "data/ResNet50_vd_ssld_v2_pretrained.pdparams" #Optional, pretrained backbone params path. pass "" or " " without loading from files. + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSNHead" #Mandatory, indicate the type of head, please refer to the 'paddlevideo/modeling/heads' + num_classes: 101 #Optional, the number of classes to be classified. + in_channels: 2048 #Optional, input channels of the extracted feature. + drop_ratio: 0.4 #Optional, the ratio of dropout. + std: 0.01 #Optional, std value in params initialization. + +DATASET: #DATASET field + batch_size: 4 #Mandatory, batch size per gpu. + valid_batch_size: 4 #Optional, valid batch size per gpu. + test_batch_size: 4 #Optional, test batch size per gpu. + num_workers: 2 #Mandatory, the number of subprocess on each GPU. + train: + format: "VideoDataset" #Mandatory, indicate the type of train dataset, please refer to the 'paddlevidel/loader/dateset'. + data_prefix: "" #Optional, train data root path. + file_path: "data/ucf101/ucf101_train_split_1_videos.txt" #Mandatory, train data index file path + suffix: ".avi" + valid: + format: "VideoDataset" #Mandatory, indicate the type of valid dataset, please refer to the 'paddlevidel/loader/dateset' + data_prefix: "" #Optional, valid data root path + file_path: "data/ucf101/ucf101_val_split_1_videos.txt" #Mandatory, valid data index file path + suffix: ".avi" + test: + format: "VideoDataset" #Mandotary, indicate the type of test dataset, please refer to the 'paddlevideo/loader/dataset'. + data_prefix: "" #Optional, test data root path. + file_path: "data/ucf101/ucf101_val_split_1_videos.txt" #Mandotary, test data index file path. + suffix: ".avi" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, please refer to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" #Decoder type. + sample: + name: "Sampler" #Sampler type. + num_seg: 8 + seg_len: 1 + valid_mode: False + transform: #Mandotary, image transform operator + - Scale: + short_size: 256 + - RandomCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. please refer to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" #Decoder type. + sample: + name: "Sampler" #Sampler type. + valid_mode: True + num_seg: 8 + seg_len: 1 + transform: #Mandatory, image transform operator. + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: #Mandatory, indicate the pipeline to deal with the validing data. please refer to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" #Decoder type. + sample: + name: "Sampler" #Sampler type. + valid_mode: True + num_seg: 8 + seg_len: 1 + transform: #Mandatory, image transform operator. + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, please to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, please refer to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [30, 60] + values: [0.00025, 0.000025, 0.0000025] + weight_decay: + name: 'L2' + value: 1e-4 + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TSN" #Mandatory, model name. +log_interval: 20 #Optional, the interval of logger. +epochs: 5 #Mandatory, total epoch +log_level: "DEBUG" #Optional, the logger level. diff --git a/configs/localization/bmn.yaml b/configs/localization/bmn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c109fec76be01d7e5f233d6b205497f8ca09d87a --- /dev/null +++ b/configs/localization/bmn.yaml @@ -0,0 +1,100 @@ +MODEL: #MODEL field + framework: "BMNLocalizer" + backbone: + name: "BMN" + tscale: 100 + dscale: 100 + prop_boundary_ratio: 0.5 + num_sample: 32 + num_sample_perbin: 3 + loss: + name: "BMNLoss" + tscale: 100 + dscale: 100 + +DATASET: #DATASET field + batch_size: 4 #single card bacth size + test_batch_size: 1 + num_workers: 8 + train: + format: "BMNDataset" + file_path: "data/bmn_data/activitynet_1.3_annotations.json" + subset: "train" + valid: + format: "BMNDataset" + file_path: "data/bmn_data/activitynet_1.3_annotations.json" + subset: "validation" + test: + format: "BMNDataset" + test_mode: True + file_path: "data/bmn_data/activitynet_1.3_annotations.json" + subset: "validation" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + load_feat: + name: "LoadFeat" + feat_path: "data/bmn_data/fix_feat_100" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 100 + - GetVideoLabel: + tscale: 100 + dscale: 100 + + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + load_feat: + name: "LoadFeat" + feat_path: "data/bmn_data/fix_feat_100" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 100 + - GetVideoLabel: + tscale: 100 + dscale: 100 + + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + load_feat: + name: "LoadFeat" + feat_path: "data/bmn_data/fix_feat_100" + transform: #Mandotary, image transfrom operator + - GetMatchMap: + tscale: 100 + - GetVideoLabel: + tscale: 100 + dscale: 100 + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + iter_step: True + name: 'CustomPiecewiseDecay' + boundaries: [4200] + values: [0.001, 0.0001] + weight_decay: + name: 'L2' + value: 1e-4 + +METRIC: + name: 'BMNMetric' + tscale: 100 + dscale: 100 + file_path: "data/bmn_data/activitynet_1.3_annotations.json" + ground_truth_filename: "data/bmn_data/activity_net_1_3_new.json" + subset: "validation" + output_path: "data/bmn/BMN_Test_output" + result_path: "data/bmn/BMN_Test_results" + + +INFERENCE: + name: 'BMN_Inference_helper' + feat_dim: 400 + dscale: 100 + tscale: 100 + result_path: "data/bmn/BMN_INFERENCE_results" + + +model_name: BMN +epochs: 9 #Mandatory, total epoch +log_level: "INFO" +resume_from: "" #checkpoint path. diff --git a/configs/localization/bsn.yaml b/configs/localization/bsn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4f2e970e97a712a25f7860d90f20a7a035514ff8 --- /dev/null +++ b/configs/localization/bsn.yaml @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/configs/localization/ctcn.yaml b/configs/localization/ctcn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4f2e970e97a712a25f7860d90f20a7a035514ff8 --- /dev/null +++ b/configs/localization/ctcn.yaml @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/configs/multimodal/actbert/actbert.yaml b/configs/multimodal/actbert/actbert.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e0c718def3633bdb68972faf2a43f2d7b4712b23 --- /dev/null +++ b/configs/multimodal/actbert/actbert.yaml @@ -0,0 +1,48 @@ +MODEL: + framework: "ActBert" + backbone: + name: "BertForMultiModalPreTraining" + pretrained: "./bert_for_actbert.pdparams" + loss: + name: "ActBertLoss" + + +DATASET: #DATASET field + num_workers: 0 + batch_size: 8 + train: + format: "ActBertDataset" + file_path: "data/howto100m/actbert_train_data.npy" + valid: + format: "ActBertDataset" + file_path: "data/howto100m/my_data.npy" + +PIPELINE: #PIPELINE field + train: + transform: + - FeaturePadding: + - RandomCap: + caption_path: "data/howto100m/caption_train.json" + - Tokenize: + - RandomMask: + valid: + transform: + - FeaturePadding: + - RandomCap: + caption_path: "data/howto100m/caption_val.json" + - Tokenize: + - RandomMask: + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + iter_step: True + name: 'CustomPiecewiseDecay' + boundaries: [4200] + values: [0.0002, 0.00002] + weight_decay: + name: 'L2' + value: 1e-4 + +model_name: "ActBert" +epochs: 10 diff --git a/configs/multimodal/actbert/actbert_msrvtt.yaml b/configs/multimodal/actbert/actbert_msrvtt.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7205400f7946df7b3ba465f5e39801811abce345 --- /dev/null +++ b/configs/multimodal/actbert/actbert_msrvtt.yaml @@ -0,0 +1,23 @@ +MODEL: + framework: "ActBert" + backbone: + name: "BertForMultiModalPreTraining" + + +DATASET: #DATASET field + num_workers: 4 + test_batch_size: 1 + test: + format: "MSRVTTDataset" + file_path: "./test_data/MSRVTT_JSFUSION_test.csv" + features_path: "./test_data/msrvtt_test.lmdb" + +PIPELINE: #PIPELINE field + test: + + +METRIC: + name: 'MSRVTTMetric' + + +model_name: "ActBert" diff --git a/configs/partitioners/transnetv2/transnetv2.yaml b/configs/partitioners/transnetv2/transnetv2.yaml new file mode 100644 index 0000000000000000000000000000000000000000..19823fbdd5d7462dfc5773c914d1dd4a6b883e70 --- /dev/null +++ b/configs/partitioners/transnetv2/transnetv2.yaml @@ -0,0 +1,55 @@ +MODEL: + framework: "TransNetV2Partitioner" + backbone: + name: "TransNetV2" + F: 16 + L: 3 + S: 2 + D: 1024 + use_many_hot_targets: True + use_frame_similarity: True + use_color_histograms: True + use_mean_pooling: False + dropout_rate: 0.5 + use_convex_comb_reg: False + use_resnet_features: False + use_resnet_like_top: False + frame_similarity_on_last_layer: False + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + head: + name: "TransNetV2Head" + num_classes: 2 # not used + in_channels: 3 # not used + + +# OPTIMIZER: #OPTIMIZER field +# name: 'Momentum' +# momentum: 0.9 +# learning_rate: +# name: 'LRScheduler' +# values: 0.01 +# weight_decay: +# name: 'L2' +# value: 0.0001 +# grad_clip: +# name: 'ClipGradByGlobalNorm' +# value: 10.0 + +# METRIC: +# name: 'TransNetV2Metric' + + +INFERENCE: + name: 'TransNetV2_Inference_helper' + num_frames: 100 + height: 27 + width: 48 + num_channels: 3 + threshold: 0.5 + output_path: inference_results + visualize: True + + +model_name: "TransNetV2" +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/agcn/agcn_fsd.yaml b/configs/recognition/agcn/agcn_fsd.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1340d40f71e4381148092e7993ce54943d6c5622 --- /dev/null +++ b/configs/recognition/agcn/agcn_fsd.yaml @@ -0,0 +1,72 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "AGCN" #Mandatory, The name of backbone. + head: + name: "STGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 30 #Optional, the number of classes to be classified. + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 64 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "train_data.npy" #Mandatory, train data index file path + label_path: "train_label.npy" + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "test_A_data.npy" #Mandatory, valid data index file path + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "AutoPadding" + window_size: 350 + transform: #Mandotary, image transfrom operator + - SkeletonNorm: + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "AutoPadding" + window_size: 350 + transform: #Mandotary, image transfrom operator + - SkeletonNorm: + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 100 + warmup_epochs: 10 + warmup_start_lr: 0.005 + cosine_base_lr: 0.05 + weight_decay: + name: 'L2' + value: 1e-4 + +MIX: + name: "Mixup" + alpha: 0.2 + + +METRIC: + name: 'SkeletonMetric' + out_file: 'submission.csv' + + +INFERENCE: + name: 'STGCN_Inference_helper' + num_channels: 2 + window_size: 350 + vertex_nums: 25 + person_nums: 1 + + +model_name: "AGCN" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 100 #Mandatory, total epoch diff --git a/configs/recognition/agcn/agcn_ntucs.yaml b/configs/recognition/agcn/agcn_ntucs.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4a8f0c1c38b43f0f54402bf1c3aec0d26355c114 --- /dev/null +++ b/configs/recognition/agcn/agcn_ntucs.yaml @@ -0,0 +1,67 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "AGCN" #Mandatory, The name of backbone. + in_channels: 3 + head: + name: "STGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 60 #Optional, the number of classes to be classified. + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "NTU-RGB-D/xsub/train_data.npy" #Mandatory, train data index file path + label_path: "NTU-RGB-D/xsub/train_label.pkl" + valid: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "NTU-RGB-D/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "NTU-RGB-D/xsub/val_label.pkl" + test_mode: True + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "NTU-RGB-D/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "NTU-RGB-D/xsub/val_label.pkl" + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + test: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 60 + warmup_epochs: 10 + warmup_start_lr: 0.01 + cosine_base_lr: 0.1 + weight_decay: + name: 'L2' + value: 1e-4 + +MIX: + name: "Mixup" + alpha: 0.2 + + +METRIC: + name: 'SkeletonMetric' + + +model_name: "AGCN" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 60 #Mandatory, total epoch diff --git a/configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml b/configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml new file mode 100644 index 0000000000000000000000000000000000000000..66cb13381665ddc5f11bef1a8b27b2b23066ff32 --- /dev/null +++ b/configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml @@ -0,0 +1,71 @@ +MODEL: #MODEL field + framework: "Recognizer1D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + head: + name: "AttentionLstmHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 3862 #Optional, the number of classes to be classified. + feature_num: 2 + feature_dims: [1024, 128] + embedding_size: 512 + lstm_size: 1024 + +DATASET: #DATASET field + batch_size: 128 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + shuffle_valid: True + train: + format: "FeatureDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/yt8m" + file_path: "data/yt8m/train.list" #Mandatory, train data index file path + valid: + format: "FeatureDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/yt8m" + file_path: "data/yt8m/val.list" #Mandatory, train data index file path + test: + format: "FeatureDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/yt8m" + file_path: "data/yt8m/val.list" #Mandatory, train data index file path + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FeatureDecoder" #"VideoDecoder" + num_classes: 3862 + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FeatureDecoder" #"VideoDecoder" + num_classes: 3862 + test: + decode: + name: "FeatureDecoder" #"VideoDecoder" + num_classes: 3862 + +OPTIMIZER: #OPTIMIZER field + name: 'RMSProp' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + centered: True + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [5, 8] + values: [0.00025, 0.000025, 0.0000025] + weight_decay: + name: 'L2' + value: 0.0004 + +METRIC: + name: 'HitOneMetric' + num_class: 3862 + top_k: 5 + +INFERENCE: + name: 'AttentionLSTM_Inference_helper' + num_classes: 3862 #Optional, the number of classes to be classified. + feature_num: 2 + feature_dims: [1024, 128] + embedding_size: 512 + lstm_size: 1024 + +model_name: "AttentionLSTM" +log_interval: 20 #Optional, the interal of logger, default:10 +epochs: 10 #Mandatory, total epoch +save_interval: 2 +log_level: "INFO" diff --git a/configs/recognition/ctrgcn/ctrgcn_ntucs_bone.yaml b/configs/recognition/ctrgcn/ctrgcn_ntucs_bone.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5f8033803b4b52b7e7011fe5fd40f66a072c3deb --- /dev/null +++ b/configs/recognition/ctrgcn/ctrgcn_ntucs_bone.yaml @@ -0,0 +1,112 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "CTRGCN" #Mandatory, The name of backbone. + num_point: 25 + num_person: 2 + base_channel: 64 + graph: "ntu_rgb_d" + graph_args: + labeling_mode: "spatial" + in_channels: 3 + adaptive: True + head: + name: "CTRGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 60 #Optional, the number of classes to be classified. + in_channels: 64 + drop_out: 0 + +DATASET: #DATASET field + batch_size: 64 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 64 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/train_data.npy" #Mandatory, train data index file path + label_path: "./data/ntu-rgb-d/xsub/train_label.pkl" + valid: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "./data/ntu-rgb-d/xsub/val_label.pkl" + test_mode: True + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "./data/ntu-rgb-d/xsub/val_label.pkl" + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.5, 1] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: True + - SketeonModalityTransform: + joint: Fasle + bone: True + motion: False + graph: 'ntu_rgb_d' + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.95] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: False + - SketeonModalityTransform: + joint: Fasle + bone: True + motion: False + graph: 'ntu_rgb_d' + test: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.95] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: False + - SketeonModalityTransform: + joint: Fasle + bone: True + motion: False + graph: 'ntu_rgb_d' + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + name: 'CustomWarmupAdjustDecay' + step_base_lr: 0.1 + warmup_epochs: 5 + lr_decay_rate: 0.1 + boundaries: [35, 55] + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + + +METRIC: + name: 'SkeletonMetric' + +INFERENCE: + name: 'CTRGCN_Inference_helper' + window_size: 64 + p_interval: [0.95] + num_channels: 3 + vertex_nums: 25 + person_nums: 2 + +model_name: "CTRGCN_bone" +log_interval: 100 #Optional, the interal of logger, default:10 +epochs: 65 #Mandatory, total epoch +save_interval: 10 diff --git a/configs/recognition/ctrgcn/ctrgcn_ntucs_bone_motion.yaml b/configs/recognition/ctrgcn/ctrgcn_ntucs_bone_motion.yaml new file mode 100644 index 0000000000000000000000000000000000000000..025c57c53e34d1e0b3795782d84385a1710a7bc7 --- /dev/null +++ b/configs/recognition/ctrgcn/ctrgcn_ntucs_bone_motion.yaml @@ -0,0 +1,112 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "CTRGCN" #Mandatory, The name of backbone. + num_point: 25 + num_person: 2 + base_channel: 64 + graph: "ntu_rgb_d" + graph_args: + labeling_mode: "spatial" + in_channels: 3 + adaptive: True + head: + name: "CTRGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 60 #Optional, the number of classes to be classified. + in_channels: 64 + drop_out: 0 + +DATASET: #DATASET field + batch_size: 64 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 64 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/train_data.npy" #Mandatory, train data index file path + label_path: "./data/ntu-rgb-d/xsub/train_label.pkl" + valid: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "./data/ntu-rgb-d/xsub/val_label.pkl" + test_mode: True + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "./data/ntu-rgb-d/xsub/val_label.pkl" + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.5, 1] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: True + - SketeonModalityTransform: + joint: Fasle + bone: True + motion: True + graph: 'ntu_rgb_d' + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.95] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: False + - SketeonModalityTransform: + joint: Fasle + bone: True + motion: True + graph: 'ntu_rgb_d' + test: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.95] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: False + - SketeonModalityTransform: + joint: Fasle + bone: True + motion: True + graph: 'ntu_rgb_d' + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + name: 'CustomWarmupAdjustDecay' + step_base_lr: 0.1 + warmup_epochs: 5 + lr_decay_rate: 0.1 + boundaries: [35, 55] + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + + +METRIC: + name: 'SkeletonMetric' + +INFERENCE: + name: 'CTRGCN_Inference_helper' + window_size: 64 + p_interval: [0.95] + num_channels: 3 + vertex_nums: 25 + person_nums: 2 + +model_name: "CTRGCN_bone_motion" +log_interval: 100 #Optional, the interal of logger, default:10 +epochs: 65 #Mandatory, total epoch +save_interval: 10 diff --git a/configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml b/configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e7a4659a72cff6ffa9afaa36713ace3a2b576d90 --- /dev/null +++ b/configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml @@ -0,0 +1,112 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "CTRGCN" #Mandatory, The name of backbone. + num_point: 25 + num_person: 2 + base_channel: 64 + graph: "ntu_rgb_d" + graph_args: + labeling_mode: "spatial" + in_channels: 3 + adaptive: True + head: + name: "CTRGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 60 #Optional, the number of classes to be classified. + in_channels: 64 + drop_out: 0 + +DATASET: #DATASET field + batch_size: 64 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 64 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/train_data.npy" #Mandatory, train data index file path + label_path: "./data/ntu-rgb-d/xsub/train_label.pkl" + valid: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "./data/ntu-rgb-d/xsub/val_label.pkl" + test_mode: True + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "./data/ntu-rgb-d/xsub/val_label.pkl" + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.5, 1] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: True + - SketeonModalityTransform: + joint: True + bone: Fasle + motion: False + graph: 'ntu_rgb_d' + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.95] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: False + - SketeonModalityTransform: + joint: True + bone: Fasle + motion: False + graph: 'ntu_rgb_d' + test: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.95] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: False + - SketeonModalityTransform: + joint: True + bone: Fasle + motion: False + graph: 'ntu_rgb_d' + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + name: 'CustomWarmupAdjustDecay' + step_base_lr: 0.1 + warmup_epochs: 5 + lr_decay_rate: 0.1 + boundaries: [35, 55] + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + + +METRIC: + name: 'SkeletonMetric' + +INFERENCE: + name: 'CTRGCN_Inference_helper' + window_size: 64 + p_interval: [0.95] + num_channels: 3 + vertex_nums: 25 + person_nums: 2 + +model_name: "CTRGCN_joint" +log_interval: 100 #Optional, the interal of logger, default:10 +epochs: 65 #Mandatory, total epoch +save_interval: 10 diff --git a/configs/recognition/ctrgcn/ctrgcn_ntucs_motion.yaml b/configs/recognition/ctrgcn/ctrgcn_ntucs_motion.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e71854b962c599490e98fa3a9efdbcdcb14d10fd --- /dev/null +++ b/configs/recognition/ctrgcn/ctrgcn_ntucs_motion.yaml @@ -0,0 +1,112 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "CTRGCN" #Mandatory, The name of backbone. + num_point: 25 + num_person: 2 + base_channel: 64 + graph: "ntu_rgb_d" + graph_args: + labeling_mode: "spatial" + in_channels: 3 + adaptive: True + head: + name: "CTRGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 60 #Optional, the number of classes to be classified. + in_channels: 64 + drop_out: 0 + +DATASET: #DATASET field + batch_size: 64 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 64 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/train_data.npy" #Mandatory, train data index file path + label_path: "./data/ntu-rgb-d/xsub/train_label.pkl" + valid: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "./data/ntu-rgb-d/xsub/val_label.pkl" + test_mode: True + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/ntu-rgb-d/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "./data/ntu-rgb-d/xsub/val_label.pkl" + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.5, 1] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: True + - SketeonModalityTransform: + joint: Fasle + bone: False + motion: True + graph: 'ntu_rgb_d' + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.95] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: False + - SketeonModalityTransform: + joint: Fasle + bone: False + motion: True + graph: 'ntu_rgb_d' + test: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + - Iden: + - SketeonCropSample: + window_size: 64 + p_interval: [0.95] + transform: #Mandotary, image transfrom operator + - RandomRotation: + argument: False + - SketeonModalityTransform: + joint: Fasle + bone: False + motion: True + graph: 'ntu_rgb_d' + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + name: 'CustomWarmupAdjustDecay' + step_base_lr: 0.1 + warmup_epochs: 5 + lr_decay_rate: 0.1 + boundaries: [35, 55] + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + + +METRIC: + name: 'SkeletonMetric' + +INFERENCE: + name: 'CTRGCN_Inference_helper' + window_size: 64 + p_interval: [0.95] + num_channels: 3 + vertex_nums: 25 + person_nums: 2 + +model_name: "CTRGCN_motion" +log_interval: 100 #Optional, the interal of logger, default:10 +epochs: 65 #Mandatory, total epoch +save_interval: 10 diff --git a/configs/recognition/movinet/movinet_k400_frame.yaml b/configs/recognition/movinet/movinet_k400_frame.yaml new file mode 100644 index 0000000000000000000000000000000000000000..acfa871472d6ff98d1089d051c65d9b345aeffe8 --- /dev/null +++ b/configs/recognition/movinet/movinet_k400_frame.yaml @@ -0,0 +1,122 @@ +MODEL: #MODEL field + framework: "MoViNetRecognizerFrame" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "MoViNet" #Mandatory, The name of backbone. + model_type: "A0" + causal: False #True + conv_type: "3d" + num_classes: 400 + head: + name: "MoViNetHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + +DATASET: #DATASET field + batch_size: 32 #128 #32 #Mandatory, bacth size + num_workers: 4 #0 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + #data_prefix: "data/k400/rawframes" #Mandatory, train data root path + file_path: "data/k400/train.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + #data_prefix: "data/k400/rawframes" #Mandatory, valid data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + #data_prefix: "data/k400/rawframes" #Mandatory, valid data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 50 + seg_len: 1 + valid_mode: False + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 192 + - MultiScaleCrop: + target_size: 192 + - RandomCrop: + target_size: 172 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 50 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 192 + - CenterCrop: + target_size: 172 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 50 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 192 + - CenterCrop: + target_size: 172 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'RMSProp' + momentum: 0.9 + rho: 0.9 + epsilon: 1.0 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 160 + warmup_epochs: 10 + warmup_start_lr: 0.001 + cosine_base_lr: 0.5 + weight_decay: + name: 'L2' + value: 0.00003 + + +METRIC: + name: 'CenterCropMetric' + + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 50 + short_size: 192 + target_size: 172 + +model_name: "MoViNet" +log_interval: 1 #20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 160 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/non_local/non_local.yaml b/configs/recognition/non_local/non_local.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4f2e970e97a712a25f7860d90f20a7a035514ff8 --- /dev/null +++ b/configs/recognition/non_local/non_local.yaml @@ -0,0 +1 @@ +PLACEHOLDER diff --git a/configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml b/configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..522c599ef5429aec16f2116b50d8adc15324b673 --- /dev/null +++ b/configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml @@ -0,0 +1,163 @@ +MODEL: #MODEL field + framework: "RecognizerTransformer" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "VisionTransformer_tweaks" #Mandatory, The name of backbone. + pretrained: "data/vit_base_patch16_224_miil_21k_trans.pdparams" #Optional, pretrained model path. + img_size: 224 + patch_size: 16 + in_channels: 3 + embed_dim: 768 + depth: 12 + num_heads: 12 + mlp_ratio: 4 + qkv_bias: False + epsilon: 1e-6 + num_seg: 8 + attention_type: 'divided_space_time' + head: + name: "ppTimeSformerHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 768 #input channel of the extracted feature. + std: 0.02 #std value in params initialization + ls_eps: 0.1 + runtime_cfg: # configuration used when the model is train or test. + test: # test config + num_seg: 8 + avg_type: 'prob' # 'score' or 'prob' + +DATASET: #DATASET field + batch_size: 8 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 1 + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" + file_path: "data/k400/train.list" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" + file_path: "data/k400/val.list" #Mandatory, valid data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" + file_path: "data/k400/val.list" #Mandatory, valid data index file path + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'train' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + linspace_sample: True + transform: #Mandotary, image transform operator. + - Normalization: + mean: [0, 0, 0] + std: [1, 1, 1] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'valid' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + linspace_sample: True + transform: + - Normalization: + mean: [0, 0, 0] + std: [1, 1, 1] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 256 + - CenterCrop: + target_size: 224 + + test: + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'test' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + linspace_sample: True + transform: + - Normalization: + mean: [0, 0, 0] + std: [1, 1, 1] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 224 + max_size: 224 + - UniformCrop: + target_size: 224 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 20 + warmup_epochs: 2 + warmup_start_lr: 0.00025 + cosine_base_lr: 0.0025 + weight_decay: + name: 'L2' + value: 0.00007 + use_nesterov: True + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 40.0 + +GRADIENT_ACCUMULATION: + global_batch_size: 64 # Specify the sum of batches to be calculated by all GPUs + +MIX: + name: "VideoMix" + cutmix_prob: 0.5 + mixup_alpha: 0.2 + cutmix_alpha: 1.0 + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'TimeSformer_Inference_helper' + num_seg: 8 + target_size: 224 + mean: [0, 0, 0] + std: [1, 1, 1] + +model_name: "ppTimeSformer" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 5 +epochs: 20 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/pptsm/pptsm_k400_frames_dense.yaml b/configs/recognition/pptsm/pptsm_k400_frames_dense.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a69083489cec78faabce8d2ca3f4a00ee98bf913 --- /dev/null +++ b/configs/recognition/pptsm/pptsm_k400_frames_dense.yaml @@ -0,0 +1,123 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTweaksTSM" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_vd_ssld_v2_pretrained.pdparams" #Optional, pretrained model path. + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "ppTSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 1 #Mandatory, test bacth size + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, train data root path + file_path: "data/k400_frames/train.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/k400_frames/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/k400_frames/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + dense_sample: True + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + - MultiScaleCrop: + target_size: 256 + - RandomCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + dense_sample: True + transform: + - Scale: + short_size: 256 + - GroupFullResSample: + crop_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 80 + warmup_epochs: 10 + warmup_start_lr: 0.01 + cosine_base_lr: 0.02 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + +MIX: + name: "Mixup" + alpha: 0.2 + +PRECISEBN: + preciseBN_interval: 5 # epoch interval to do preciseBN, default 1. + num_iters_preciseBN: 200 # how many batches used to do preciseBN, default 200. + + +METRIC: + name: 'CenterCropMetric' + +model_name: "ppTSM" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 80 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/pptsm/pptsm_k400_frames_dense_r101.yaml b/configs/recognition/pptsm/pptsm_k400_frames_dense_r101.yaml new file mode 100644 index 0000000000000000000000000000000000000000..690e1c2776e6f4ad0ecbcf8045406592fc0058a9 --- /dev/null +++ b/configs/recognition/pptsm/pptsm_k400_frames_dense_r101.yaml @@ -0,0 +1,123 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTweaksTSM" #Mandatory, The name of backbone. + pretrained: "data/ResNet101_vd_ssld_pretrained.pdparams" #Optional, pretrained model path. + depth: 101 #Optional, the depth of backbone architecture. + head: + name: "ppTSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 1 #Mandatory, test bacth size + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, train data root path + file_path: "data/k400_frames/train.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/k400_frames/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/k400_frames/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + dense_sample: True + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + - MultiScaleCrop: + target_size: 256 + - RandomCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + dense_sample: True + transform: + - Scale: + short_size: 256 + - GroupFullResSample: + crop_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 100 + warmup_epochs: 10 + warmup_start_lr: 0.01 + cosine_base_lr: 0.02 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + +MIX: + name: "Mixup" + alpha: 0.2 + +PRECISEBN: + preciseBN_interval: 5 # epoch interval to do preciseBN, default 1. + num_iters_preciseBN: 200 # how many batches used to do preciseBN, default 200. + + +METRIC: + name: 'CenterCropMetric' + +model_name: "ppTSM" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 100 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml b/configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml new file mode 100644 index 0000000000000000000000000000000000000000..afb65113555e3ca669db5044c05f2a83f30d3709 --- /dev/null +++ b/configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml @@ -0,0 +1,125 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTweaksTSM" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_vd_ssld_v2_pretrained.pdparams" #Optional, pretrained model path. + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "ppTSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, train data root path + file_path: "data/k400/train_frames.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, valid data root path + file_path: "data/k400/val_frames.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, valid data root path + file_path: "data/k400/val_frames.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + - MultiScaleCrop: + target_size: 256 + - RandomCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 80 + warmup_epochs: 10 + warmup_start_lr: 0.005 + cosine_base_lr: 0.01 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + +MIX: + name: "Mixup" + alpha: 0.2 + +PRECISEBN: + preciseBN_interval: 5 # epoch interval to do preciseBN, default 1. + num_iters_preciseBN: 200 # how many batches used to do preciseBN, default 200. + + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "ppTSM" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 80 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/pptsm/pptsm_k400_frames_uniform_quantization.yaml b/configs/recognition/pptsm/pptsm_k400_frames_uniform_quantization.yaml new file mode 100644 index 0000000000000000000000000000000000000000..38de26e68a93eeab3f916e38d755292bb856b464 --- /dev/null +++ b/configs/recognition/pptsm/pptsm_k400_frames_uniform_quantization.yaml @@ -0,0 +1,33 @@ +DATASET: + batch_size: 1 + num_workers: 0 + quant: + format: "FrameDataset" + data_prefix: "../../data/k400/rawframes" + file_path: "../../data/k400/val_small_frames.list" + suffix: 'img_{:05}.jpg' + +PIPELINE: + quant: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +inference_model_dir: "../../inference/ppTSM" +quant_output_dir: "../../inference/ppTSM/quant_model" + +model_name: "ppTSM" +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/pptsm/pptsm_k400_videos_uniform.yaml b/configs/recognition/pptsm/pptsm_k400_videos_uniform.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9588617fa5fe3b012fb1ece5920900136c82a023 --- /dev/null +++ b/configs/recognition/pptsm/pptsm_k400_videos_uniform.yaml @@ -0,0 +1,126 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTweaksTSM" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_vd_ssld_v2_pretrained.pdparams" #Optional, pretrained model path. + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "ppTSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/train.list" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + - MultiScaleCrop: + target_size: 256 + - RandomCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 80 + warmup_epochs: 10 + warmup_start_lr: 0.005 + cosine_base_lr: 0.01 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + +MIX: + name: "Mixup" + alpha: 0.2 + +PRECISEBN: + preciseBN_interval: 5 # epoch interval to do preciseBN, default 1. + num_iters_preciseBN: 200 # how many batches used to do preciseBN, default 200. + + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "ppTSM" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 80 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/pptsn/pptsn_k400_frames.yaml b/configs/recognition/pptsn/pptsn_k400_frames.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0a01f9b96cd5fa1fd037a57497421fce407efb37 --- /dev/null +++ b/configs/recognition/pptsn/pptsn_k400_frames.yaml @@ -0,0 +1,146 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTweaksTSN" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_vd_ssld_v2_pretrained.pdparams" #Optional, pretrained model path. + layers: 50 #Optional, the depth of backbone architecture. + head: + name: "ppTSNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.4 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + + +DATASET: #DATASET field + batch_size: 32 #Mandatory, bacth size + valid_batch_size: 32 + test_batch_size: 1 + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, train data root path + file_path: "data/k400_frames/train.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/k400_frames/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/k400_frames/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 3 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + more_fix_crop: True + backend: 'cv2' + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 3 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - CenterCrop: + target_size: 224 + do_round: False + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 25 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - TenCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 100 + warmup_epochs: 10 + warmup_start_lr: 0.005 + cosine_base_lr: 0.01 + weight_decay: + name: 'L2' + value: 0.0001 + use_nesterov: True + + +MIX: + name: "Mixup" + alpha: 0.2 + + +METRIC: + name: 'CenterCropMetric' + + +INFERENCE: + name: 'ppTSN_Inference_helper' + num_seg: 25 + target_size: 224 + + +model_name: "ppTSN" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 20 +epochs: 100 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/pptsn/pptsn_k400_videos.yaml b/configs/recognition/pptsn/pptsn_k400_videos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..331abefc38292074d5fcd3319a3ff256b0e0fb4b --- /dev/null +++ b/configs/recognition/pptsn/pptsn_k400_videos.yaml @@ -0,0 +1,145 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTweaksTSN" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_vd_ssld_v2_pretrained.pdparams" #Optional, pretrained model path. + layers: 50 #Optional, the depth of backbone architecture. + head: + name: "ppTSNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.4 #the ratio of dropout + std: 0.01 #std value in params initialization + ls_eps: 0.1 + + +DATASET: #DATASET field + batch_size: 32 #Mandatory, bacth size + valid_batch_size: 32 + test_batch_size: 1 + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/train.list" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 3 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + more_fix_crop: True + backend: 'cv2' + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 3 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - CenterCrop: + target_size: 224 + do_round: False + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 25 + seg_len: 1 + valid_mode: True + linspace_sample: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + backend: 'cv2' + - TenCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 100 + warmup_epochs: 10 + warmup_start_lr: 0.005 + cosine_base_lr: 0.01 + weight_decay: + name: 'L2' + value: 0.0001 + use_nesterov: True + + +MIX: + name: "Mixup" + alpha: 0.2 + + +METRIC: + name: 'CenterCropMetric' + + +INFERENCE: + name: 'ppTSN_Inference_helper' + num_seg: 25 + target_size: 224 + + +model_name: "ppTSN" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 20 +epochs: 100 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/slowfast/slowfast.yaml b/configs/recognition/slowfast/slowfast.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5117ba37092ba39bd474f73491d4eba169d8f436 --- /dev/null +++ b/configs/recognition/slowfast/slowfast.yaml @@ -0,0 +1,140 @@ +MODEL: #MODEL field + framework: "Recognizer3D" + backbone: + name: "ResNetSlowFast" + depth: 50 # Not Optional, only 50 now. + alpha: 8 + beta: 8 + width_per_group: 64 + fusion_kernel_sz: 5 + head: + name: "SlowFastHead" + width_per_group: 64 + alpha: 8 + beta: 8 + num_classes: 400 + num_frames: 32 + crop_size: 224 #independent to test or train mode + dropout_rate: 0.5 + +DATASET: #DATASET field + batch_size: 8 #single card bacth size + test_batch_size: 8 + num_workers: 8 + train: + format: "SFVideoDataset" + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/train.list" #Mandatory, train data index file path + valid: + format: "SFVideoDataset" + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + test: + format: "SFVideoDataset" + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, test data index file path + num_ensemble_views: 10 + num_spatial_crops: 3 + +PIPELINE: + train: + decode_sampler: + name: "DecodeSampler" + num_frames: 32 + sampling_rate: 2 + transform: #Mandotary, image transfrom operator + - JitterScale: + min_size: 256 + max_size: 320 + - MultiCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + transpose: False + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - PackOutput: + alpha: 8 + + valid: + decode_sampler: + name: "DecodeSampler" + num_frames: 32 + sampling_rate: 2 + transform: #Mandotary, image transfrom operator + - JitterScale: + min_size: 256 + max_size: 320 + - MultiCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + transpose: False + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - PackOutput: + alpha: 8 + + test: + decode_sampler: + name: "DecodeSampler" + num_frames: 32 + sampling_rate: 2 + test_mode: True + transform: #Mandotary, image transfrom operator + - JitterScale: + min_size: 256 + max_size: 256 + - MultiCrop: + target_size: 256 + test_mode: True + - Image2Array: + transpose: False + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - PackOutput: + alpha: 8 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' + max_epoch: 196 + warmup_epochs: 34 + warmup_start_lr: 0.01 + cosine_base_lr: 0.1 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + +METRIC: + name: 'MultiCropMetric' + num_ensemble_views: 10 + num_spatial_crops: 3 + num_classes: 400 + +PRECISEBN: + preciseBN_interval: 10 + num_iters_preciseBN: 200 #default + + +INFERENCE: + name: 'SlowFast_Inference_helper' + num_frames: 32 + alpha: 8 + target_size: 256 + +model_name: SlowFast +save_interval: 10 +val_interval: 10 +epochs: 196 #Mandatory, total epoch +log_level: "INFO" diff --git a/configs/recognition/slowfast/slowfast_multigrid.yaml b/configs/recognition/slowfast/slowfast_multigrid.yaml new file mode 100644 index 0000000000000000000000000000000000000000..490822a69df281622393fe0a7f5a8511017c6d91 --- /dev/null +++ b/configs/recognition/slowfast/slowfast_multigrid.yaml @@ -0,0 +1,152 @@ +MODEL: #MODEL field + framework: "Recognizer3D" + backbone: + name: "ResNetSlowFast" + depth: 50 # Not Optional, only 50 now. + alpha: 4 + beta: 8 + width_per_group: 64 + fusion_kernel_sz: 7 + bn_norm_type: "batchnorm" + head: + name: "SlowFastHead" + width_per_group: 64 + alpha: 4 + beta: 8 + num_classes: 400 + num_frames: 32 + crop_size: 224 #independent to test or train mode + dropout_rate: 0.5 + multigrid_short: True + +DATASET: #DATASET field + batch_size: 8 #single bacth size + num_workers: 4 + train: + format: "SFVideoDataset" + file_path: "./data/k400/train.list" #Mandatory, train data index file path + valid: + format: "SFVideoDataset" + file_path: "./data/k400/val.list" #Mandatory, valid data index file path + test: + format: "SFVideoDataset" + file_path: "../data/k400/val.list" #Mandatory, test data index file path + test_mode: True + num_ensemble_views: 10 + num_spatial_crops: 3 + +PIPELINE: + train: + decode_sampler: + name: "DecodeSampler" + num_frames: 32 + sampling_rate: 2 + transform: #Mandotary, image transfrom operator + - JitterScale: + min_size: 256 + max_size: 320 + - MultiCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + transpose: False + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - PackOutput: + alpha: 4 + + valid: + decode_sampler: + name: "DecodeSampler" + num_frames: 32 + sampling_rate: 2 + transform: #Mandotary, image transfrom operator + - JitterScale: + min_size: 256 + max_size: 320 + - MultiCrop: + target_size: 224 + - RandomFlip: + - Image2Array: + transpose: False + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - PackOutput: + alpha: 4 + + test: + decode_sampler: + name: "DecodeSampler" + num_frames: 32 + sampling_rate: 2 + test_mode: True + transform: #Mandotary, image transfrom operator + - JitterScale: + min_size: 224 + max_size: 224 + - MultiCrop: + target_size: 224 + test_mode: True + - Image2Array: + transpose: False + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - PackOutput: + alpha: 4 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupPiecewiseDecay' + warmup_epochs: 34 + warmup_start_lr: 0.01 + step_base_lr: 0.1 + lrs: [1, 0.1, 0.01, 0.001, 0.0001, 0.00001] + gamma: 0.1 + steps: [0, 94, 154, 196] + max_epoch: 239 + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + +METRIC: + name: 'MultiCropMetric' + num_ensemble_views: 10 + num_spatial_crops: 3 + num_classes: 400 + +MULTIGRID: + LONG_CYCLE: True + SHORT_CYCLE: True + default_batch_size: 0 + default_temporal_size: 0 + default_crop_size: 0 + epoch_factor: 1.5 #1.0 + bn_base_size: 8 + long_cycle_sampling_rate: 0 + long_cycle_factors: + - value: [0.25, 0.7071] # 0.5 ** 0.5 + - value: [0.5, 0.7071] + - value: [0.5, 1] + - value: [1, 1] + short_cycle_factors: [0.5, 0.7071] + EVAL_FREQ: 3 + +PRECISEBN: + preciseBN_interval: 20 + num_iters_preciseBN: 200 #default + +model_name: SlowFast +val_interval: 20 +log_interval: 20 +epochs: 239 #Mandatory, total epoch +log_level: "INFO" diff --git a/configs/recognition/stgcn/stgcn_fsd.yaml b/configs/recognition/stgcn/stgcn_fsd.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7d4cfea8a18beb9c5544cbf81e14acb7c96373fd --- /dev/null +++ b/configs/recognition/stgcn/stgcn_fsd.yaml @@ -0,0 +1,64 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "STGCN" #Mandatory, The name of backbone. + head: + name: "STGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 30 #Optional, the number of classes to be classified. + +DATASET: #DATASET field + batch_size: 64 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "train_data.npy" #Mandatory, train data index file path + label_path: "train_label.npy" + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "test_A_data.npy" #Mandatory, valid data index file path + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "AutoPadding" + window_size: 350 + transform: #Mandotary, image transfrom operator + - SkeletonNorm: + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "AutoPadding" + window_size: 350 + transform: #Mandotary, image transfrom operator + - SkeletonNorm: + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + name: 'CosineAnnealingDecay' + learning_rate: 0.05 + T_max: 90 + weight_decay: + name: 'L2' + value: 1e-4 + + +METRIC: + name: 'SkeletonMetric' + out_file: 'submission.csv' + + +INFERENCE: + name: 'STGCN_Inference_helper' + num_channels: 2 + window_size: 350 + vertex_nums: 25 + person_nums: 1 + + +model_name: "STGCN" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 90 #Mandatory, total epoch diff --git a/configs/recognition/stgcn/stgcn_ntucs.yaml b/configs/recognition/stgcn/stgcn_ntucs.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f44c6a83dfb7c57c3c79566a95e72d613756eed5 --- /dev/null +++ b/configs/recognition/stgcn/stgcn_ntucs.yaml @@ -0,0 +1,62 @@ +MODEL: #MODEL field + framework: "RecognizerGCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "STGCN" #Mandatory, The name of backbone. + in_channels: 3 + dropout: 0.5 + layout: 'ntu-rgb+d' + head: + name: "STGCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 60 #Optional, the number of classes to be classified. + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "NTU-RGB-D/xsub/train_data.npy" #Mandatory, train data index file path + label_path: "NTU-RGB-D/xsub/train_label.pkl" + valid: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "NTU-RGB-D/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "NTU-RGB-D/xsub/val_label.pkl" + test_mode: True + test: + format: "SkeletonDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "NTU-RGB-D/xsub/val_data.npy" #Mandatory, valid data index file path + label_path: "NTU-RGB-D/xsub/val_label.pkl" + test_mode: True + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + test: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + transform: #Mandotary, image transfrom operator + - Iden: + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' + momentum: 0.9 + learning_rate: + name: 'PiecewiseDecay' + boundaries: [30, 40] + values: [0.1, 0.01, 0.001] + weight_decay: + name: 'L2' + value: 1e-4 + use_nesterov: True + + +METRIC: + name: 'SkeletonMetric' + + +model_name: "STGCN" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch diff --git a/configs/recognition/timesformer/timesformer_k400_videos.yaml b/configs/recognition/timesformer/timesformer_k400_videos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..35fa6414ab02f063c8d7cf736fb3ed24ecba6e5c --- /dev/null +++ b/configs/recognition/timesformer/timesformer_k400_videos.yaml @@ -0,0 +1,149 @@ +MODEL: #MODEL field + framework: "RecognizerTransformer" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "VisionTransformer" #Mandatory, The name of backbone. + pretrained: "data/ViT_base_patch16_224_pretrained.pdparams" #Optional, pretrained model path. + img_size: 224 + patch_size: 16 + in_channels: 3 + embed_dim: 768 + depth: 12 + num_heads: 12 + mlp_ratio: 4 + qkv_bias: True + epsilon: 1e-6 + num_seg: 8 + attention_type: 'divided_space_time' + head: + name: "TimeSformerHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 768 #input channel of the extracted feature. + std: 0.02 #std value in params initialization + runtime_cfg: # configuration used when the model is train or test. + test: # test config + num_seg: 8 + avg_type: 'score' # 'score' or 'prob' + +DATASET: #DATASET field + batch_size: 1 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 8 + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/train.list" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + +PIPELINE: #PIPELINE field TODO..... + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'train' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + linspace_sample: True + transform: #Mandotary, image transform operator. + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'valid' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False # It is indeed False when verifying + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 256 + max_size: 320 + - RandomCrop: + target_size: 224 + - RandomFlip: + test: + decode: + name: "VideoDecoder" + backend: 'pyav' + mode: 'test' + num_seg: 8 + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + linspace_sample: True + transform: + - Normalization: + mean: [0.45, 0.45, 0.45] + std: [0.225, 0.225, 0.225] + tensor_shape: [1, 1, 1, 3] + - Image2Array: + data_format: 'cthw' + - JitterScale: + min_size: 224 + max_size: 224 + - UniformCrop: + target_size: 224 + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + learning_rate: 0.005 # Applicable when global batch size=64 + name: 'MultiStepDecay' + milestones: [11, 14] + gamma: 0.1 + weight_decay: + name: 'L2' + value: 0.0001 + use_nesterov: True + +GRADIENT_ACCUMULATION: + global_batch_size: 64 # Specify the sum of batches to be calculated by all GPUs + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'TimeSformer_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TimeSformer" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 3 +epochs: 15 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsm/tsm_k400_frames.yaml b/configs/recognition/tsm/tsm_k400_frames.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5a6d847b25d87639d3a8b9470d743584f87daac7 --- /dev/null +++ b/configs/recognition/tsm/tsm_k400_frames.yaml @@ -0,0 +1,123 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTSM" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_pretrain.pdparams" #Optional, pretrained model path. + num_seg: 8 + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.001 #std value in params initialization + + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, train data root path + file_path: "data/k400/train_frames.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, valid data root path + file_path: "data/k400/val_frames.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, valid data root path + file_path: "data/k400/val_frames.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transform operator. + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [20, 40] + values: [0.02, 0.002, 0.0002] #8 cards * 16 batch size + weight_decay: + name: 'L2' + value: 0.0001 + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 20.0 + + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TSM" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 50 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsm/tsm_k400_frames_nhwc.yaml b/configs/recognition/tsm/tsm_k400_frames_nhwc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7e31147c9da19b66a619665de1d573d135709ef1 --- /dev/null +++ b/configs/recognition/tsm/tsm_k400_frames_nhwc.yaml @@ -0,0 +1,128 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTSM" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_pretrain.pdparams" #Optional, pretrained model path. + num_seg: 8 + depth: 50 #Optional, the depth of backbone architecture. + data_format: "NHWC" + head: + name: "TSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.001 #std value in params initialization + data_format: "NHWC" + + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, train data root path + file_path: "data/k400_frames/train.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/k400_frames/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/k400_frames/val.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transform operator. + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + - RandomFlip: + - Image2Array: + transpose: False + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + tensor_shape: [1,1,3] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + transpose: False + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + tensor_shape: [1,1,3] + + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + transpose: False + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + tensor_shape: [1,1,3] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [20, 40] + values: [0.02, 0.002, 0.0002] #8 cards * 16 batch size + weight_decay: + name: 'L2' + value: 0.0001 + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 20.0 + + +METRIC: + name: 'CenterCropMetric' + + +model_name: "TSM" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 50 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsm/tsm_k400_videos.yaml b/configs/recognition/tsm/tsm_k400_videos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0e3253e56a62a1aacc27bcba3eaaca1c2835cb30 --- /dev/null +++ b/configs/recognition/tsm/tsm_k400_videos.yaml @@ -0,0 +1,123 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTSM" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_pretrain.pdparams" #Optional, pretrained model path. + num_seg: 8 + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.5 #the ratio of dropout + std: 0.001 #std value in params initialization + + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/train.list" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transform operator. + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [20, 40] + values: [0.02, 0.002, 0.0002] #8 cards * 16 batch size + weight_decay: + name: 'L2' + value: 0.0001 + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 20.0 + + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TSM" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 50 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsm/tsm_ucf101_frames.yaml b/configs/recognition/tsm/tsm_ucf101_frames.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5886e3cd2fb754a766e5547d09ff0adf23210f6e --- /dev/null +++ b/configs/recognition/tsm/tsm_ucf101_frames.yaml @@ -0,0 +1,123 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTSM" #Mandatory, The name of backbone. + pretrained: "data/TSM_k400.pdparams" #Optional, pretrained model path. + num_seg: 8 + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 101 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.8 #the ratio of dropout + std: 0.001 #std value in params initialization + + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, train data root path + file_path: "data/ucf101/ucf101_train_split_1_rawframes.txt" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/ucf101/ucf101_val_split_1_rawframes.txt" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/ucf101/ucf101_val_split_1_rawframes.txt" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transform operator. + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [10, 20] + values: [0.001, 0.0001, 0.00001] #4 cards * 16 batch size + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 20.0 + + +METRIC: + name: 'CenterCropMetric' + + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + + +model_name: "TSM" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 25 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsm/tsm_ucf101_frames_nhwc.yaml b/configs/recognition/tsm/tsm_ucf101_frames_nhwc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f6ae326513c85cca0a8e4eca13a90a81fb6d6774 --- /dev/null +++ b/configs/recognition/tsm/tsm_ucf101_frames_nhwc.yaml @@ -0,0 +1,125 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTSM" #Mandatory, The name of backbone. + pretrained: "data/TSM_k400.pdparams" #Optional, pretrained model path. + num_seg: 8 + depth: 50 #Optional, the depth of backbone architecture. + data_format: "NHWC" + head: + name: "TSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 101 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.8 #the ratio of dropout + std: 0.001 #std value in params initialization + data_format: "NHWC" + + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, train data root path + file_path: "data/ucf101/ucf101_train_split_1_rawframes.txt" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/ucf101/ucf101_val_split_1_rawframes.txt" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/ucf101/ucf101_val_split_1_rawframes.txt" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transform operator. + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + - RandomFlip: + - Image2Array: + transpose: False + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + tensor_shape: [1,1,3] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + transpose: False + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + tensor_shape: [1,1,3] + + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + transpose: False + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + tensor_shape: [1,1,3] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [10, 20] + values: [0.001, 0.0001, 0.00001] #4 cards * 16 batch size + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 20.0 + + +METRIC: + name: 'CenterCropMetric' + + +model_name: "TSM" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 25 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsm/tsm_ucf101_videos.yaml b/configs/recognition/tsm/tsm_ucf101_videos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8f1ecaf95a07f5b147b1186246f4943da248a5a5 --- /dev/null +++ b/configs/recognition/tsm/tsm_ucf101_videos.yaml @@ -0,0 +1,121 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNetTSM" #Mandatory, The name of backbone. + pretrained: "data/TSM_k400.pdparams" #Optional, pretrained model path. + num_seg: 8 + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSMHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 101 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.8 #the ratio of dropout + std: 0.001 #std value in params initialization + + +DATASET: #DATASET field + batch_size: 16 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, train data root path + file_path: "data/ucf101/ucf101_train_split_1_videos.txt" #Mandatory, train data index file path + suffix: '.avi' + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/ucf101/ucf101_val_split_1_videos.txt" #Mandatory, valid data index file path + suffix: '.avi' + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "" #Mandatory, valid data root path + file_path: "data/ucf101/ucf101_val_split_1_videos.txt" #Mandatory, valid data index file path + suffix: '.avi' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transform operator. + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "VideoDecoder" + sample: + name: "Sampler" + num_seg: 8 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [10, 20] + values: [0.001, 0.0001, 0.00001] #4 cards * 16 batch size + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 20.0 + + +METRIC: + name: 'CenterCropMetric' + +INFERENCE: + name: 'ppTSM_Inference_helper' + num_seg: 8 + target_size: 224 + +model_name: "TSM" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 25 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsn/tsn_dali.yaml b/configs/recognition/tsn/tsn_dali.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7f97eb582dbe13bbd903196dadc9a9769a97515b --- /dev/null +++ b/configs/recognition/tsn/tsn_dali.yaml @@ -0,0 +1,73 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNet" #Mandatory, The name of backbone. + pretrained: "/data/pretrained_model/ResNet50_pretrained.pdparams" #Optional, pretrained model path. + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.2 #the ratio of dropout + # ls_eps: 0.1 # label smoothing epsilon + std: 0.01 #std value in params initialization + + +DALI_LOADER: + batch_size: 32 + file_path: 'data/k400/train.csv' + num_seg: 3 + seglen: 1 + short_size: 256 + target_size: 224 + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +DATASET: #DATASET field, only used when test in tsn_dali + test_batch_size: 4 + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "data/k400/val.csv" #Mandatory, valid data index file path + + +PIPELINE: #PIPELINE field + test: + decode: + name: "VideoDecoder" + sample: + name: "Sampler" + valid_mode: True + num_seg: 25 + seg_len: 1 + transform: + - Scale: + short_size: 256 + - CenterCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [30, 60] + values: [0.01, 0.001, 0.0001] #4 cards * 32 batch size + weight_decay: + name: 'L2' + value: 1e-4 + + +METRIC: + name: 'CenterCropMetric' + +model_name: "TSN" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 80 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsn/tsn_k400_frames.yaml b/configs/recognition/tsn/tsn_k400_frames.yaml new file mode 100644 index 0000000000000000000000000000000000000000..495941e057f531c1b335384d049d4358b9a013a7 --- /dev/null +++ b/configs/recognition/tsn/tsn_k400_frames.yaml @@ -0,0 +1,138 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNet" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_pretrain.pdparams" #Optional, pretrained model path. + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.4 #the ratio of dropout + std: 0.01 #std value in params initialization + + +DATASET: #DATASET field + batch_size: 32 #Mandatory, bacth size + valid_batch_size: 32 + test_batch_size: 1 + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, train data root path + file_path: "data/k400/train_frames.list" #Mandatory, train data index file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, valid data root path + file_path: "data/k400/val_frames.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/rawframes" #Mandatory, valid data root path + file_path: "data/k400/val_frames.list" #Mandatory, valid data index file path + suffix: 'img_{:05}.jpg' + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 3 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + more_fix_crop: False + backend: 'cv2' + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 3 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - CenterCrop: + target_size: 224 + do_round: False + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "FrameDecoder" + sample: + name: "Sampler" + num_seg: 25 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - TenCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [40, 80] + values: [0.01, 0.001, 0.0001] #4 cards * 32 batch size + weight_decay: + name: 'L2' + value: 0.0001 + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 40.0 + +METRIC: + name: 'CenterCropMetric' + + +INFERENCE: + name: 'ppTSN_Inference_helper' + num_seg: 25 + target_size: 224 + + +model_name: "TSN" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 100 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/recognition/tsn/tsn_k400_videos.yaml b/configs/recognition/tsn/tsn_k400_videos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..44585f5133231d329189212279d3e93ba3a05bdb --- /dev/null +++ b/configs/recognition/tsn/tsn_k400_videos.yaml @@ -0,0 +1,135 @@ +MODEL: #MODEL field + framework: "Recognizer2D" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ResNet" #Mandatory, The name of backbone. + pretrained: "data/ResNet50_pretrain.pdparams" #Optional, pretrained model path. + depth: 50 #Optional, the depth of backbone architecture. + head: + name: "TSNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 2048 #input channel of the extracted feature. + drop_ratio: 0.4 #the ratio of dropout + std: 0.01 #std value in params initialization + + +DATASET: #DATASET field + batch_size: 32 #Mandatory, bacth size + valid_batch_size: 32 + test_batch_size: 1 + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "data/k400/train.list" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "data/k400/val.list" #Mandatory, valid data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "data/k400/val.list" #Mandatory, valid data index file path + + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 3 + seg_len: 1 + valid_mode: False + select_left: True + transform: #Mandotary, image transfrom operator + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - MultiScaleCrop: + target_size: 224 + allow_duplication: True + more_fix_crop: False + backend: 'cv2' + - RandomFlip: + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 3 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - CenterCrop: + target_size: 224 + do_round: False + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + test: + decode: + name: "VideoDecoder" + backend: "decord" + sample: + name: "Sampler" + num_seg: 25 + seg_len: 1 + valid_mode: True + select_left: True + transform: + - Scale: + short_size: 256 + fixed_ratio: False + do_round: True + backend: 'cv2' + - TenCrop: + target_size: 224 + - Image2Array: + - Normalization: + mean: [0.485, 0.456, 0.406] + std: [0.229, 0.224, 0.225] + + +OPTIMIZER: #OPTIMIZER field + name: 'Momentum' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + momentum: 0.9 + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'PiecewiseDecay' + boundaries: [40, 80] + values: [0.01, 0.001, 0.0001] #4 cards * 32 batch size + weight_decay: + name: 'L2' + value: 0.0001 + grad_clip: + name: 'ClipGradByGlobalNorm' + value: 40.0 + +METRIC: + name: 'CenterCropMetric' + + +INFERENCE: + name: 'ppTSN_Inference_helper' + num_seg: 25 + target_size: 224 + + +model_name: "TSN" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 10 +epochs: 100 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" \ No newline at end of file diff --git a/configs/recognition/videoswin/videoswin_k400_videos.yaml b/configs/recognition/videoswin/videoswin_k400_videos.yaml new file mode 100644 index 0000000000000000000000000000000000000000..67aafeeb0cc7a87ab4ef71f11d6a2d0f1c8cac6a --- /dev/null +++ b/configs/recognition/videoswin/videoswin_k400_videos.yaml @@ -0,0 +1,175 @@ +MODEL: #MODEL field + framework: "RecognizerTransformer" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "SwinTransformer3D" #Mandatory, The name of backbone. + pretrained: "data/SwinTransformer_imagenet.pdparams" #Optional, pretrained model path. + patch_size: [2, 4, 4] + embed_dim: 128 + depths: [2, 2, 18, 2] + num_heads: [4, 8, 16, 32] + window_size: [8, 7, 7] + mlp_ratio: 4. + qkv_bias: True + qk_scale: None + drop_rate: 0.0 + attn_drop_rate: 0.0 + drop_path_rate: 0.2 + patch_norm: True + head: + name: "I3DHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 400 #Optional, the number of classes to be classified. + in_channels: 1024 #input channel of the extracted feature. + spatial_type: 'avg' + drop_ratio: 0.5 #the ratio of dropout + std: 0.01 #std value in params initialization + runtime_cfg: # configuration used when the model is train or test. + test: # test config + num_seg: 32 + avg_type: 'prob' # 'score' or 'prob' + +DATASET: #DATASET field + batch_size: 1 #Mandatory, bacth size + num_workers: 4 #Mandatory, XXX the number of subprocess on each GPU. + test_batch_size: 1 + train: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/train.list" #Mandatory, train data index file path + valid: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + test: + format: "VideoDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + data_prefix: "data/k400/videos" #Mandatory, train data root path + file_path: "data/k400/val.list" #Mandatory, valid data index file path + +PIPELINE: #PIPELINE field TODO..... + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'decord' + mode: 'train' + sample: + name: "Sampler" + num_seg: 1 + frame_interval: 2 + seg_len: 32 + valid_mode: False + use_pil: False + transform: #Mandotary, image transform operator. + - Scale: + short_size: 256 + fixed_ratio: False + keep_ratio: True + backend: 'cv2' + do_round: True + - RandomResizedCrop: + backend: 'cv2' + - Scale: + short_size: 224 + fixed_ratio: False + keep_ratio: False + backend: 'cv2' + do_round: True + - RandomFlip: + - Normalization: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + tensor_shape: [3, 1, 1, 1] + inplace: True + - Image2Array: + data_format: 'cthw' + valid: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + decode: + name: "VideoDecoder" + backend: 'decord' + mode: 'valid' + sample: + name: "Sampler" + num_seg: 1 + frame_interval: 2 + seg_len: 32 + valid_mode: True + use_pil: False + transform: #Mandotary, image transform operator. + - Scale: + short_size: 256 + fixed_ratio: False + keep_ratio: True + backend: 'cv2' + do_round: True + - CenterCrop: + target_size: 224 + do_round: False + backend: 'cv2' + - Normalization: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + tensor_shape: [3, 1, 1, 1] + inplace: True + - Image2Array: + data_format: 'cthw' + test: + decode: + name: "VideoDecoder" + backend: 'decord' + mode: 'valid' + sample: + name: "Sampler" + num_seg: 4 + frame_interval: 2 + seg_len: 32 + valid_mode: True + use_pil: False + transform: #Mandotary, image transform operator. + - Scale: + short_size: 224 + fixed_ratio: False + keep_ratio: True + backend: 'cv2' + do_round: True + - UniformCrop: + target_size: 224 + backend: 'cv2' + - Normalization: + mean: [123.675, 116.28, 103.53] + std: [58.395, 57.12, 57.375] + tensor_shape: [3, 1, 1, 1] + inplace: True + - Image2Array: + data_format: 'cthw' + +OPTIMIZER: #OPTIMIZER field + name: 'AdamW' #Mandatory, the type of optimizer, associate to the 'paddlevideo/solver/' + beta1: 0.9 + beta2: 0.999 + no_weight_decay_name: 'norm relative_position_bias_table' + learning_rate: #Mandatory, the type of learning rate scheduler, associate to the 'paddlevideo/solver/' + name: 'CustomWarmupCosineStepDecay' + iter_step: True + warmup_iters: 2.5 + warmup_ratio: 0.1 + min_lr: 0 + base_lr: 3e-5 + max_epoch: 30 + weight_decay: 0.05 + +METRIC: + name: 'CenterCropMetric' + +GRADIENT_ACCUMULATION: + global_batch_size: 64 # Specify the sum of batches to be calculated by all GPUs + +INFERENCE: + name: 'VideoSwin_Inference_helper' + num_seg: 1 + seg_len: 32 + short_size: 256 + target_size: 224 + +model_name: "VideoSwin" +log_interval: 20 #Optional, the interal of logger, default:10 +save_interval: 5 +epochs: 30 #Mandatory, total epoch +log_level: "INFO" #Optional, the logger level. default: "INFO" diff --git a/configs/segmentation/asrf/asrf_50salads.yaml b/configs/segmentation/asrf/asrf_50salads.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f185ec3d406512b5f01f592f96a0b2c30cf23fc1 --- /dev/null +++ b/configs/segmentation/asrf/asrf_50salads.yaml @@ -0,0 +1,107 @@ +MODEL: #MODEL field + framework: "ASRF" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ASRF" #Mandatory, The name of backbone. + in_channel: 2048 + num_features: 64 + num_classes: 19 + num_stages: 4 + num_layers: 10 + head: + name: "ASRFHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 19 + num_features: 64 + num_stages: 4 + num_layers: 10 + num_stages_asb: 4 + num_stages_brb: 4 + loss: + name: "ASRFLoss" + lambda_bound_loss: 0.1 + num_classes: 19 + ce: True + asl_focal: False + tmse: False + gstmse: True + asl_weight: True + ce_weight: 1.0 + focal_weight: 1.0 + tmse_weight: 0.15 + gstmse_weight: 1.0 + file_path: "./data/50salads/splits/train.split5.bundle" #Mandatory, train data index file path + label_path: "./data/50salads/gt_arr" + boundary_path: "./data/50salads/gt_boundary_arr" + postprocessing_method: "refinement_with_boundary" + boundary_threshold: 0.5 + + +DATASET: #DATASET field + batch_size: 1 #! Mandatory, bacth size, segmentation only support batch_size 1 + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/50salads/splits/train.split5.bundle" #Mandatory, train data index file path + feature_path: "./data/50salads/features" + label_path: "./data/50salads/gt_arr" + boundary_path: "./data/50salads/gt_boundary_arr" + valid: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/50salads/splits/test.split5.bundle" #Mandatory, valid data index file path + feature_path: "./data/50salads/features" + label_path: "./data/50salads/gt_arr" + boundary_path: "./data/50salads/gt_boundary_arr" + test: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/50salads/splits/test.split5.bundle" #Mandatory, test data index file path + feature_path: "./data/50salads/features" + label_path: "./data/50salads/gt_arr" + boundary_path: "./data/50salads/gt_boundary_arr" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 2 + #transform: #Mandotary, image transfrom operator + + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 2 + #transform: #Mandotary, image transfrom operator + + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 2 + #transform: #Mandotary, image transfrom operator + + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + name: 'PiecewiseDecay' + boundaries: [50] + values: [0.0005, 1] + +METRIC: + name: 'SegmentationMetric' + overlap: [.1, .25, .5] + actions_map_file_path: "./data/50salads/mapping.txt" + + +INFERENCE: + name: 'ASRF_Inference_helper' + num_channels: 2048 + actions_map_file_path: "./data/50salads/mapping.txt" + postprocessing_method: "refinement_with_boundary" + boundary_threshold: 0.5 + feature_path: "./data/50salads/features" + + +model_name: "ASRF" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch +save_interval: 40 diff --git a/configs/segmentation/asrf/asrf_breakfast.yaml b/configs/segmentation/asrf/asrf_breakfast.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0e88e762318e09bf7761dd9de7b6de77124a1ea5 --- /dev/null +++ b/configs/segmentation/asrf/asrf_breakfast.yaml @@ -0,0 +1,108 @@ +MODEL: #MODEL field + framework: "ASRF" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ASRF" #Mandatory, The name of backbone. + in_channel: 2048 + num_features: 64 + num_classes: 48 + num_stages: 4 + num_layers: 10 + head: + name: "ASRFHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 48 + num_features: 64 + num_stages: 4 + num_layers: 10 + num_stages_asb: 4 + num_stages_brb: 4 + loss: + name: "ASRFLoss" + lambda_bound_loss: 0.1 + num_classes: 48 + ce: True + asl_focal: False + tmse: False + gstmse: True + asl_weight: True + ce_weight: 1.0 + focal_weight: 1.0 + tmse_weight: 0.15 + gstmse_weight: 1.0 + file_path: "./data/breakfast/splits/train.split4.bundle" #Mandatory, train data index file path + label_path: "./data/breakfast/gt_arr" + boundary_path: "./data/breakfast/gt_boundary_arr" + postprocessing_method: "refinement_with_boundary" + boundary_threshold: 0.5 + + +DATASET: #DATASET field + batch_size: 1 #! Mandatory, bacth size, segmentation only support batch_size 1 + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/breakfast/splits/train.split4.bundle" #Mandatory, train data index file path + feature_path: "./data/breakfast/features" + label_path: "./data/breakfast/gt_arr" + boundary_path: "./data/breakfast/gt_boundary_arr" + valid: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/breakfast/splits/test.split4.bundle" #Mandatory, valid data index file path + feature_path: "./data/breakfast/features" + label_path: "./data/breakfast/gt_arr" + boundary_path: "./data/breakfast/gt_boundary_arr" + test: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/breakfast/splits/test.split4.bundle" #Mandatory, test data index file path + feature_path: "./data/breakfast/features" + label_path: "./data/breakfast/gt_arr" + boundary_path: "./data/breakfast/gt_boundary_arr" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + name: 'PiecewiseDecay' + boundaries: [50] + values: [0.0005, 1] + + +METRIC: + name: 'SegmentationMetric' + overlap: [.1, .25, .5] + actions_map_file_path: "./data/breakfast/mapping.txt" + + +INFERENCE: + name: 'ASRF_Inference_helper' + num_channels: 2048 + actions_map_file_path: "./data/breakfast/mapping.txt" + postprocessing_method: "refinement_with_boundary" + boundary_threshold: 0.5 + feature_path: "./data/breakfast/features" + + +model_name: "ASRF" +log_interval: 1440 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch +save_interval: 1440 diff --git a/configs/segmentation/asrf/asrf_gtea.yaml b/configs/segmentation/asrf/asrf_gtea.yaml new file mode 100644 index 0000000000000000000000000000000000000000..aefcd04bb9f5b59af40da15d02af28303b395f5a --- /dev/null +++ b/configs/segmentation/asrf/asrf_gtea.yaml @@ -0,0 +1,108 @@ +MODEL: #MODEL field + framework: "ASRF" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "ASRF" #Mandatory, The name of backbone. + in_channel: 2048 + num_features: 64 + num_classes: 11 + num_stages: 4 + num_layers: 10 + head: + name: "ASRFHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 11 + num_features: 64 + num_stages: 4 + num_layers: 10 + num_stages_asb: 4 + num_stages_brb: 4 + loss: + name: "ASRFLoss" + lambda_bound_loss: 0.1 + num_classes: 11 + ce: True + asl_focal: False + tmse: False + gstmse: True + asl_weight: True + ce_weight: 1.0 + focal_weight: 1.0 + tmse_weight: 0.15 + gstmse_weight: 1.0 + file_path: "./data/gtea/splits/train.split4.bundle" #Mandatory, train data index file path + label_path: "./data/gtea/gt_arr" + boundary_path: "./data/gtea/gt_boundary_arr" + postprocessing_method: "refinement_with_boundary" + boundary_threshold: 0.5 + + +DATASET: #DATASET field + batch_size: 1 #! Mandatory, bacth size, segmentation only support batch_size 1 + num_workers: 4 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/gtea/splits/train.split4.bundle" #Mandatory, train data index file path + feature_path: "./data/gtea/features" + label_path: "./data/gtea/gt_arr" + boundary_path: "./data/gtea/gt_boundary_arr" + valid: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/gtea/splits/test.split4.bundle" #Mandatory, valid data index file path + feature_path: "./data/gtea/features" + label_path: "./data/gtea/gt_arr" + boundary_path: "./data/gtea/gt_boundary_arr" + test: + format: "ASRFDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/gtea/splits/test.split4.bundle" #Mandatory, test data index file path + feature_path: "./data/gtea/features" + label_path: "./data/gtea/gt_arr" + boundary_path: "./data/gtea/gt_boundary_arr" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + name: 'PiecewiseDecay' + boundaries: [50] + values: [0.0005, 1] + + +METRIC: + name: 'SegmentationMetric' + overlap: [.1, .25, .5] + actions_map_file_path: "./data/gtea/mapping.txt" + + +INFERENCE: + name: 'ASRF_Inference_helper' + num_channels: 2048 + actions_map_file_path: "./data/gtea/mapping.txt" + postprocessing_method: "refinement_with_boundary" + boundary_threshold: 0.5 + feature_path: "./data/gtea/features" + + +model_name: "ASRF" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch +save_interval: 40 diff --git a/configs/segmentation/cfbip_davis.yaml b/configs/segmentation/cfbip_davis.yaml new file mode 100644 index 0000000000000000000000000000000000000000..145c2c16b6effc784f1707320510b142a73b95a7 --- /dev/null +++ b/configs/segmentation/cfbip_davis.yaml @@ -0,0 +1,29 @@ +MODEL: #MODEL field + framework: "CFBI" + backbone: + name: "CFBI" + head: + name: "CollaborativeEnsemblerMS" + +DATASET: #DATASET field + test_batch_size: 1 + num_workers: 0 + test: + format: "DavisDataset" + file_path: "datasets/DAVIS" + result_root: "output/CFBI" + +PIPELINE: #PIPELINE field + test: + transform: + - MultiRestrictSize: + max_size: 1040.0 + multi_scale: [1.0] + - MultiNorm: + +METRIC: + name: "VOSMetric" + result_root: "output/CFBI" + zip_dir: "output/CFBI.zip" + +model_name: "CFBI" diff --git a/configs/segmentation/ms_tcn/ms_tcn_50salads.yaml b/configs/segmentation/ms_tcn/ms_tcn_50salads.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b7b1d96040ec3da62315d883f885103114603498 --- /dev/null +++ b/configs/segmentation/ms_tcn/ms_tcn_50salads.yaml @@ -0,0 +1,85 @@ +MODEL: #MODEL field + framework: "MSTCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "MSTCN" #Mandatory, The name of backbone. + num_stages: 4 #Optional, the number of stages. + num_layers: 10 # Optional, the number of layers in each stage. + num_f_maps: 64 #Optional, the number of channels in each layers. + dim: 2048 #Optional, the number of channels in input feature. + num_classes: 19 #Optional, the number of classes to be classified. + head: + name: "MSTCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 19 #Optional, the number of classes to be classified. + in_channels: 64 #Optional, the number of channels in input feature. + + +DATASET: #DATASET field + batch_size: 1 #! Mandatory, bacth size, segmentation only support batch_size 1 + num_workers: 1 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/50salads/splits/train.split5.bundle" #Mandatory, train data index file path + feature_path: "./data/50salads/features" + gt_path: "./data/50salads/groundTruth" + actions_map_file_path: "./data/50salads/mapping.txt" + valid: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/50salads/splits/test.split5.bundle" #Mandatory, valid data index file path + feature_path: "./data/50salads/features" + gt_path: "./data/50salads/groundTruth" + actions_map_file_path: "./data/50salads/mapping.txt" + test: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/50salads/splits/test.split5.bundle" #Mandatory, test data index file path + feature_path: "./data/50salads/features" + gt_path: "./data/50salads/groundTruth" + actions_map_file_path: "./data/50salads/mapping.txt" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 2 + #transform: #Mandotary, image transfrom operator + + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 2 + #transform: #Mandotary, image transfrom operator + + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 2 + #transform: #Mandotary, image transfrom operator + + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + name: 'PiecewiseDecay' + boundaries: [50] + values: [0.0005, 1] + +METRIC: + name: 'SegmentationMetric' + overlap: [.1, .25, .5] + actions_map_file_path: "./data/50salads/mapping.txt" + tolerance: 5 + boundary_threshold: 0.7 + + +INFERENCE: + name: 'MSTCN_Inference_helper' + num_channels: 2048 + actions_map_file_path: "./data/50salads/mapping.txt" + feature_path: "./data/50salads/features" + + +model_name: "MSTCN" +log_interval: 40 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch +save_interval: 40 diff --git a/configs/segmentation/ms_tcn/ms_tcn_breakfast.yaml b/configs/segmentation/ms_tcn/ms_tcn_breakfast.yaml new file mode 100644 index 0000000000000000000000000000000000000000..04feccf54b1cd5343e2a50a36a6b5a533116ea78 --- /dev/null +++ b/configs/segmentation/ms_tcn/ms_tcn_breakfast.yaml @@ -0,0 +1,86 @@ +MODEL: #MODEL field + framework: "MSTCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "MSTCN" #Mandatory, The name of backbone. + num_stages: 4 #Optional, the number of stages. + num_layers: 10 # Optional, the number of layers in each stage. + num_f_maps: 64 #Optional, the number of channels in each layers. + dim: 2048 #Optional, the number of channels in input feature. + num_classes: 48 #Optional, the number of classes to be classified. + head: + name: "MSTCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 48 #Optional, the number of classes to be classified. + in_channels: 64 #Optional, the number of channels in input feature. + + +DATASET: #DATASET field + batch_size: 1 #! Mandatory, bacth size, segmentation only support batch_size 1 + num_workers: 1 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/breakfast/splits/train.split1.bundle" #Mandatory, train data index file path + feature_path: "./data/breakfast/features" + gt_path: "./data/breakfast/groundTruth" + actions_map_file_path: "./data/breakfast/mapping.txt" + valid: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/breakfast/splits/test.split1.bundle" #Mandatory, valid data index file path + feature_path: "./data/breakfast/features" + gt_path: "./data/breakfast/groundTruth" + actions_map_file_path: "./data/breakfast/mapping.txt" + test: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/breakfast/splits/test.split1.bundle" #Mandatory, test data index file path + feature_path: "./data/breakfast/features" + gt_path: "./data/breakfast/groundTruth" + actions_map_file_path: "./data/breakfast/mapping.txt" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + name: 'PiecewiseDecay' + boundaries: [50] + values: [0.0005, 1] + + +METRIC: + name: 'SegmentationMetric' + overlap: [.1, .25, .5] + actions_map_file_path: "./data/breakfast/mapping.txt" + tolerance: 5 + boundary_threshold: 0.7 + + +INFERENCE: + name: 'MSTCN_Inference_helper' + num_channels: 2048 + actions_map_file_path: "./data/breakfast/mapping.txt" + feature_path: "./data/breakfast/features" + + +model_name: "MSTCN" +log_interval: 1440 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch +save_interval: 1440 diff --git a/configs/segmentation/ms_tcn/ms_tcn_gtea.yaml b/configs/segmentation/ms_tcn/ms_tcn_gtea.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4b143ab24095fd3213aa47ebc9450ba65ee68cac --- /dev/null +++ b/configs/segmentation/ms_tcn/ms_tcn_gtea.yaml @@ -0,0 +1,86 @@ +MODEL: #MODEL field + framework: "MSTCN" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: "MSTCN" #Mandatory, The name of backbone. + num_stages: 4 #Optional, the number of stages. + num_layers: 10 # Optional, the number of layers in each stage. + num_f_maps: 64 #Optional, the number of channels in each layers. + dim: 2048 #Optional, the number of channels in input feature. + num_classes: 11 #Optional, the number of classes to be classified. + head: + name: "MSTCNHead" #Mandatory, indicate the type of head, associate to the 'paddlevideo/modeling/heads' + num_classes: 11 #Optional, the number of classes to be classified. + in_channels: 64 #Optional, the number of channels in input feature. + + +DATASET: #DATASET field + batch_size: 1 #! Mandatory, bacth size, segmentation only support batch_size 1 + num_workers: 1 #Mandatory, the number of subprocess on each GPU. + test_batch_size: 1 + test_num_workers: 0 + train: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/gtea/splits/train.split1.bundle" #Mandatory, train data index file path + feature_path: "./data/gtea/features" + gt_path: "./data/gtea/groundTruth" + actions_map_file_path: "./data/gtea/mapping.txt" + valid: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/gtea/splits/test.split1.bundle" #Mandatory, valid data index file path + feature_path: "./data/gtea/features" + gt_path: "./data/gtea/groundTruth" + actions_map_file_path: "./data/gtea/mapping.txt" + test: + format: "MSTCNDataset" #Mandatory, indicate the type of dataset, associate to the 'paddlevidel/loader/dateset' + file_path: "./data/gtea/splits/test.split1.bundle" #Mandatory, test data index file path + feature_path: "./data/gtea/features" + gt_path: "./data/gtea/groundTruth" + actions_map_file_path: "./data/gtea/mapping.txt" + +PIPELINE: #PIPELINE field + train: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + valid: #Mandotary, indicate the pipeline to deal with the training data, associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + test: #Mandatory, indicate the pipeline to deal with the validing data. associate to the 'paddlevideo/loader/pipelines/' + sample: + name: "SegmentationSampler" + sample_rate: 1 + #transform: #Mandotary, image transfrom operator + + +OPTIMIZER: #OPTIMIZER field + name: 'Adam' + learning_rate: + name: 'PiecewiseDecay' + boundaries: [50] + values: [0.0005, 1] + + +METRIC: + name: 'SegmentationMetric' + overlap: [.1, .25, .5] + actions_map_file_path: "./data/gtea/mapping.txt" + tolerance: 5 + boundary_threshold: 0.7 + + +INFERENCE: + name: 'MSTCN_Inference_helper' + num_channels: 2048 + actions_map_file_path: "./data/gtea/mapping.txt" + feature_path: "./data/gtea/features" + + +model_name: "MSTCN" +log_interval: 10 #Optional, the interal of logger, default:10 +epochs: 50 #Mandatory, total epoch +save_interval: 10 diff --git a/data/50salads/prepare_asrf_data.py b/data/50salads/prepare_asrf_data.py new file mode 100644 index 0000000000000000000000000000000000000000..249faff8ab31ad627459844b068bb6fdbc1b6e03 --- /dev/null +++ b/data/50salads/prepare_asrf_data.py @@ -0,0 +1,113 @@ +import argparse +import glob +import os +import sys +from typing import Dict +import numpy as np + +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + +dataset_names = ["50salads", "breakfast", "gtea"] + + +def get_class2id_map(dataset: str, + dataset_dir: str = "./dataset") -> Dict[str, int]: + """ + Args: + dataset: 50salads, gtea, breakfast + dataset_dir: the path to the datset directory + """ + + assert (dataset in dataset_names + ), "You have to choose 50salads, gtea or breakfast as dataset." + + with open(os.path.join(dataset_dir, "{}/mapping.txt".format(dataset)), + "r") as f: + actions = f.read().split("\n")[:-1] + + class2id_map = dict() + for a in actions: + class2id_map[a.split()[1]] = int(a.split()[0]) + + return class2id_map + + +def get_arguments() -> argparse.Namespace: + """ + parse all the arguments from command line inteface + return a list of parsed arguments + """ + + parser = argparse.ArgumentParser( + description="convert ground truth txt files to numpy array") + parser.add_argument( + "--dataset_dir", + type=str, + default="./dataset", + help="path to a dataset directory (default: ./dataset)", + ) + + return parser.parse_args() + + +def main() -> None: + args = get_arguments() + + datasets = ["50salads", "gtea", "breakfast", "baseball"] + + for dataset in datasets: + # make directory for saving ground truth numpy arrays + cls_save_dir = os.path.join(args.dataset_dir, dataset, "gt_arr") + if not os.path.exists(cls_save_dir): + os.mkdir(cls_save_dir) + + # make directory for saving ground truth numpy arrays + boundary_save_dir = os.path.join(args.dataset_dir, dataset, + "gt_boundary_arr") + if not os.path.exists(boundary_save_dir): + os.mkdir(boundary_save_dir) + + # class to index mapping + class2id_map = get_class2id_map(dataset, dataset_dir=args.dataset_dir) + + gt_dir = os.path.join(args.dataset_dir, dataset, "groundTruth") + gt_paths = glob.glob(os.path.join(gt_dir, "*.txt")) + + for gt_path in gt_paths: + # the name of ground truth text file + gt_name = os.path.relpath(gt_path, gt_dir) + + with open(gt_path, "r") as f: + gt = f.read().split("\n")[:-1] + + gt_array = np.zeros(len(gt)) + for i in range(len(gt)): + gt_array[i] = class2id_map[gt[i]] + + # save array + np.save(os.path.join(cls_save_dir, gt_name[:-4] + ".npy"), gt_array) + + # the name of ground truth text file + gt_name = os.path.relpath(gt_path, gt_dir) + + with open(gt_path, "r") as f: + gt = f.read().split("\n")[:-1] + + # define the frame where new action starts as boundary frame + boundary = np.zeros(len(gt)) + last = gt[0] + boundary[0] = 1 + for i in range(1, len(gt)): + if last != gt[i]: + boundary[i] = 1 + last = gt[i] + + # save array + np.save(os.path.join(boundary_save_dir, gt_name[:-4] + ".npy"), + boundary) + + print("Done") + + +if __name__ == "__main__": + main() diff --git a/data/50salads/transform_segmentation_label.py b/data/50salads/transform_segmentation_label.py new file mode 100644 index 0000000000000000000000000000000000000000..2b7c3c72683b3ffb393d2dcf7cd2ef14b7083d5f --- /dev/null +++ b/data/50salads/transform_segmentation_label.py @@ -0,0 +1,195 @@ +import json +import numpy as np +import argparse +import os + +from tqdm import tqdm + + +def generate_mapping_list_txt(action_dict, out_path): + out_txt_file_path = os.path.join(out_path, "mapping.txt") + f = open(out_txt_file_path, "w", encoding='utf-8') + for key, action_name in action_dict.items(): + str_str = str(key) + " " + action_name + "\n" + f.write(str_str) + # add None + str_str = str(len(action_dict)) + " None" + "\n" + f.write(str_str) + f.close() + + +def segmentation_convert_localization_label(prefix_data_path, out_path, + action_dict, fps): + label_path = os.path.join(prefix_data_path) + label_txt_name_list = os.listdir(label_path) + + labels_dict = {} + labels_dict["fps"] = fps + labels_list = [] + for label_name in tqdm(label_txt_name_list, desc='label convert:'): + label_dict = {} + label_dict["url"] = label_name.split(".")[0] + ".mp4" + label_txt_path = os.path.join(prefix_data_path, label_name) + + with open(label_txt_path, "r", encoding='utf-8') as f: + gt = f.read().split("\n")[:-1] + label_dict["total_frames"] = len(gt) + + boundary_index_list = [0] + before_action_name = gt[0] + for index in range(1, len(gt)): + if before_action_name != gt[index]: + boundary_index_list.append(index) + before_action_name = gt[index] + actions_list = [] + for index in range(len(boundary_index_list) - 1): + if gt[boundary_index_list[index]] != "None": + action_name = gt[boundary_index_list[index]] + start_sec = float(boundary_index_list[index]) / float(fps) + end_sec = float(boundary_index_list[index + 1] - 1) / float(fps) + action_id = action_dict[action_name] + label_action_dict = {} + label_action_dict["label_names"] = action_name + label_action_dict["start_id"] = start_sec + label_action_dict["end_id"] = end_sec + label_action_dict["label_ids"] = [action_id] + actions_list.append(label_action_dict) + + label_dict["actions"] = actions_list + labels_list.append(label_dict) + labels_dict["gts"] = labels_list + output_path = os.path.join(out_path, "output.json") + f = open(output_path, "w", encoding='utf-8') + f.write(json.dumps(labels_dict, indent=4)) + f.close() + + +def generate_action_dict(label): + action_dict = {} + for gt in label["gts"]: + for action in gt["actions"]: + label_id = action["label_ids"][0] + label_name = action["label_names"][0] + action_dict[label_id] = label_name + + return action_dict + + +def load_action_dict(data_path): + mapping_txt_path = os.path.join(data_path, "mapping.txt") + with open(mapping_txt_path, "r", encoding='utf-8') as f: + actions = f.read().split("\n")[:-1] + + class2id_map = dict() + for a in actions: + class2id_map[a.split()[1]] = int(a.split()[0]) + + return class2id_map + + +def localization_convert_segmentation_label(label, prefix_data_path, out_path): + path = os.path.join(out_path, "groundTruth") + isExists = os.path.exists(path) + if not isExists: + os.makedirs(path) + print(path + ' 创建成功') + else: + print(path + ' 目录已存在') + + fps = float(label["fps"]) + video_list = [] + for gt in tqdm(label["gts"], desc='label convert:'): + video_name = gt["url"].split(".")[0] + data_path = os.path.join(prefix_data_path, video_name + ".pkl") + video_list.append(video_name + ".txt") + feature = np.load(data_path, allow_pickle=True)["image_feature"] + + num_feture = feature.shape[0] + seg_label = ["None"] * (num_feture) + for action in gt["actions"]: + start_id = action["start_id"] + end_id = action["end_id"] + + label_name = action["label_names"] + + start_index = int(np.floor(start_id * fps)) + end_index = int(np.floor(end_id * fps)) + 1 + + if end_index < num_feture - 1: + seg_label[start_index:end_index] = label_name * (end_index - + start_index) + elif start_index < num_feture - 1: + seg_label[start_index:] = label_name * (num_feture - + start_index) + else: + pass + + if len(seg_label) != num_feture: + seg_label = seg_label[:num_feture] + out_txt_file_path = os.path.join(out_path, "groundTruth", + video_name + ".txt") + str = '\n' + f = open(out_txt_file_path, "w", encoding='utf-8') + f.write(str.join(seg_label) + str) + f.close() + out_txt_file_path = os.path.join(out_path, "train_list.txt") + str = '\n' + f = open(out_txt_file_path, "w", encoding='utf-8') + f.write(str.join(video_list) + str) + f.close() + + +def main(): + args = get_arguments() + + if args.mode in ["segmentation", "localization"]: + if args.mode == "segmentation": + with open(args.label_path, 'r', encoding='utf-8') as json_file: + label = json.load(json_file) + action_dict = generate_action_dict(label) + generate_mapping_list_txt(action_dict, args.out_path) + localization_convert_segmentation_label(label, args.data_path, + args.out_path) + + elif args.mode == "localization": + action_dict = load_action_dict(args.label_path) + segmentation_convert_localization_label(args.data_path, + args.out_path, + action_dict, + fps=25.0) + + else: + raise NotImplementedError + + +def get_arguments(): + """ + parse all the arguments from command line inteface + return a list of parsed arguments + """ + + parser = argparse.ArgumentParser( + description="convert segmentation and localization label") + parser.add_argument("label_path", type=str, help="path of a label file") + parser.add_argument( + "data_path", + type=str, + help="path of video feature or segmentation label txt.", + ) + parser.add_argument( + "out_path", + type=str, + help="path of output file.", + ) + parser.add_argument( + "--mode", + type=str, + default="segmentation", + help="Convert segmentation label or localization label.", + ) + + return parser.parse_args() + + +if __name__ == "__main__": + main() diff --git a/data/ava/script/cut_videos.sh b/data/ava/script/cut_videos.sh new file mode 100644 index 0000000000000000000000000000000000000000..ec9469301cc225a256ce7d15030c507b6c89cab3 --- /dev/null +++ b/data/ava/script/cut_videos.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################## + +# Cut each video from its 15th to 30th minute. + +IN_DATA_DIR="../../../data/ava/videos" +OUT_DATA_DIR="../../../data/ava/videos_15min" + +if [[ ! -d "${OUT_DATA_DIR}" ]]; then + echo "${OUT_DATA_DIR} doesn't exist. Creating it."; + mkdir -p ${OUT_DATA_DIR} +fi + +for video in $(ls -A1 -U ${IN_DATA_DIR}/*) +do + out_name="${OUT_DATA_DIR}/${video##*/}" + if [ ! -f "${out_name}" ]; then + ffmpeg -ss 900 -t 901 -r 30 -i "${video}" -strict experimental "${out_name}" + fi +done diff --git a/data/ava/script/download_annotations.sh b/data/ava/script/download_annotations.sh new file mode 100644 index 0000000000000000000000000000000000000000..e914a19522f627c404d8ca4ff4899b049456774f --- /dev/null +++ b/data/ava/script/download_annotations.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -e + +VERSION=${VERSION:-"2.1"} +DATA_DIR="../../../data/ava/annotations" + +if [[ ! -d "${DATA_DIR}" ]]; then + echo "${DATA_DIR} does not exist. Creating"; + mkdir -p ${DATA_DIR} +fi + +wget https://s3.amazonaws.com/ava-dataset/annotations/ava_v${VERSION}.zip +unzip -j ava_v${VERSION}.zip -d ${DATA_DIR}/ +rm ava_v${VERSION}.zip diff --git a/data/ava/script/download_videos.sh b/data/ava/script/download_videos.sh new file mode 100644 index 0000000000000000000000000000000000000000..ba8c5692b2dd13bca7c9e1d0d3736496da0d036b --- /dev/null +++ b/data/ava/script/download_videos.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +DATA_DIR="../../../data/ava/videos" +ANNO_DIR="../../../data/ava/annotations" + +if [[ ! -d "${DATA_DIR}" ]]; then + echo "${DATA_DIR} does not exist. Creating"; + mkdir -p ${DATA_DIR} +fi + +wget https://s3.amazonaws.com/ava-dataset/annotations/ava_file_names_trainval_v2.1.txt -P ${ANNO_DIR} + +cat ${ANNO_DIR}/ava_file_names_trainval_v2.1.txt | +while read vid; + do wget -c "https://s3.amazonaws.com/ava-dataset/trainval/${vid}" -P ${DATA_DIR}; done + +echo "Downloading finished." diff --git a/data/ava/script/extract_rgb_frames.sh b/data/ava/script/extract_rgb_frames.sh new file mode 100644 index 0000000000000000000000000000000000000000..24cc52e5d0b79af3b529f18162e2f3806f76e998 --- /dev/null +++ b/data/ava/script/extract_rgb_frames.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################## + +# Extract frames from videos. + +IN_DATA_DIR="../../../data/ava/videos_15min" +OUT_DATA_DIR="../../../data/ava/rawframes" + +if [[ ! -d "${OUT_DATA_DIR}" ]]; then + echo "${OUT_DATA_DIR} doesn't exist. Creating it."; + mkdir -p ${OUT_DATA_DIR} +fi + +for video in $(ls -A1 -U ${IN_DATA_DIR}/*) +do + video_name=${video##*/} + + if [[ $video_name = *".webm" ]]; then + video_name=${video_name::-5} + else + video_name=${video_name::-4} + fi + + out_video_dir=${OUT_DATA_DIR}/${video_name}/ + mkdir -p "${out_video_dir}" + + out_name="${out_video_dir}/%05d.jpg" + + ffmpeg -i "${video}" -r 30 -q:v 1 "${out_name}" +done diff --git a/data/ava/script/extract_video_frames.sh b/data/ava/script/extract_video_frames.sh new file mode 100644 index 0000000000000000000000000000000000000000..e05c956d02a6507c5b0e488ae43166c7488eb94d --- /dev/null +++ b/data/ava/script/extract_video_frames.sh @@ -0,0 +1,6 @@ +o "Frame path: $1" +echo "Video path: $2" +echo "FPS: $3" +out_name="$1/%05d.jpg" +echo "${out_name}" +ffmpeg -i $2 -r $3 -q:v 1 "${out_name}" diff --git a/data/ava/script/fetch_ava_proposals.sh b/data/ava/script/fetch_ava_proposals.sh new file mode 100644 index 0000000000000000000000000000000000000000..57d2b2aa0743f1dc10a9ff01a93d0a062110763b --- /dev/null +++ b/data/ava/script/fetch_ava_proposals.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +DATA_DIR="../../../data/ava/annotations" + +wget https://download.openmmlab.com/mmaction/dataset/ava/ava_dense_proposals_train.FAIR.recall_93.9.pkl -P ${DATA_DIR} +wget https://download.openmmlab.com/mmaction/dataset/ava/ava_dense_proposals_val.FAIR.recall_93.9.pkl -P ${DATA_DIR} +wget https://download.openmmlab.com/mmaction/dataset/ava/ava_dense_proposals_test.FAIR.recall_93.9.pkl -P ${DATA_DIR} diff --git a/data/example.avi b/data/example.avi new file mode 100644 index 0000000000000000000000000000000000000000..ded660f086d8c488fdae736e2bb3ada547b64908 Binary files /dev/null and b/data/example.avi differ diff --git a/data/example.pkl b/data/example.pkl new file mode 100644 index 0000000000000000000000000000000000000000..88325122b6b6ab1643c858b093d6794aaf157ced Binary files /dev/null and b/data/example.pkl differ diff --git a/data/example.png b/data/example.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1f090947e9784c0e7685cfeb9a54ea66c38fb9 Binary files /dev/null and b/data/example.png differ diff --git a/data/example_NTU-RGB-D_sketeton.npy b/data/example_NTU-RGB-D_sketeton.npy new file mode 100644 index 0000000000000000000000000000000000000000..aa25b118edaeb7e3914533af83847e0064d2c6a2 Binary files /dev/null and b/data/example_NTU-RGB-D_sketeton.npy differ diff --git a/data/example_feat.list b/data/example_feat.list new file mode 100644 index 0000000000000000000000000000000000000000..27871479307726e420028be8b966e02afcf3ffd2 --- /dev/null +++ b/data/example_feat.list @@ -0,0 +1 @@ +{"feat_path": "data/example_feat.npy", "duration_second": 124.23} diff --git a/data/example_feat.npy b/data/example_feat.npy new file mode 100644 index 0000000000000000000000000000000000000000..45ef0d7c6e7086bbbb9f979b71cf2ca639e01d97 Binary files /dev/null and b/data/example_feat.npy differ diff --git a/data/fsd10/example_skeleton.npy b/data/fsd10/example_skeleton.npy new file mode 100644 index 0000000000000000000000000000000000000000..c3e42166db65834dd2bd75fe21ef0ed748fe6cbb Binary files /dev/null and b/data/fsd10/example_skeleton.npy differ diff --git a/data/howto100m/download_features.sh b/data/howto100m/download_features.sh new file mode 100644 index 0000000000000000000000000000000000000000..71095aff89a4bd2e256f1c0b5f73c93783743141 --- /dev/null +++ b/data/howto100m/download_features.sh @@ -0,0 +1,3 @@ +wget https://videotag.bj.bcebos.com/Data/ActBERT/actbert_train_data.npy +wget https://videotag.bj.bcebos.com/Data/ActBERT/caption_train.json +wget https://videotag.bj.bcebos.com/Data/ActBERT/caption_val.json \ No newline at end of file diff --git a/data/k400/Kinetics-400_label_list.txt b/data/k400/Kinetics-400_label_list.txt new file mode 100644 index 0000000000000000000000000000000000000000..8488908b1de6cffed45d1d3cc95b370830add69e --- /dev/null +++ b/data/k400/Kinetics-400_label_list.txt @@ -0,0 +1,400 @@ +0 abseiling +1 air_drumming +2 answering_questions +3 applauding +4 applying_cream +5 archery +6 arm_wrestling +7 arranging_flowers +8 assembling_computer +9 auctioning +10 baby_waking_up +11 baking_cookies +12 balloon_blowing +13 bandaging +14 barbequing +15 bartending +16 beatboxing +17 bee_keeping +18 belly_dancing +19 bench_pressing +20 bending_back +21 bending_metal +22 biking_through_snow +23 blasting_sand +24 blowing_glass +25 blowing_leaves +26 blowing_nose +27 blowing_out_candles +28 bobsledding +29 bookbinding +30 bouncing_on_trampoline +31 bowling +32 braiding_hair +33 breading_or_breadcrumbing +34 breakdancing +35 brush_painting +36 brushing_hair +37 brushing_teeth +38 building_cabinet +39 building_shed +40 bungee_jumping +41 busking +42 canoeing_or_kayaking +43 capoeira +44 carrying_baby +45 cartwheeling +46 carving_pumpkin +47 catching_fish +48 catching_or_throwing_baseball +49 catching_or_throwing_frisbee +50 catching_or_throwing_softball +51 celebrating +52 changing_oil +53 changing_wheel +54 checking_tires +55 cheerleading +56 chopping_wood +57 clapping +58 clay_pottery_making +59 clean_and_jerk +60 cleaning_floor +61 cleaning_gutters +62 cleaning_pool +63 cleaning_shoes +64 cleaning_toilet +65 cleaning_windows +66 climbing_a_rope +67 climbing_ladder +68 climbing_tree +69 contact_juggling +70 cooking_chicken +71 cooking_egg +72 cooking_on_campfire +73 cooking_sausages +74 counting_money +75 country_line_dancing +76 cracking_neck +77 crawling_baby +78 crossing_river +79 crying +80 curling_hair +81 cutting_nails +82 cutting_pineapple +83 cutting_watermelon +84 dancing_ballet +85 dancing_charleston +86 dancing_gangnam_style +87 dancing_macarena +88 deadlifting +89 decorating_the_christmas_tree +90 digging +91 dining +92 disc_golfing +93 diving_cliff +94 dodgeball +95 doing_aerobics +96 doing_laundry +97 doing_nails +98 drawing +99 dribbling_basketball +100 drinking +101 drinking_beer +102 drinking_shots +103 driving_car +104 driving_tractor +105 drop_kicking +106 drumming_fingers +107 dunking_basketball +108 dying_hair +109 eating_burger +110 eating_cake +111 eating_carrots +112 eating_chips +113 eating_doughnuts +114 eating_hotdog +115 eating_ice_cream +116 eating_spaghetti +117 eating_watermelon +118 egg_hunting +119 exercising_arm +120 exercising_with_an_exercise_ball +121 extinguishing_fire +122 faceplanting +123 feeding_birds +124 feeding_fish +125 feeding_goats +126 filling_eyebrows +127 finger_snapping +128 fixing_hair +129 flipping_pancake +130 flying_kite +131 folding_clothes +132 folding_napkins +133 folding_paper +134 front_raises +135 frying_vegetables +136 garbage_collecting +137 gargling +138 getting_a_haircut +139 getting_a_tattoo +140 giving_or_receiving_award +141 golf_chipping +142 golf_driving +143 golf_putting +144 grinding_meat +145 grooming_dog +146 grooming_horse +147 gymnastics_tumbling +148 hammer_throw +149 headbanging +150 headbutting +151 high_jump +152 high_kick +153 hitting_baseball +154 hockey_stop +155 holding_snake +156 hopscotch +157 hoverboarding +158 hugging +159 hula_hooping +160 hurdling +161 hurling_(sport) +162 ice_climbing +163 ice_fishing +164 ice_skating +165 ironing +166 javelin_throw +167 jetskiing +168 jogging +169 juggling_balls +170 juggling_fire +171 juggling_soccer_ball +172 jumping_into_pool +173 jumpstyle_dancing +174 kicking_field_goal +175 kicking_soccer_ball +176 kissing +177 kitesurfing +178 knitting +179 krumping +180 laughing +181 laying_bricks +182 long_jump +183 lunge +184 making_a_cake +185 making_a_sandwich +186 making_bed +187 making_jewelry +188 making_pizza +189 making_snowman +190 making_sushi +191 making_tea +192 marching +193 massaging_back +194 massaging_feet +195 massaging_legs +196 massaging_person's_head +197 milking_cow +198 mopping_floor +199 motorcycling +200 moving_furniture +201 mowing_lawn +202 news_anchoring +203 opening_bottle +204 opening_present +205 paragliding +206 parasailing +207 parkour +208 passing_American_football_(in_game) +209 passing_American_football_(not_in_game) +210 peeling_apples +211 peeling_potatoes +212 petting_animal_(not_cat) +213 petting_cat +214 picking_fruit +215 planting_trees +216 plastering +217 playing_accordion +218 playing_badminton +219 playing_bagpipes +220 playing_basketball +221 playing_bass_guitar +222 playing_cards +223 playing_cello +224 playing_chess +225 playing_clarinet +226 playing_controller +227 playing_cricket +228 playing_cymbals +229 playing_didgeridoo +230 playing_drums +231 playing_flute +232 playing_guitar +233 playing_harmonica +234 playing_harp +235 playing_ice_hockey +236 playing_keyboard +237 playing_kickball +238 playing_monopoly +239 playing_organ +240 playing_paintball +241 playing_piano +242 playing_poker +243 playing_recorder +244 playing_saxophone +245 playing_squash_or_racquetball +246 playing_tennis +247 playing_trombone +248 playing_trumpet +249 playing_ukulele +250 playing_violin +251 playing_volleyball +252 playing_xylophone +253 pole_vault +254 presenting_weather_forecast +255 pull_ups +256 pumping_fist +257 pumping_gas +258 punching_bag +259 punching_person_(boxing) +260 push_up +261 pushing_car +262 pushing_cart +263 pushing_wheelchair +264 reading_book +265 reading_newspaper +266 recording_music +267 riding_a_bike +268 riding_camel +269 riding_elephant +270 riding_mechanical_bull +271 riding_mountain_bike +272 riding_mule +273 riding_or_walking_with_horse +274 riding_scooter +275 riding_unicycle +276 ripping_paper +277 robot_dancing +278 rock_climbing +279 rock_scissors_paper +280 roller_skating +281 running_on_treadmill +282 sailing +283 salsa_dancing +284 sanding_floor +285 scrambling_eggs +286 scuba_diving +287 setting_table +288 shaking_hands +289 shaking_head +290 sharpening_knives +291 sharpening_pencil +292 shaving_head +293 shaving_legs +294 shearing_sheep +295 shining_shoes +296 shooting_basketball +297 shooting_goal_(soccer) +298 shot_put +299 shoveling_snow +300 shredding_paper +301 shuffling_cards +302 side_kick +303 sign_language_interpreting +304 singing +305 situp +306 skateboarding +307 ski_jumping +308 skiing_(not_slalom_or_crosscountry) +309 skiing_crosscountry +310 skiing_slalom +311 skipping_rope +312 skydiving +313 slacklining +314 slapping +315 sled_dog_racing +316 smoking +317 smoking_hookah +318 snatch_weight_lifting +319 sneezing +320 sniffing +321 snorkeling +322 snowboarding +323 snowkiting +324 snowmobiling +325 somersaulting +326 spinning_poi +327 spray_painting +328 spraying +329 springboard_diving +330 squat +331 sticking_tongue_out +332 stomping_grapes +333 stretching_arm +334 stretching_leg +335 strumming_guitar +336 surfing_crowd +337 surfing_water +338 sweeping_floor +339 swimming_backstroke +340 swimming_breast_stroke +341 swimming_butterfly_stroke +342 swing_dancing +343 swinging_legs +344 swinging_on_something +345 sword_fighting +346 tai_chi +347 taking_a_shower +348 tango_dancing +349 tap_dancing +350 tapping_guitar +351 tapping_pen +352 tasting_beer +353 tasting_food +354 testifying +355 texting +356 throwing_axe +357 throwing_ball +358 throwing_discus +359 tickling +360 tobogganing +361 tossing_coin +362 tossing_salad +363 training_dog +364 trapezing +365 trimming_or_shaving_beard +366 trimming_trees +367 triple_jump +368 tying_bow_tie +369 tying_knot_(not_on_a_tie) +370 tying_tie +371 unboxing +372 unloading_truck +373 using_computer +374 using_remote_controller_(not_gaming) +375 using_segway +376 vault +377 waiting_in_line +378 walking_the_dog +379 washing_dishes +380 washing_feet +381 washing_hair +382 washing_hands +383 water_skiing +384 water_sliding +385 watering_plants +386 waxing_back +387 waxing_chest +388 waxing_eyebrows +389 waxing_legs +390 weaving_basket +391 welding +392 whistling +393 windsurfing +394 wrapping_present +395 wrestling +396 writing +397 yawning +398 yoga +399 zumba diff --git a/data/k400/download_k400_data.sh b/data/k400/download_k400_data.sh new file mode 100644 index 0000000000000000000000000000000000000000..acdf0a346fa2c56bcded3462ebfce8b8e2a56f2a --- /dev/null +++ b/data/k400/download_k400_data.sh @@ -0,0 +1,9 @@ +file=$1 +while read line +do + wget "$line" + array=(${line//// }) + file_name=${array[-1]} + cmd=`tar -xzvf ${file_name} --strip-components 1 -C ./videos` + eval $cmd +done <$file diff --git a/data/k400/extract_rawframes.py b/data/k400/extract_rawframes.py new file mode 100644 index 0000000000000000000000000000000000000000..c3aeef53ea95d079c8ef24428191012ec44cb4be --- /dev/null +++ b/data/k400/extract_rawframes.py @@ -0,0 +1,96 @@ +import argparse +import sys +import os +import os.path as osp +import glob +from pipes import quote +from multiprocessing import Pool, current_process +import cv2 + + +def dump_frames(vid_item): + full_path, vid_path, vid_id = vid_item + vid_name = vid_path + out_full_path = osp.join(args.out_dir, vid_name) + try: + os.mkdir(out_full_path) + except OSError: + pass + vr = cv2.VideoCapture(full_path) + videolen = int(vr.get(cv2.CAP_PROP_FRAME_COUNT)) + for i in range(videolen): + ret, frame = vr.read() + if ret == False: + continue + img = frame[:, :, ::-1] + # covert to BGR img + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + if img is not None: + # cv2.imwrite will automatically write the BGR image into RGB image + cv2.imwrite('{}/img_{:05d}.jpg'.format(out_full_path, i + 1), img) + else: + print('[Warning] length inconsistent!' + 'Early stop with {} out of {} frames'.format(i + 1, videolen)) + break + print('full_path={} vid_name={} num_frames={} dump_frames done'.format(full_path, vid_name, videolen)) + sys.stdout.flush() + return True + + +def parse_args(): + parser = argparse.ArgumentParser(description='extract optical flows') + parser.add_argument('src_dir', type=str) + parser.add_argument('out_dir', type=str) + parser.add_argument('--level', type=int, + choices=[1, 2], + default=2) + parser.add_argument('--num_worker', type=int, default=8) + parser.add_argument("--out_format", type=str, default='dir', + choices=['dir', 'zip'], help='output format') + parser.add_argument("--ext", type=str, default='avi', + choices=['avi', 'mp4'], help='video file extensions') + parser.add_argument("--resume", action='store_true', default=False, + help='resume optical flow extraction ' + 'instead of overwriting') + args = parser.parse_args() + + return args + + +if __name__ == '__main__': + args = parse_args() + if not osp.isdir(args.out_dir): + print('Creating folder: {}'.format(args.out_dir)) + os.makedirs(args.out_dir) + if args.level == 2: + classes = os.listdir(args.src_dir) + for classname in classes: + new_dir = osp.join(args.out_dir, classname) + if not osp.isdir(new_dir): + print('Creating folder: {}'.format(new_dir)) + os.makedirs(new_dir) + + print('Reading videos from folder: ', args.src_dir) + print('Extension of videos: ', args.ext) + print("args.src_dir:",args.src_dir) + if args.level == 2: + fullpath_list = glob.glob(args.src_dir + '/*/*') + done_fullpath_list = glob.glob(args.out_dir + '/*/*') + elif args.level == 1: + fullpath_list = glob.glob(args.src_dir + '/*' ) + done_fullpath_list = glob.glob(args.out_dir + '/*') + print('Total number of videos found: ', len(fullpath_list)) + if args.resume: + fullpath_list = set(fullpath_list).difference(set(done_fullpath_list)) + fullpath_list = list(fullpath_list) + print('Resuming. number of videos to be done: ', len(fullpath_list)) + + if args.level == 2: + vid_list = list(map(lambda p: osp.join( + '/'.join(p.split('/')[-2:])), fullpath_list)) + elif args.level == 1: + vid_list = list(map(lambda p: p.split('/')[-1], fullpath_list)) + + pool = Pool(args.num_worker) + pool.map(dump_frames, zip( + fullpath_list, vid_list, range(len(vid_list)))) diff --git a/data/k400/train_small_frames.list b/data/k400/train_small_frames.list new file mode 100644 index 0000000000000000000000000000000000000000..472300e13df26fd63263723603d275eec714482e --- /dev/null +++ b/data/k400/train_small_frames.list @@ -0,0 +1,7680 @@ +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 diff --git a/data/k400/train_small_videos.list b/data/k400/train_small_videos.list new file mode 100644 index 0000000000000000000000000000000000000000..b62da8c7881d9a826f320d43ebeaef29369cf6ea --- /dev/null +++ b/data/k400/train_small_videos.list @@ -0,0 +1,7680 @@ +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 diff --git a/data/k400/val_small_frames.list b/data/k400/val_small_frames.list new file mode 100644 index 0000000000000000000000000000000000000000..34dcb83a8598f03bb950412d38519261dcdd1bdb --- /dev/null +++ b/data/k400/val_small_frames.list @@ -0,0 +1,200 @@ +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 250 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 293 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 236 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 130 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 255 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 300 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 250 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 150 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 300 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 250 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 300 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 300 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 240 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 250 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 300 0 +abseiling/_xN3j__e368_000001_000011.mp4 300 0 diff --git a/data/k400/val_small_videos.list b/data/k400/val_small_videos.list new file mode 100644 index 0000000000000000000000000000000000000000..67f278eadabcd844e45a160f230d20480ade4476 --- /dev/null +++ b/data/k400/val_small_videos.list @@ -0,0 +1,128 @@ +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 +abseiling/_4YTwq0-73Y_000044_000054.mp4 0 +abseiling/_EDt9CNqqxk_000260_000270.mp4 0 +abseiling/_HVO2_ZrJno_000048_000058.mp4 0 +abseiling/_IkgLzKQVzk_000020_000030.mp4 0 +abseiling/_SSkUUyN_zA_000058_000068.mp4 0 +abseiling/_UtLXOVn5Jk_000083_000093.mp4 0 +abseiling/_WWyCxEok2I_000078_000088.mp4 0 +abseiling/_xN3j__e368_000001_000011.mp4 0 +air_drumming/_9SlRyQ2Aio_000192_000202.mp4 1 +air_drumming/_axE99QAhe8_000026_000036.mp4 1 +air_drumming/_dbcJuKJQNs_000040_000050.mp4 1 +air_drumming/_DBoa58Pzb4_000185_000195.mp4 1 +air_drumming/_EKBiWK_AFg_000023_000033.mp4 1 +air_drumming/_GQytBQLCtE_000033_000043.mp4 1 +air_drumming/_lQsYpklt5c_000041_000051.mp4 1 +air_drumming/_m883MdWzbo_000016_000026.mp4 1 diff --git a/data/ntu-rgb-d/download_dataset.sh b/data/ntu-rgb-d/download_dataset.sh new file mode 100644 index 0000000000000000000000000000000000000000..bcdadf06dff87d230ae94e41d03db541e77bb6cc --- /dev/null +++ b/data/ntu-rgb-d/download_dataset.sh @@ -0,0 +1,12 @@ +cd data/ntu-rgb-d + +# download +wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://docs.google.com/uc?export=download&id=1CUZnBtYwifVXS21yVg62T-vrPVayso5H' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=1CUZnBtYwifVXS21yVg62T-vrPVayso5H" -O nturgbd_skeletons_s001_to_s017.zip && rm -rf /tmp/cookies.txt + +unzip nturgbd_skeletons_s001_to_s017.zip && rm -rf nturgbd_skeletons_s001_to_s017.zip + +wget https://videotag.bj.bcebos.com/Data/statistics.zip + +mkdir statistics + +unzip statistics.zip -d statistics/ && rm -rf statistics.zip diff --git a/data/ntu-rgb-d/get_raw_denoised_data.py b/data/ntu-rgb-d/get_raw_denoised_data.py new file mode 100644 index 0000000000000000000000000000000000000000..be5457bca5ed9c410fa1558f26360401e9834024 --- /dev/null +++ b/data/ntu-rgb-d/get_raw_denoised_data.py @@ -0,0 +1,471 @@ +# ref: https://github.com/Uason-Chen/CTR-GCN/blob/main/data/ntu/get_raw_denoised_data.py + +import os +import os.path as osp +import numpy as np +import pickle +import logging + +root_path = './' +raw_data_file = osp.join(root_path, 'raw_data', 'raw_skes_data.pkl') +save_path = osp.join(root_path, 'denoised_data') + +if not osp.exists(save_path): + os.mkdir(save_path) + +rgb_ske_path = osp.join(save_path, 'rgb+ske') +if not osp.exists(rgb_ske_path): + os.mkdir(rgb_ske_path) + +actors_info_dir = osp.join(save_path, 'actors_info') +if not osp.exists(actors_info_dir): + os.mkdir(actors_info_dir) + +missing_count = 0 +noise_len_thres = 11 +noise_spr_thres1 = 0.8 +noise_spr_thres2 = 0.69754 +noise_mot_thres_lo = 0.089925 +noise_mot_thres_hi = 2 + +noise_len_logger = logging.getLogger('noise_length') +noise_len_logger.setLevel(logging.INFO) +noise_len_logger.addHandler( + logging.FileHandler(osp.join(save_path, 'noise_length.log'))) +noise_len_logger.info('{:^20}\t{:^17}\t{:^8}\t{}'.format( + 'Skeleton', 'bodyID', 'Motion', 'Length')) + +noise_spr_logger = logging.getLogger('noise_spread') +noise_spr_logger.setLevel(logging.INFO) +noise_spr_logger.addHandler( + logging.FileHandler(osp.join(save_path, 'noise_spread.log'))) +noise_spr_logger.info('{:^20}\t{:^17}\t{:^8}\t{:^8}'.format( + 'Skeleton', 'bodyID', 'Motion', 'Rate')) + +noise_mot_logger = logging.getLogger('noise_motion') +noise_mot_logger.setLevel(logging.INFO) +noise_mot_logger.addHandler( + logging.FileHandler(osp.join(save_path, 'noise_motion.log'))) +noise_mot_logger.info('{:^20}\t{:^17}\t{:^8}'.format('Skeleton', 'bodyID', + 'Motion')) + +fail_logger_1 = logging.getLogger('noise_outliers_1') +fail_logger_1.setLevel(logging.INFO) +fail_logger_1.addHandler( + logging.FileHandler(osp.join(save_path, 'denoised_failed_1.log'))) + +fail_logger_2 = logging.getLogger('noise_outliers_2') +fail_logger_2.setLevel(logging.INFO) +fail_logger_2.addHandler( + logging.FileHandler(osp.join(save_path, 'denoised_failed_2.log'))) + +missing_skes_logger = logging.getLogger('missing_frames') +missing_skes_logger.setLevel(logging.INFO) +missing_skes_logger.addHandler( + logging.FileHandler(osp.join(save_path, 'missing_skes.log'))) +missing_skes_logger.info('{:^20}\t{}\t{}'.format('Skeleton', 'num_frames', + 'num_missing')) + +missing_skes_logger1 = logging.getLogger('missing_frames_1') +missing_skes_logger1.setLevel(logging.INFO) +missing_skes_logger1.addHandler( + logging.FileHandler(osp.join(save_path, 'missing_skes_1.log'))) +missing_skes_logger1.info('{:^20}\t{}\t{}\t{}\t{}\t{}'.format( + 'Skeleton', 'num_frames', 'Actor1', 'Actor2', 'Start', 'End')) + +missing_skes_logger2 = logging.getLogger('missing_frames_2') +missing_skes_logger2.setLevel(logging.INFO) +missing_skes_logger2.addHandler( + logging.FileHandler(osp.join(save_path, 'missing_skes_2.log'))) +missing_skes_logger2.info('{:^20}\t{}\t{}\t{}'.format('Skeleton', 'num_frames', + 'Actor1', 'Actor2')) + + +def denoising_by_length(ske_name, bodies_data): + """ + Denoising data based on the frame length for each bodyID. + Filter out the bodyID which length is less or equal than the predefined threshold. + + """ + noise_info = str() + new_bodies_data = bodies_data.copy() + for (bodyID, body_data) in new_bodies_data.items(): + length = len(body_data['interval']) + if length <= noise_len_thres: + noise_info += 'Filter out: %s, %d (length).\n' % (bodyID, length) + noise_len_logger.info('{}\t{}\t{:.6f}\t{:^6d}'.format( + ske_name, bodyID, body_data['motion'], length)) + del bodies_data[bodyID] + if noise_info != '': + noise_info += '\n' + + return bodies_data, noise_info + + +def get_valid_frames_by_spread(points): + """ + Find the valid (or reasonable) frames (index) based on the spread of X and Y. + + :param points: joints or colors + """ + num_frames = points.shape[0] + valid_frames = [] + for i in range(num_frames): + x = points[i, :, 0] + y = points[i, :, 1] + if (x.max() - x.min()) <= noise_spr_thres1 * (y.max() - y.min()): # 0.8 + valid_frames.append(i) + return valid_frames + + +def denoising_by_spread(ske_name, bodies_data): + """ + Denoising data based on the spread of Y value and X value. + Filter out the bodyID which the ratio of noisy frames is higher than the predefined + threshold. + + bodies_data: contains at least 2 bodyIDs + """ + noise_info = str() + denoised_by_spr = False # mark if this sequence has been processed by spread. + + new_bodies_data = bodies_data.copy() + # for (bodyID, body_data) in bodies_data.items(): + for (bodyID, body_data) in new_bodies_data.items(): + if len(bodies_data) == 1: + break + valid_frames = get_valid_frames_by_spread(body_data['joints'].reshape( + -1, 25, 3)) + num_frames = len(body_data['interval']) + num_noise = num_frames - len(valid_frames) + if num_noise == 0: + continue + + ratio = num_noise / float(num_frames) + motion = body_data['motion'] + if ratio >= noise_spr_thres2: # 0.69754 + del bodies_data[bodyID] + denoised_by_spr = True + noise_info += 'Filter out: %s (spread rate >= %.2f).\n' % ( + bodyID, noise_spr_thres2) + noise_spr_logger.info('%s\t%s\t%.6f\t%.6f' % + (ske_name, bodyID, motion, ratio)) + else: # Update motion + joints = body_data['joints'].reshape(-1, 25, 3)[valid_frames] + body_data['motion'] = min( + motion, np.sum(np.var(joints.reshape(-1, 3), axis=0))) + noise_info += '%s: motion %.6f -> %.6f\n' % (bodyID, motion, + body_data['motion']) + # TODO: Consider removing noisy frames for each bodyID + + if noise_info != '': + noise_info += '\n' + + return bodies_data, noise_info, denoised_by_spr + + +def denoising_by_motion(ske_name, bodies_data, bodies_motion): + """ + Filter out the bodyID which motion is out of the range of predefined interval + + """ + # Sort bodies based on the motion, return a list of tuples + # bodies_motion = sorted(bodies_motion.items(), key=lambda x, y: cmp(x[1], y[1]), reverse=True) + bodies_motion = sorted(bodies_motion.items(), + key=lambda x: x[1], + reverse=True) + + # Reserve the body data with the largest motion + denoised_bodies_data = [(bodies_motion[0][0], + bodies_data[bodies_motion[0][0]])] + noise_info = str() + + for (bodyID, motion) in bodies_motion[1:]: + if (motion < noise_mot_thres_lo) or (motion > noise_mot_thres_hi): + noise_info += 'Filter out: %s, %.6f (motion).\n' % (bodyID, motion) + noise_mot_logger.info('{}\t{}\t{:.6f}'.format( + ske_name, bodyID, motion)) + else: + denoised_bodies_data.append((bodyID, bodies_data[bodyID])) + if noise_info != '': + noise_info += '\n' + + return denoised_bodies_data, noise_info + + +def denoising_bodies_data(bodies_data): + """ + Denoising data based on some heuristic methods, not necessarily correct for all samples. + + Return: + denoised_bodies_data (list): tuple: (bodyID, body_data). + """ + ske_name = bodies_data['name'] + bodies_data = bodies_data['data'] + + # Step 1: Denoising based on frame length. + bodies_data, noise_info_len = denoising_by_length(ske_name, bodies_data) + + if len(bodies_data) == 1: # only has one bodyID left after step 1 + return bodies_data.items(), noise_info_len + + # Step 2: Denoising based on spread. + bodies_data, noise_info_spr, denoised_by_spr = denoising_by_spread( + ske_name, bodies_data) + + if len(bodies_data) == 1: + return bodies_data.items(), noise_info_len + noise_info_spr + + bodies_motion = dict() # get body motion + for (bodyID, body_data) in bodies_data.items(): + bodies_motion[bodyID] = body_data['motion'] + # Sort bodies based on the motion + # bodies_motion = sorted(bodies_motion.items(), key=lambda x, y: cmp(x[1], y[1]), reverse=True) + bodies_motion = sorted(bodies_motion.items(), + key=lambda x: x[1], + reverse=True) + denoised_bodies_data = list() + for (bodyID, _) in bodies_motion: + denoised_bodies_data.append((bodyID, bodies_data[bodyID])) + + return denoised_bodies_data, noise_info_len + noise_info_spr + + # TODO: Consider denoising further by integrating motion method + + # if denoised_by_spr: # this sequence has been denoised by spread + # bodies_motion = sorted(bodies_motion.items(), lambda x, y: cmp(x[1], y[1]), reverse=True) + # denoised_bodies_data = list() + # for (bodyID, _) in bodies_motion: + # denoised_bodies_data.append((bodyID, bodies_data[bodyID])) + # return denoised_bodies_data, noise_info + + # Step 3: Denoising based on motion + # bodies_data, noise_info = denoising_by_motion(ske_name, bodies_data, bodies_motion) + + # return bodies_data, noise_info + + +def get_one_actor_points(body_data, num_frames): + """ + Get joints and colors for only one actor. + For joints, each frame contains 75 X-Y-Z coordinates. + For colors, each frame contains 25 x 2 (X, Y) coordinates. + """ + joints = np.zeros((num_frames, 75), dtype=np.float32) + colors = np.ones((num_frames, 1, 25, 2), dtype=np.float32) * np.nan + start, end = body_data['interval'][0], body_data['interval'][-1] + joints[start:end + 1] = body_data['joints'].reshape(-1, 75) + colors[start:end + 1, 0] = body_data['colors'] + + return joints, colors + + +def remove_missing_frames(ske_name, joints, colors): + """ + Cut off missing frames which all joints positions are 0s + + For the sequence with 2 actors' data, also record the number of missing frames for + actor1 and actor2, respectively (for debug). + """ + num_frames = joints.shape[0] + num_bodies = colors.shape[1] # 1 or 2 + + if num_bodies == 2: # DEBUG + missing_indices_1 = np.where(joints[:, :75].sum(axis=1) == 0)[0] + missing_indices_2 = np.where(joints[:, 75:].sum(axis=1) == 0)[0] + cnt1 = len(missing_indices_1) + cnt2 = len(missing_indices_2) + + start = 1 if 0 in missing_indices_1 else 0 + end = 1 if num_frames - 1 in missing_indices_1 else 0 + if max(cnt1, cnt2) > 0: + if cnt1 > cnt2: + info = '{}\t{:^10d}\t{:^6d}\t{:^6d}\t{:^5d}\t{:^3d}'.format( + ske_name, num_frames, cnt1, cnt2, start, end) + missing_skes_logger1.info(info) + else: + info = '{}\t{:^10d}\t{:^6d}\t{:^6d}'.format( + ske_name, num_frames, cnt1, cnt2) + missing_skes_logger2.info(info) + + # Find valid frame indices that the data is not missing or lost + # For two-subjects action, this means both data of actor1 and actor2 is missing. + valid_indices = np.where(joints.sum(axis=1) != 0)[0] # 0-based index + missing_indices = np.where(joints.sum(axis=1) == 0)[0] + num_missing = len(missing_indices) + + if num_missing > 0: # Update joints and colors + joints = joints[valid_indices] + colors[missing_indices] = np.nan + global missing_count + missing_count += 1 + missing_skes_logger.info('{}\t{:^10d}\t{:^11d}'.format( + ske_name, num_frames, num_missing)) + + return joints, colors + + +def get_bodies_info(bodies_data): + bodies_info = '{:^17}\t{}\t{:^8}\n'.format('bodyID', 'Interval', 'Motion') + for (bodyID, body_data) in bodies_data.items(): + start, end = body_data['interval'][0], body_data['interval'][-1] + bodies_info += '{}\t{:^8}\t{:f}\n'.format(bodyID, str([start, end]), + body_data['motion']) + + return bodies_info + '\n' + + +def get_two_actors_points(bodies_data): + """ + Get the first and second actor's joints positions and colors locations. + + # Arguments: + bodies_data (dict): 3 key-value pairs: 'name', 'data', 'num_frames'. + bodies_data['data'] is also a dict, while the key is bodyID, the value is + the corresponding body_data which is also a dict with 4 keys: + - joints: raw 3D joints positions. Shape: (num_frames x 25, 3) + - colors: raw 2D color locations. Shape: (num_frames, 25, 2) + - interval: a list which records the frame indices. + - motion: motion amount + + # Return: + joints, colors. + """ + ske_name = bodies_data['name'] + label = int(ske_name[-2:]) + num_frames = bodies_data['num_frames'] + bodies_info = get_bodies_info(bodies_data['data']) + + bodies_data, noise_info = denoising_bodies_data( + bodies_data) # Denoising data + bodies_info += noise_info + + bodies_data = list(bodies_data) + if len(bodies_data) == 1: # Only left one actor after denoising + if label >= 50: # DEBUG: Denoising failed for two-subjects action + fail_logger_2.info(ske_name) + + bodyID, body_data = bodies_data[0] + joints, colors = get_one_actor_points(body_data, num_frames) + bodies_info += 'Main actor: %s' % bodyID + else: + if label < 50: # DEBUG: Denoising failed for one-subject action + fail_logger_1.info(ske_name) + + joints = np.zeros((num_frames, 150), dtype=np.float32) + colors = np.ones((num_frames, 2, 25, 2), dtype=np.float32) * np.nan + + bodyID, actor1 = bodies_data[0] # the 1st actor with largest motion + start1, end1 = actor1['interval'][0], actor1['interval'][-1] + joints[start1:end1 + 1, :75] = actor1['joints'].reshape(-1, 75) + colors[start1:end1 + 1, 0] = actor1['colors'] + actor1_info = '{:^17}\t{}\t{:^8}\n'.format('Actor1', 'Interval', 'Motion') + \ + '{}\t{:^8}\t{:f}\n'.format(bodyID, str([start1, end1]), actor1['motion']) + del bodies_data[0] + + actor2_info = '{:^17}\t{}\t{:^8}\n'.format('Actor2', 'Interval', + 'Motion') + start2, end2 = [0, 0] # initial interval for actor2 (virtual) + + while len(bodies_data) > 0: + bodyID, actor = bodies_data[0] + start, end = actor['interval'][0], actor['interval'][-1] + if min(end1, end) - max(start1, + start) <= 0: # no overlap with actor1 + joints[start:end + 1, :75] = actor['joints'].reshape(-1, 75) + colors[start:end + 1, 0] = actor['colors'] + actor1_info += '{}\t{:^8}\t{:f}\n'.format( + bodyID, str([start, end]), actor['motion']) + # Update the interval of actor1 + start1 = min(start, start1) + end1 = max(end, end1) + elif min(end2, end) - max(start2, + start) <= 0: # no overlap with actor2 + joints[start:end + 1, 75:] = actor['joints'].reshape(-1, 75) + colors[start:end + 1, 1] = actor['colors'] + actor2_info += '{}\t{:^8}\t{:f}\n'.format( + bodyID, str([start, end]), actor['motion']) + # Update the interval of actor2 + start2 = min(start, start2) + end2 = max(end, end2) + del bodies_data[0] + + bodies_info += ('\n' + actor1_info + '\n' + actor2_info) + + with open(osp.join(actors_info_dir, ske_name + '.txt'), 'w') as fw: + fw.write(bodies_info + '\n') + + return joints, colors + + +def get_raw_denoised_data(): + """ + Get denoised data (joints positions and color locations) from raw skeleton sequences. + + For each frame of a skeleton sequence, an actor's 3D positions of 25 joints represented + by an 2D array (shape: 25 x 3) is reshaped into a 75-dim vector by concatenating each + 3-dim (x, y, z) coordinates along the row dimension in joint order. Each frame contains + two actor's joints positions constituting a 150-dim vector. If there is only one actor, + then the last 75 values are filled with zeros. Otherwise, select the main actor and the + second actor based on the motion amount. Each 150-dim vector as a row vector is put into + a 2D numpy array where the number of rows equals the number of valid frames. All such + 2D arrays are put into a list and finally the list is serialized into a cPickle file. + + For the skeleton sequence which contains two or more actors (mostly corresponds to the + last 11 classes), the filename and actors' information are recorded into log files. + For better understanding, also generate RGB+skeleton videos for visualization. + """ + + with open(raw_data_file, 'rb') as fr: # load raw skeletons data + raw_skes_data = pickle.load(fr) + + num_skes = len(raw_skes_data) + print('Found %d available skeleton sequences.' % num_skes) + + raw_denoised_joints = [] + raw_denoised_colors = [] + frames_cnt = [] + + for (idx, bodies_data) in enumerate(raw_skes_data): + ske_name = bodies_data['name'] + print('Processing %s' % ske_name) + num_bodies = len(bodies_data['data']) + + if num_bodies == 1: # only 1 actor + num_frames = bodies_data['num_frames'] + body_data = list(bodies_data['data'].values())[0] + joints, colors = get_one_actor_points(body_data, num_frames) + else: # more than 1 actor, select two main actors + joints, colors = get_two_actors_points(bodies_data) + # Remove missing frames + joints, colors = remove_missing_frames(ske_name, joints, colors) + num_frames = joints.shape[0] # Update + # Visualize selected actors' skeletons on RGB videos. + + raw_denoised_joints.append(joints) + raw_denoised_colors.append(colors) + frames_cnt.append(num_frames) + + if (idx + 1) % 1000 == 0: + print('Processed: %.2f%% (%d / %d), ' % \ + (100.0 * (idx + 1) / num_skes, idx + 1, num_skes) + \ + 'Missing count: %d' % missing_count) + + raw_skes_joints_pkl = osp.join(save_path, 'raw_denoised_joints.pkl') + with open(raw_skes_joints_pkl, 'wb') as f: + pickle.dump(raw_denoised_joints, f, pickle.HIGHEST_PROTOCOL) + + raw_skes_colors_pkl = osp.join(save_path, 'raw_denoised_colors.pkl') + with open(raw_skes_colors_pkl, 'wb') as f: + pickle.dump(raw_denoised_colors, f, pickle.HIGHEST_PROTOCOL) + + frames_cnt = np.array(frames_cnt, dtype=np.int) + np.savetxt(osp.join(save_path, 'frames_cnt.txt'), frames_cnt, fmt='%d') + + print('Saved raw denoised positions of {} frames into {}'.format( + np.sum(frames_cnt), raw_skes_joints_pkl)) + print('Found %d files that have missing data' % missing_count) + + +if __name__ == '__main__': + get_raw_denoised_data() diff --git a/data/ntu-rgb-d/get_raw_skes_data.py b/data/ntu-rgb-d/get_raw_skes_data.py new file mode 100644 index 0000000000000000000000000000000000000000..3cd2912e5980a3117bc2435df17945acc5927089 --- /dev/null +++ b/data/ntu-rgb-d/get_raw_skes_data.py @@ -0,0 +1,157 @@ +# ref: https://github.com/Uason-Chen/CTR-GCN/blob/main/data/ntu/get_raw_skes_data.py + +import os.path as osp +import os +import numpy as np +import pickle +import logging + + +def get_raw_bodies_data(skes_path, ske_name, frames_drop_skes, + frames_drop_logger): + """ + Get raw bodies data from a skeleton sequence. + + Each body's data is a dict that contains the following keys: + - joints: raw 3D joints positions. Shape: (num_frames x 25, 3) + - colors: raw 2D color locations. Shape: (num_frames, 25, 2) + - interval: a list which stores the frame indices of this body. + - motion: motion amount (only for the sequence with 2 or more bodyIDs). + + Return: + a dict for a skeleton sequence with 3 key-value pairs: + - name: the skeleton filename. + - data: a dict which stores raw data of each body. + - num_frames: the number of valid frames. + """ + ske_file = osp.join(skes_path, ske_name + '.skeleton') + assert osp.exists(ske_file), 'Error: Skeleton file %s not found' % ske_file + # Read all data from .skeleton file into a list (in string format) + print('Reading data from %s' % ske_file[-29:]) + with open(ske_file, 'r') as fr: + str_data = fr.readlines() + + num_frames = int(str_data[0].strip('\r\n')) + frames_drop = [] + bodies_data = dict() + valid_frames = -1 # 0-based index + current_line = 1 + + for f in range(num_frames): + num_bodies = int(str_data[current_line].strip('\r\n')) + current_line += 1 + + if num_bodies == 0: # no data in this frame, drop it + frames_drop.append(f) # 0-based index + continue + + valid_frames += 1 + joints = np.zeros((num_bodies, 25, 3), dtype=np.float32) + colors = np.zeros((num_bodies, 25, 2), dtype=np.float32) + + for b in range(num_bodies): + bodyID = str_data[current_line].strip('\r\n').split()[0] + current_line += 1 + num_joints = int(str_data[current_line].strip('\r\n')) # 25 joints + current_line += 1 + + for j in range(num_joints): + temp_str = str_data[current_line].strip('\r\n').split() + joints[b, j, :] = np.array(temp_str[:3], dtype=np.float32) + colors[b, j, :] = np.array(temp_str[5:7], dtype=np.float32) + current_line += 1 + + if bodyID not in bodies_data: # Add a new body's data + body_data = dict() + body_data['joints'] = joints[b] # ndarray: (25, 3) + body_data['colors'] = colors[b, + np.newaxis] # ndarray: (1, 25, 2) + body_data['interval'] = [valid_frames + ] # the index of the first frame + else: # Update an already existed body's data + body_data = bodies_data[bodyID] + # Stack each body's data of each frame along the frame order + body_data['joints'] = np.vstack( + (body_data['joints'], joints[b])) + body_data['colors'] = np.vstack( + (body_data['colors'], colors[b, np.newaxis])) + pre_frame_idx = body_data['interval'][-1] + body_data['interval'].append(pre_frame_idx + + 1) # add a new frame index + + bodies_data[bodyID] = body_data # Update bodies_data + + num_frames_drop = len(frames_drop) + assert num_frames_drop < num_frames, \ + 'Error: All frames data (%d) of %s is missing or lost' % (num_frames, ske_name) + if num_frames_drop > 0: + frames_drop_skes[ske_name] = np.array(frames_drop, dtype=np.int) + frames_drop_logger.info('{}: {} frames missed: {}\n'.format( + ske_name, num_frames_drop, frames_drop)) + + # Calculate motion (only for the sequence with 2 or more bodyIDs) + if len(bodies_data) > 1: + for body_data in bodies_data.values(): + body_data['motion'] = np.sum(np.var(body_data['joints'], axis=0)) + + return { + 'name': ske_name, + 'data': bodies_data, + 'num_frames': num_frames - num_frames_drop + } + + +def get_raw_skes_data(): + + skes_name = np.loadtxt(skes_name_file, dtype=str) + + num_files = skes_name.size + print('Found %d available skeleton files.' % num_files) + + raw_skes_data = [] + frames_cnt = np.zeros(num_files, dtype=np.int) + + for (idx, ske_name) in enumerate(skes_name): + bodies_data = get_raw_bodies_data(skes_path, ske_name, frames_drop_skes, + frames_drop_logger) + raw_skes_data.append(bodies_data) + frames_cnt[idx] = bodies_data['num_frames'] + if (idx + 1) % 1000 == 0: + print('Processed: %.2f%% (%d / %d)' % \ + (100.0 * (idx + 1) / num_files, idx + 1, num_files)) + + with open(save_data_pkl, 'wb') as fw: + pickle.dump(raw_skes_data, fw, pickle.HIGHEST_PROTOCOL) + np.savetxt(osp.join(save_path, 'raw_data', 'frames_cnt.txt'), + frames_cnt, + fmt='%d') + + print('Saved raw bodies data into %s' % save_data_pkl) + print('Total frames: %d' % np.sum(frames_cnt)) + + with open(frames_drop_pkl, 'wb') as fw: + pickle.dump(frames_drop_skes, fw, pickle.HIGHEST_PROTOCOL) + + +if __name__ == '__main__': + save_path = './' + + skes_path = '../ntu-rgb-d/nturgb+d_skeletons/' + stat_path = osp.join(save_path, 'statistics') + if not osp.exists('./raw_data'): + os.makedirs('./raw_data') + + skes_name_file = osp.join(stat_path, 'skes_available_name.txt') + save_data_pkl = osp.join(save_path, 'raw_data', 'raw_skes_data.pkl') + frames_drop_pkl = osp.join(save_path, 'raw_data', 'frames_drop_skes.pkl') + + frames_drop_logger = logging.getLogger('frames_drop') + frames_drop_logger.setLevel(logging.INFO) + frames_drop_logger.addHandler( + logging.FileHandler(osp.join(save_path, 'raw_data', 'frames_drop.log'))) + frames_drop_skes = dict() + + get_raw_skes_data() + + with open(frames_drop_pkl, 'wb') as fw: + pickle.dump(frames_drop_skes, fw, pickle.HIGHEST_PROTOCOL) diff --git a/data/ntu-rgb-d/seq_transformation.py b/data/ntu-rgb-d/seq_transformation.py new file mode 100644 index 0000000000000000000000000000000000000000..9528452892465988dfffe7f4ff85ff1da8890482 --- /dev/null +++ b/data/ntu-rgb-d/seq_transformation.py @@ -0,0 +1,266 @@ +# ref: https://github.com/Uason-Chen/CTR-GCN/blob/main/data/ntu/seq_transformation.py + +import os +import os.path as osp +import numpy as np +import pickle +import logging +from sklearn.model_selection import train_test_split + +root_path = './' +stat_path = osp.join(root_path, 'statistics') +setup_file = osp.join(stat_path, 'setup.txt') +camera_file = osp.join(stat_path, 'camera.txt') +performer_file = osp.join(stat_path, 'performer.txt') +replication_file = osp.join(stat_path, 'replication.txt') +label_file = osp.join(stat_path, 'label.txt') +skes_name_file = osp.join(stat_path, 'skes_available_name.txt') + +denoised_path = osp.join(root_path, 'denoised_data') +raw_skes_joints_pkl = osp.join(denoised_path, 'raw_denoised_joints.pkl') +frames_file = osp.join(denoised_path, 'frames_cnt.txt') + +save_path = './' + +if not osp.exists(save_path): + os.mkdir(save_path) + + +def remove_nan_frames(ske_name, ske_joints, nan_logger): + num_frames = ske_joints.shape[0] + valid_frames = [] + + for f in range(num_frames): + if not np.any(np.isnan(ske_joints[f])): + valid_frames.append(f) + else: + nan_indices = np.where(np.isnan(ske_joints[f]))[0] + nan_logger.info('{}\t{:^5}\t{}'.format(ske_name, f + 1, + nan_indices)) + + return ske_joints[valid_frames] + + +def seq_translation(skes_joints): + for idx, ske_joints in enumerate(skes_joints): + num_frames = ske_joints.shape[0] + num_bodies = 1 if ske_joints.shape[1] == 75 else 2 + if num_bodies == 2: + missing_frames_1 = np.where(ske_joints[:, :75].sum(axis=1) == 0)[0] + missing_frames_2 = np.where(ske_joints[:, 75:].sum(axis=1) == 0)[0] + cnt1 = len(missing_frames_1) + cnt2 = len(missing_frames_2) + + i = 0 # get the "real" first frame of actor1 + while i < num_frames: + if np.any(ske_joints[i, :75] != 0): + break + i += 1 + + origin = np.copy(ske_joints[i, 3:6]) # new origin: joint-2 + + for f in range(num_frames): + if num_bodies == 1: + ske_joints[f] -= np.tile(origin, 25) + else: # for 2 actors + ske_joints[f] -= np.tile(origin, 50) + + if (num_bodies == 2) and (cnt1 > 0): + ske_joints[missing_frames_1, :75] = np.zeros((cnt1, 75), + dtype=np.float32) + + if (num_bodies == 2) and (cnt2 > 0): + ske_joints[missing_frames_2, 75:] = np.zeros((cnt2, 75), + dtype=np.float32) + + skes_joints[idx] = ske_joints # Update + + return skes_joints + + +def frame_translation(skes_joints, skes_name, frames_cnt): + nan_logger = logging.getLogger('nan_skes') + nan_logger.setLevel(logging.INFO) + nan_logger.addHandler(logging.FileHandler("./nan_frames.log")) + nan_logger.info('{}\t{}\t{}'.format('Skeleton', 'Frame', 'Joints')) + + for idx, ske_joints in enumerate(skes_joints): + num_frames = ske_joints.shape[0] + # Calculate the distance between spine base (joint-1) and spine (joint-21) + j1 = ske_joints[:, 0:3] + j21 = ske_joints[:, 60:63] + dist = np.sqrt(((j1 - j21)**2).sum(axis=1)) + + for f in range(num_frames): + origin = ske_joints[f, 3: + 6] # new origin: middle of the spine (joint-2) + if (ske_joints[f, 75:] == 0).all(): + ske_joints[f, :75] = (ske_joints[f, :75] - np.tile(origin, 25)) / \ + dist[f] + np.tile(origin, 25) + else: + ske_joints[f] = (ske_joints[f] - np.tile(origin, 50)) / \ + dist[f] + np.tile(origin, 50) + + ske_name = skes_name[idx] + ske_joints = remove_nan_frames(ske_name, ske_joints, nan_logger) + frames_cnt[idx] = num_frames # update valid number of frames + skes_joints[idx] = ske_joints + + return skes_joints, frames_cnt + + +def align_frames(skes_joints, frames_cnt): + """ + Align all sequences with the same frame length. + + """ + num_skes = len(skes_joints) + max_num_frames = frames_cnt.max() # 300 + aligned_skes_joints = np.zeros((num_skes, max_num_frames, 150), + dtype=np.float32) + + for idx, ske_joints in enumerate(skes_joints): + num_frames = ske_joints.shape[0] + num_bodies = 1 if ske_joints.shape[1] == 75 else 2 + if num_bodies == 1: + aligned_skes_joints[idx, :num_frames] = np.hstack( + (ske_joints, np.zeros_like(ske_joints))) + else: + aligned_skes_joints[idx, :num_frames] = ske_joints + + return aligned_skes_joints + + +def one_hot_vector(labels): + num_skes = len(labels) + labels_vector = np.zeros((num_skes, 60)) + for idx, l in enumerate(labels): + labels_vector[idx, l] = 1 + + return labels_vector + + +def split_train_val(train_indices, method='sklearn', ratio=0.05): + """ + Get validation set by splitting data randomly from training set with two methods. + In fact, I thought these two methods are equal as they got the same performance. + + """ + if method == 'sklearn': + return train_test_split(train_indices, + test_size=ratio, + random_state=10000) + else: + np.random.seed(10000) + np.random.shuffle(train_indices) + val_num_skes = int(np.ceil(0.05 * len(train_indices))) + val_indices = train_indices[:val_num_skes] + train_indices = train_indices[val_num_skes:] + return train_indices, val_indices + + +def split_dataset(skes_name, skes_joints, label, performer, camera, evaluation, + save_path): + train_indices, test_indices = get_indices(performer, camera, evaluation) + m = 'sklearn' # 'sklearn' or 'numpy' + # Select validation set from training set + # train_indices, val_indices = split_train_val(train_indices, m) + + # Save labels and num_frames for each sequence of each data set + train_labels = label[train_indices] + test_labels = label[test_indices] + + train_x = skes_joints[train_indices] + # train_y = one_hot_vector(train_labels) + test_x = skes_joints[test_indices] + # test_y = one_hot_vector(test_labels) + + evaluation_path = osp.join(save_path, evaluation) + isExists = osp.exists(evaluation_path) + if not isExists: + os.makedirs(evaluation_path) + + train_data_save_path = osp.join(evaluation_path, 'train_data.npy') + train_label_save_path = osp.join(evaluation_path, 'train_label.pkl') + val_data_save_path = osp.join(evaluation_path, 'val_data.npy') + val_label_save_path = osp.join(evaluation_path, 'val_label.pkl') + + # reshape data + N, T, VC = train_x.shape + train_x = np.reshape(train_x, (N, T, 2, 25, 3)) + train_x = np.transpose(train_x, (0, 4, 1, 3, 2)) + + N, T, VC = test_x.shape + test_x = np.reshape(test_x, (N, T, 2, 25, 3)) + test_x = np.transpose(test_x, (0, 4, 1, 3, 2)) + # save train + np.save(train_data_save_path, train_x) + out = [skes_name[train_indices], train_labels] + with open(train_label_save_path, 'wb') as f: + pickle.dump(out, f) + # save test + np.save(val_data_save_path, test_x) + out = [skes_name[test_indices], test_labels] + with open(val_label_save_path, 'wb') as f: + pickle.dump(out, f) + + +def get_indices(performer, camera, evaluation='xsub'): + test_indices = np.empty(0) + train_indices = np.empty(0) + + if evaluation == 'xsub': # Cross Subject (Subject IDs) + train_ids = [ + 1, 2, 4, 5, 8, 9, 13, 14, 15, 16, 17, 18, 19, 25, 27, 28, 31, 34, + 35, 38 + ] + test_ids = [ + 3, 6, 7, 10, 11, 12, 20, 21, 22, 23, 24, 26, 29, 30, 32, 33, 36, 37, + 39, 40 + ] + + # Get indices of test data + for idx in test_ids: + temp = np.where(performer == idx)[0] # 0-based index + test_indices = np.hstack((test_indices, temp)).astype(np.int) + + # Get indices of training data + for train_id in train_ids: + temp = np.where(performer == train_id)[0] # 0-based index + train_indices = np.hstack((train_indices, temp)).astype(np.int) + else: # Cross View (Camera IDs) + train_ids = [2, 3] + test_ids = 1 + # Get indices of test data + temp = np.where(camera == test_ids)[0] # 0-based index + test_indices = np.hstack((test_indices, temp)).astype(np.int) + + # Get indices of training data + for train_id in train_ids: + temp = np.where(camera == train_id)[0] # 0-based index + train_indices = np.hstack((train_indices, temp)).astype(np.int) + + return train_indices, test_indices + + +if __name__ == '__main__': + camera = np.loadtxt(camera_file, dtype=np.int) # camera id: 1, 2, 3 + performer = np.loadtxt(performer_file, dtype=np.int) # subject id: 1~40 + label = np.loadtxt(label_file, dtype=np.int) - 1 # action label: 0~59 + + frames_cnt = np.loadtxt(frames_file, dtype=np.int) # frames_cnt + skes_name = np.loadtxt(skes_name_file, dtype=np.string_) + + with open(raw_skes_joints_pkl, 'rb') as fr: + skes_joints = pickle.load(fr) # a list + + skes_joints = seq_translation(skes_joints) + + skes_joints = align_frames(skes_joints, + frames_cnt) # aligned to the same frame length + + evaluations = ['xview', 'xsub'] + for evaluation in evaluations: + split_dataset(skes_name, skes_joints, label, performer, camera, + evaluation, save_path) + print('Done!') diff --git a/data/ucf101/build_ucf101_file_list.py b/data/ucf101/build_ucf101_file_list.py new file mode 100644 index 0000000000000000000000000000000000000000..ecf281b529977f756d6fe29b8ca17f20fbf4d998 --- /dev/null +++ b/data/ucf101/build_ucf101_file_list.py @@ -0,0 +1,158 @@ +import argparse +import os +import glob +import fnmatch +import random + + +def parse_directory(path, + key_func=lambda x: x[-11:], + rgb_prefix='img_', + level=1): + """ + Parse directories holding extracted frames from standard benchmarks + """ + print('parse frames under folder {}'.format(path)) + if level == 1: + frame_folders = glob.glob(os.path.join(path, '*')) + elif level == 2: + frame_folders = glob.glob(os.path.join(path, '*', '*')) + else: + raise ValueError('level can be only 1 or 2') + + def count_files(directory, prefix_list): + lst = os.listdir(directory) + cnt_list = [len(fnmatch.filter(lst, x + '*')) for x in prefix_list] + return cnt_list + + # check RGB + frame_dict = {} + for i, f in enumerate(frame_folders): + all_cnt = count_files(f, (rgb_prefix)) + k = key_func(f) + + x_cnt = all_cnt[1] + y_cnt = all_cnt[2] + if x_cnt != y_cnt: + raise ValueError('x and y direction have different number ' + 'of flow images. video: ' + f) + if i % 200 == 0: + print('{} videos parsed'.format(i)) + + frame_dict[k] = (f, all_cnt[0], x_cnt) + + print('frame folder analysis done') + return frame_dict + + +def build_split_list(split, frame_info, shuffle=False): + def build_set_list(set_list): + rgb_list = list() + for item in set_list: + if item[0] not in frame_info: + continue + elif frame_info[item[0]][1] > 0: + rgb_cnt = frame_info[item[0]][1] + rgb_list.append('{} {} {}\n'.format(item[0], rgb_cnt, item[1])) + else: + rgb_list.append('{} {}\n'.format(item[0], item[1])) + if shuffle: + random.shuffle(rgb_list) + return rgb_list + + train_rgb_list = build_set_list(split[0]) + test_rgb_list = build_set_list(split[1]) + return (train_rgb_list, test_rgb_list) + + +def parse_ucf101_splits(level): + class_ind = [x.strip().split() for x in open('./annotations/classInd.txt')] + class_mapping = {x[1]: int(x[0]) - 1 for x in class_ind} + + def line2rec(line): + items = line.strip().split(' ') + vid = items[0].split('.')[0] + vid = '/'.join(vid.split('/')[-level:]) + label = class_mapping[items[0].split('/')[0]] + return vid, label + + splits = [] + for i in range(1, 4): + train_list = [ + line2rec(x) + for x in open('./annotations/trainlist{:02d}.txt'.format(i)) + ] + test_list = [ + line2rec(x) + for x in open('./annotations/testlist{:02d}.txt'.format(i)) + ] + splits.append((train_list, test_list)) + return splits + + +def parse_args(): + parser = argparse.ArgumentParser(description='Build file list') + parser.add_argument( + 'frame_path', type=str, help='root directory for the frames') + parser.add_argument('--rgb_prefix', type=str, default='img_') + parser.add_argument('--num_split', type=int, default=3) + parser.add_argument('--level', type=int, default=2, choices=[1, 2]) + parser.add_argument( + '--format', + type=str, + default='rawframes', + choices=['rawframes', 'videos']) + parser.add_argument('--out_list_path', type=str, default='./') + parser.add_argument('--shuffle', action='store_true', default=False) + args = parser.parse_args() + + return args + + +def main(): + args = parse_args() + + if args.level == 2: + + def key_func(x): + return '/'.join(x.split('/')[-2:]) + else: + + def key_func(x): + return x.split('/')[-1] + + if args.format == 'rawframes': + frame_info = parse_directory( + args.frame_path, + key_func=key_func, + rgb_prefix=args.rgb_prefix, + level=args.level) + elif args.format == 'videos': + if args.level == 1: + video_list = glob.glob(os.path.join(args.frame_path, '*')) + elif args.level == 2: + video_list = glob.glob(os.path.join(args.frame_path, '*', '*')) + frame_info = { + os.path.relpath(x.split('.')[0], args.frame_path): (x, -1, -1) + for x in video_list + } + + split_tp = parse_ucf101_splits(args.level) + assert len(split_tp) == args.num_split + + out_path = args.out_list_path + + for i, split in enumerate(split_tp): + lists = build_split_list(split_tp[i], frame_info, shuffle=args.shuffle) + filename = 'ucf101_train_split_{}_{}.txt'.format(i + 1, args.format) + + PATH = os.path.abspath(args.frame_path) + with open(os.path.join(out_path, filename), 'w') as f: + f.writelines([os.path.join(PATH, item) for item in lists[0]]) + filename = 'ucf101_val_split_{}_{}.txt'.format(i + 1, args.format) + with open(os.path.join(out_path, filename), 'w') as f: + f.writelines([os.path.join(PATH, item) for item in lists[1]]) + + +if __name__ == "__main__": + main() diff --git a/data/ucf101/download_annotations.sh b/data/ucf101/download_annotations.sh new file mode 100644 index 0000000000000000000000000000000000000000..2814314865ea813a19aedb5fc241eba5ef8fb01b --- /dev/null +++ b/data/ucf101/download_annotations.sh @@ -0,0 +1,13 @@ +#! /usr/bin/bash env + +DATA_DIR="./annotations" + +if [[ ! -d "${DATA_DIR}" ]]; then + echo "${DATA_DIR} does not exist. Creating"; + mkdir -p ${DATA_DIR} +fi + +wget --no-check-certificate "https://www.crcv.ucf.edu/data/UCF101/UCF101TrainTestSplits-RecognitionTask.zip" + +unzip -j UCF101TrainTestSplits-RecognitionTask.zip -d ${DATA_DIR}/ +rm UCF101TrainTestSplits-RecognitionTask.zip diff --git a/data/ucf101/download_videos.sh b/data/ucf101/download_videos.sh new file mode 100644 index 0000000000000000000000000000000000000000..66289e902861b0d09fafee77309ffedc5de8c316 --- /dev/null +++ b/data/ucf101/download_videos.sh @@ -0,0 +1,7 @@ +#! /usr/bin/bash env + +wget --no-check-certificate "https://www.crcv.ucf.edu/data/UCF101/UCF101.rar" +unrar x UCF101.rar +mv ./UCF-101 ./videos +rm -rf ./UCF101.rar + diff --git a/data/ucf101/extract_rawframes.py b/data/ucf101/extract_rawframes.py new file mode 100644 index 0000000000000000000000000000000000000000..aaf8ae766ff94f224e9a62d2f632e511830147cf --- /dev/null +++ b/data/ucf101/extract_rawframes.py @@ -0,0 +1,98 @@ +import argparse +import sys +import os +import os.path as osp +import glob +from pipes import quote +from multiprocessing import Pool, current_process +import cv2 + + +def dump_frames(vid_item): + full_path, vid_path, vid_id = vid_item + vid_name = vid_path.split('.')[0] + out_full_path = osp.join(args.out_dir, vid_name) + try: + os.mkdir(out_full_path) + except OSError: + pass + vr = cv2.VideoCapture(full_path) + videolen = int(vr.get(cv2.CAP_PROP_FRAME_COUNT)) + for i in range(videolen): + ret, frame = vr.read() + if ret == False: + continue + img = frame[:, :, ::-1] + # covert the BGR img + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + if img is not None: + # cv2.imwrite will write BGR into RGB images + cv2.imwrite('{}/img_{:05d}.jpg'.format(out_full_path, i + 1), img) + else: + print('[Warning] length inconsistent!' + 'Early stop with {} out of {} frames'.format(i + 1, videolen)) + break + print('{} done with {} frames'.format(vid_name, videolen)) + sys.stdout.flush() + return True + + +def parse_args(): + parser = argparse.ArgumentParser(description='extract frames') + parser.add_argument('src_dir', type=str) + parser.add_argument('out_dir', type=str) + parser.add_argument('--level', type=int, choices=[1, 2], default=2) + parser.add_argument('--num_worker', type=int, default=8) + parser.add_argument( + "--ext", + type=str, + default='avi', + choices=['avi', 'mp4'], + help='video file extensions') + + parser.add_argument( + "--resume", + action='store_true', + default=False, + help='resume optical flow extraction ' + 'instead of overwriting') + args = parser.parse_args() + + return args + + +if __name__ == '__main__': + args = parse_args() + if not osp.isdir(args.out_dir): + print('Creating folder: {}'.format(args.out_dir)) + os.makedirs(args.out_dir) + if args.level == 2: + classes = os.listdir(args.src_dir) + for classname in classes: + new_dir = osp.join(args.out_dir, classname) + if not osp.isdir(new_dir): + print('Creating folder: {}'.format(new_dir)) + os.makedirs(new_dir) + + print('Reading videos from folder: ', args.src_dir) + print('Extension of videos: ', args.ext) + if args.level == 2: + fullpath_list = glob.glob(args.src_dir + '/*/*.' + args.ext) + done_fullpath_list = glob.glob(args.out_dir + '/*/*') + elif args.level == 1: + fullpath_list = glob.glob(args.src_dir + '/*.' + args.ext) + done_fullpath_list = glob.glob(args.out_dir + '/*') + print('Total number of videos found: ', len(fullpath_list)) + if args.resume: + fullpath_list = set(fullpath_list).difference(set(done_fullpath_list)) + fullpath_list = list(fullpath_list) + print('Resuming. number of videos to be done: ', len(fullpath_list)) + + if args.level == 2: + vid_list = list( + map(lambda p: osp.join('/'.join(p.split('/')[-2:])), fullpath_list)) + elif args.level == 1: + vid_list = list(map(lambda p: p.split('/')[-1], fullpath_list)) + + pool = Pool(args.num_worker) + pool.map(dump_frames, zip(fullpath_list, vid_list, range(len(vid_list)))) diff --git a/data/yt8m/split_yt8m.py b/data/yt8m/split_yt8m.py new file mode 100644 index 0000000000000000000000000000000000000000..159e0a3c700b54b43bf17e1f37c6482b05e88a3f --- /dev/null +++ b/data/yt8m/split_yt8m.py @@ -0,0 +1,45 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import sys +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle + from io import BytesIO + +assert len(sys.argv) == 2, \ + f"split_yt8m.py need (train/val).list" + +file_list = sys.argv[1] + +def split_yt8m(filelist): + print(__file__, sys._getframe().f_lineno, "filelist:",filelist) + fl = open(filelist).readlines() + fl = [line.strip() for line in fl if line.strip() != ''] + for filepath in fl: + data = pickle.load(open(filepath, 'rb'), encoding='bytes') + save_pre = filepath[:-4] #erase ".pkl" + indexes = list(range(len(data))) + print("filepath:",filepath, "len(data):", len(data), "save_pre:", save_pre) + for i in indexes: + save_path = save_pre + "_split_" + str(i) +".pkl" + record = data[i] + output = open(save_path, 'wb') + pickle.dump(data[i], output, protocol=2) + output.close() + +if __name__ == "__main__": + split_yt8m(file_list) diff --git a/data/yt8m/tf2pkl.py b/data/yt8m/tf2pkl.py new file mode 100644 index 0000000000000000000000000000000000000000..7f79bbb3239dc3ffe5f01b96c5c433d29d871a07 --- /dev/null +++ b/data/yt8m/tf2pkl.py @@ -0,0 +1,284 @@ +# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserve. +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. +"""Provides readers configured for different datasets.""" +import os, sys +import numpy as np +import tensorflow as tf +from tensorflow import logging +try: + import cPickle as pickle +except: + import pickle + +from tensorflow.python.platform import gfile + +assert (len(sys.argv) == 3) +source_dir = sys.argv[1] +target_dir = sys.argv[2] + + +def Dequantize(feat_vector, max_quantized_value=2, min_quantized_value=-2): + """Dequantize the feature from the byte format to the float format. + + Args: + feat_vector: the input 1-d vector. + max_quantized_value: the maximum of the quantized value. + min_quantized_value: the minimum of the quantized value. + + Returns: + A float vector which has the same shape as feat_vector. + """ + assert max_quantized_value > min_quantized_value + quantized_range = max_quantized_value - min_quantized_value + scalar = quantized_range / 255.0 + bias = (quantized_range / 512.0) + min_quantized_value + return feat_vector * scalar + bias + + +def resize_axis(tensor, axis, new_size, fill_value=0): + """Truncates or pads a tensor to new_size on on a given axis. + + Truncate or extend tensor such that tensor.shape[axis] == new_size. If the + size increases, the padding will be performed at the end, using fill_value. + + Args: + tensor: The tensor to be resized. + axis: An integer representing the dimension to be sliced. + new_size: An integer or 0d tensor representing the new value for + tensor.shape[axis]. + fill_value: Value to use to fill any new entries in the tensor. Will be + cast to the type of tensor. + + Returns: + The resized tensor. + """ + tensor = tf.convert_to_tensor(tensor) + shape = tf.unstack(tf.shape(tensor)) + + pad_shape = shape[:] + pad_shape[axis] = tf.maximum(0, new_size - shape[axis]) + + shape[axis] = tf.minimum(shape[axis], new_size) + shape = tf.stack(shape) + + resized = tf.concat([ + tf.slice(tensor, tf.zeros_like(shape), shape), + tf.fill(tf.stack(pad_shape), tf.cast(fill_value, tensor.dtype)) + ], axis) + + # Update shape. + new_shape = tensor.get_shape().as_list() # A copy is being made. + new_shape[axis] = new_size + resized.set_shape(new_shape) + return resized + + +class BaseReader(object): + """Inherit from this class when implementing new readers.""" + + def prepare_reader(self, unused_filename_queue): + """Create a thread for generating prediction and label tensors.""" + raise NotImplementedError() + + +class YT8MFrameFeatureReader(BaseReader): + """Reads TFRecords of SequenceExamples. + + The TFRecords must contain SequenceExamples with the sparse in64 'labels' + context feature and a fixed length byte-quantized feature vector, obtained + from the features in 'feature_names'. The quantized features will be mapped + back into a range between min_quantized_value and max_quantized_value. + """ + + def __init__(self, + num_classes=3862, + feature_sizes=[1024], + feature_names=["inc3"], + max_frames=300): + """Construct a YT8MFrameFeatureReader. + + Args: + num_classes: a positive integer for the number of classes. + feature_sizes: positive integer(s) for the feature dimensions as a list. + feature_names: the feature name(s) in the tensorflow record as a list. + max_frames: the maximum number of frames to process. + """ + + assert len(feature_names) == len(feature_sizes), \ + "length of feature_names (={}) != length of feature_sizes (={})".format( \ + len(feature_names), len(feature_sizes)) + + self.num_classes = num_classes + self.feature_sizes = feature_sizes + self.feature_names = feature_names + self.max_frames = max_frames + + def get_video_matrix(self, features, feature_size, max_frames, + max_quantized_value, min_quantized_value): + """Decodes features from an input string and quantizes it. + + Args: + features: raw feature values + feature_size: length of each frame feature vector + max_frames: number of frames (rows) in the output feature_matrix + max_quantized_value: the maximum of the quantized value. + min_quantized_value: the minimum of the quantized value. + + Returns: + feature_matrix: matrix of all frame-features + num_frames: number of frames in the sequence + """ + decoded_features = tf.reshape( + tf.cast(tf.decode_raw(features, tf.uint8), tf.float32), + [-1, feature_size]) + + num_frames = tf.minimum(tf.shape(decoded_features)[0], max_frames) + + feature_matrix = decoded_features + + return feature_matrix, num_frames + + def prepare_reader(self, + filename_queue, + max_quantized_value=2, + min_quantized_value=-2): + """Creates a single reader thread for YouTube8M SequenceExamples. + + Args: + filename_queue: A tensorflow queue of filename locations. + max_quantized_value: the maximum of the quantized value. + min_quantized_value: the minimum of the quantized value. + + Returns: + A tuple of video indexes, video features, labels, and padding data. + """ + reader = tf.TFRecordReader() + _, serialized_example = reader.read(filename_queue) + + contexts, features = tf.parse_single_sequence_example( + serialized_example, + context_features={ + "id": tf.FixedLenFeature([], tf.string), + "labels": tf.VarLenFeature(tf.int64) + }, + sequence_features={ + feature_name: tf.FixedLenSequenceFeature( + [], dtype=tf.string) + for feature_name in self.feature_names + }) + + # read ground truth labels + labels = (tf.cast( + tf.sparse_to_dense( + contexts["labels"].values, (self.num_classes, ), + 1, + validate_indices=False), + tf.bool)) + + # loads (potentially) different types of features and concatenates them + num_features = len(self.feature_names) + assert num_features > 0, "No feature selected: feature_names is empty!" + + assert len(self.feature_names) == len(self.feature_sizes), \ + "length of feature_names (={}) != length of feature_sizes (={})".format( \ + len(self.feature_names), len(self.feature_sizes)) + + num_frames = -1 # the number of frames in the video + feature_matrices = [None + ] * num_features # an array of different features + + for feature_index in range(num_features): + feature_matrix, num_frames_in_this_feature = self.get_video_matrix( + features[self.feature_names[feature_index]], + self.feature_sizes[feature_index], self.max_frames, + max_quantized_value, min_quantized_value) + if num_frames == -1: + num_frames = num_frames_in_this_feature + #else: + # tf.assert_equal(num_frames, num_frames_in_this_feature) + + feature_matrices[feature_index] = feature_matrix + + # cap the number of frames at self.max_frames + num_frames = tf.minimum(num_frames, self.max_frames) + + # concatenate different features + video_matrix = feature_matrices[0] + audio_matrix = feature_matrices[1] + + return contexts["id"], video_matrix, audio_matrix, labels, num_frames + + +def main(files_pattern): + data_files = gfile.Glob(files_pattern) + filename_queue = tf.train.string_input_producer( + data_files, num_epochs=1, shuffle=False) + + reader = YT8MFrameFeatureReader( + feature_sizes=[1024, 128], feature_names=["rgb", "audio"]) + vals = reader.prepare_reader(filename_queue) + + with tf.Session() as sess: + sess.run(tf.initialize_local_variables()) + sess.run(tf.initialize_all_variables()) + coord = tf.train.Coordinator() + threads = tf.train.start_queue_runners(sess=sess, coord=coord) + + vid_num = 0 + all_data = [] + try: + while not coord.should_stop(): + vid, features, audios, labels, nframes = sess.run(vals) + label_index = np.where(labels == True)[0].tolist() + vid_num += 1 + + #print vid, features.shape, audios.shape, label_index, nframes + + features_int = features.astype(np.uint8) + audios_int = audios.astype(np.uint8) + + value_dict = {} + value_dict['video'] = vid + value_dict['feature'] = features_int + value_dict['audio'] = audios_int + value_dict['label'] = label_index + value_dict['nframes'] = nframes + all_data.append(value_dict) + + except tf.errors.OutOfRangeError: + print('Finished extracting.') + + finally: + coord.request_stop() + coord.join(threads) + + print(vid_num) + + record_name = files_pattern.split('/')[-1].split('.')[0] + outputdir = target_dir + if not os.path.exists(outputdir): + os.mkdir(outputdir) + fn = '%s.pkl' % record_name + outp = open(os.path.join(outputdir, fn), 'wb') + pickle.dump(all_data, outp, protocol=2) + outp.close() + + +if __name__ == '__main__': + record_dir = source_dir + record_files = os.listdir(record_dir) + record_files = [file for file in record_files if file.endswith(".tfrecord")] + for f in record_files: + record_path = os.path.join(record_dir, f) + main(record_path) diff --git a/data/yt8m/train_small.list b/data/yt8m/train_small.list new file mode 100644 index 0000000000000000000000000000000000000000..192ccb8cf5f250caf8d4ed3518ee1aadaba167ef --- /dev/null +++ b/data/yt8m/train_small.list @@ -0,0 +1,1022 @@ +pkl_frame/train0255_split_0.pkl +pkl_frame/train0255_split_1000.pkl +pkl_frame/train0255_split_1001.pkl +pkl_frame/train0255_split_1002.pkl +pkl_frame/train0255_split_1003.pkl +pkl_frame/train0255_split_1004.pkl +pkl_frame/train0255_split_1005.pkl +pkl_frame/train0255_split_1006.pkl +pkl_frame/train0255_split_1007.pkl +pkl_frame/train0255_split_1008.pkl +pkl_frame/train0255_split_1009.pkl +pkl_frame/train0255_split_100.pkl +pkl_frame/train0255_split_1010.pkl +pkl_frame/train0255_split_1011.pkl +pkl_frame/train0255_split_1012.pkl +pkl_frame/train0255_split_1013.pkl +pkl_frame/train0255_split_1014.pkl +pkl_frame/train0255_split_1015.pkl +pkl_frame/train0255_split_1016.pkl +pkl_frame/train0255_split_1017.pkl +pkl_frame/train0255_split_1018.pkl +pkl_frame/train0255_split_1019.pkl +pkl_frame/train0255_split_101.pkl +pkl_frame/train0255_split_1020.pkl +pkl_frame/train0255_split_1021.pkl +pkl_frame/train0255_split_102.pkl +pkl_frame/train0255_split_103.pkl +pkl_frame/train0255_split_104.pkl +pkl_frame/train0255_split_105.pkl +pkl_frame/train0255_split_106.pkl +pkl_frame/train0255_split_107.pkl +pkl_frame/train0255_split_108.pkl +pkl_frame/train0255_split_109.pkl +pkl_frame/train0255_split_10.pkl +pkl_frame/train0255_split_110.pkl +pkl_frame/train0255_split_111.pkl +pkl_frame/train0255_split_112.pkl +pkl_frame/train0255_split_113.pkl +pkl_frame/train0255_split_114.pkl +pkl_frame/train0255_split_115.pkl +pkl_frame/train0255_split_116.pkl +pkl_frame/train0255_split_117.pkl +pkl_frame/train0255_split_118.pkl +pkl_frame/train0255_split_119.pkl +pkl_frame/train0255_split_11.pkl +pkl_frame/train0255_split_120.pkl +pkl_frame/train0255_split_121.pkl +pkl_frame/train0255_split_122.pkl +pkl_frame/train0255_split_123.pkl +pkl_frame/train0255_split_124.pkl +pkl_frame/train0255_split_125.pkl +pkl_frame/train0255_split_126.pkl +pkl_frame/train0255_split_127.pkl +pkl_frame/train0255_split_128.pkl +pkl_frame/train0255_split_129.pkl +pkl_frame/train0255_split_12.pkl +pkl_frame/train0255_split_130.pkl +pkl_frame/train0255_split_131.pkl +pkl_frame/train0255_split_132.pkl +pkl_frame/train0255_split_133.pkl +pkl_frame/train0255_split_134.pkl +pkl_frame/train0255_split_135.pkl +pkl_frame/train0255_split_136.pkl +pkl_frame/train0255_split_137.pkl +pkl_frame/train0255_split_138.pkl +pkl_frame/train0255_split_139.pkl +pkl_frame/train0255_split_13.pkl +pkl_frame/train0255_split_140.pkl +pkl_frame/train0255_split_141.pkl +pkl_frame/train0255_split_142.pkl +pkl_frame/train0255_split_143.pkl +pkl_frame/train0255_split_144.pkl +pkl_frame/train0255_split_145.pkl +pkl_frame/train0255_split_146.pkl +pkl_frame/train0255_split_147.pkl +pkl_frame/train0255_split_148.pkl +pkl_frame/train0255_split_149.pkl +pkl_frame/train0255_split_14.pkl +pkl_frame/train0255_split_150.pkl +pkl_frame/train0255_split_151.pkl +pkl_frame/train0255_split_152.pkl +pkl_frame/train0255_split_153.pkl +pkl_frame/train0255_split_154.pkl +pkl_frame/train0255_split_155.pkl +pkl_frame/train0255_split_156.pkl +pkl_frame/train0255_split_157.pkl +pkl_frame/train0255_split_158.pkl +pkl_frame/train0255_split_159.pkl +pkl_frame/train0255_split_15.pkl +pkl_frame/train0255_split_160.pkl +pkl_frame/train0255_split_161.pkl +pkl_frame/train0255_split_162.pkl +pkl_frame/train0255_split_163.pkl +pkl_frame/train0255_split_164.pkl +pkl_frame/train0255_split_165.pkl +pkl_frame/train0255_split_166.pkl +pkl_frame/train0255_split_167.pkl +pkl_frame/train0255_split_168.pkl +pkl_frame/train0255_split_169.pkl +pkl_frame/train0255_split_16.pkl +pkl_frame/train0255_split_170.pkl +pkl_frame/train0255_split_171.pkl +pkl_frame/train0255_split_172.pkl +pkl_frame/train0255_split_173.pkl +pkl_frame/train0255_split_174.pkl +pkl_frame/train0255_split_175.pkl +pkl_frame/train0255_split_176.pkl +pkl_frame/train0255_split_177.pkl +pkl_frame/train0255_split_178.pkl +pkl_frame/train0255_split_179.pkl +pkl_frame/train0255_split_17.pkl +pkl_frame/train0255_split_180.pkl +pkl_frame/train0255_split_181.pkl +pkl_frame/train0255_split_182.pkl +pkl_frame/train0255_split_183.pkl +pkl_frame/train0255_split_184.pkl +pkl_frame/train0255_split_185.pkl +pkl_frame/train0255_split_186.pkl +pkl_frame/train0255_split_187.pkl +pkl_frame/train0255_split_188.pkl +pkl_frame/train0255_split_189.pkl +pkl_frame/train0255_split_18.pkl +pkl_frame/train0255_split_190.pkl +pkl_frame/train0255_split_191.pkl +pkl_frame/train0255_split_192.pkl +pkl_frame/train0255_split_193.pkl +pkl_frame/train0255_split_194.pkl +pkl_frame/train0255_split_195.pkl +pkl_frame/train0255_split_196.pkl +pkl_frame/train0255_split_197.pkl +pkl_frame/train0255_split_198.pkl +pkl_frame/train0255_split_199.pkl +pkl_frame/train0255_split_19.pkl +pkl_frame/train0255_split_1.pkl +pkl_frame/train0255_split_200.pkl +pkl_frame/train0255_split_201.pkl +pkl_frame/train0255_split_202.pkl +pkl_frame/train0255_split_203.pkl +pkl_frame/train0255_split_204.pkl +pkl_frame/train0255_split_205.pkl +pkl_frame/train0255_split_206.pkl +pkl_frame/train0255_split_207.pkl +pkl_frame/train0255_split_208.pkl +pkl_frame/train0255_split_209.pkl +pkl_frame/train0255_split_20.pkl +pkl_frame/train0255_split_210.pkl +pkl_frame/train0255_split_211.pkl +pkl_frame/train0255_split_212.pkl +pkl_frame/train0255_split_213.pkl +pkl_frame/train0255_split_214.pkl +pkl_frame/train0255_split_215.pkl +pkl_frame/train0255_split_216.pkl +pkl_frame/train0255_split_217.pkl +pkl_frame/train0255_split_218.pkl +pkl_frame/train0255_split_219.pkl +pkl_frame/train0255_split_21.pkl +pkl_frame/train0255_split_220.pkl +pkl_frame/train0255_split_221.pkl +pkl_frame/train0255_split_222.pkl +pkl_frame/train0255_split_223.pkl +pkl_frame/train0255_split_224.pkl +pkl_frame/train0255_split_225.pkl +pkl_frame/train0255_split_226.pkl +pkl_frame/train0255_split_227.pkl +pkl_frame/train0255_split_228.pkl +pkl_frame/train0255_split_229.pkl +pkl_frame/train0255_split_22.pkl +pkl_frame/train0255_split_230.pkl +pkl_frame/train0255_split_231.pkl +pkl_frame/train0255_split_232.pkl +pkl_frame/train0255_split_233.pkl +pkl_frame/train0255_split_234.pkl +pkl_frame/train0255_split_235.pkl +pkl_frame/train0255_split_236.pkl +pkl_frame/train0255_split_237.pkl +pkl_frame/train0255_split_238.pkl +pkl_frame/train0255_split_239.pkl +pkl_frame/train0255_split_23.pkl +pkl_frame/train0255_split_240.pkl +pkl_frame/train0255_split_241.pkl +pkl_frame/train0255_split_242.pkl +pkl_frame/train0255_split_243.pkl +pkl_frame/train0255_split_244.pkl +pkl_frame/train0255_split_245.pkl +pkl_frame/train0255_split_246.pkl +pkl_frame/train0255_split_247.pkl +pkl_frame/train0255_split_248.pkl +pkl_frame/train0255_split_249.pkl +pkl_frame/train0255_split_24.pkl +pkl_frame/train0255_split_250.pkl +pkl_frame/train0255_split_251.pkl +pkl_frame/train0255_split_252.pkl +pkl_frame/train0255_split_253.pkl +pkl_frame/train0255_split_254.pkl +pkl_frame/train0255_split_255.pkl +pkl_frame/train0255_split_256.pkl +pkl_frame/train0255_split_257.pkl +pkl_frame/train0255_split_258.pkl +pkl_frame/train0255_split_259.pkl +pkl_frame/train0255_split_25.pkl +pkl_frame/train0255_split_260.pkl +pkl_frame/train0255_split_261.pkl +pkl_frame/train0255_split_262.pkl +pkl_frame/train0255_split_263.pkl +pkl_frame/train0255_split_264.pkl +pkl_frame/train0255_split_265.pkl +pkl_frame/train0255_split_266.pkl +pkl_frame/train0255_split_267.pkl +pkl_frame/train0255_split_268.pkl +pkl_frame/train0255_split_269.pkl +pkl_frame/train0255_split_26.pkl +pkl_frame/train0255_split_270.pkl +pkl_frame/train0255_split_271.pkl +pkl_frame/train0255_split_272.pkl +pkl_frame/train0255_split_273.pkl +pkl_frame/train0255_split_274.pkl +pkl_frame/train0255_split_275.pkl +pkl_frame/train0255_split_276.pkl +pkl_frame/train0255_split_277.pkl +pkl_frame/train0255_split_278.pkl +pkl_frame/train0255_split_279.pkl +pkl_frame/train0255_split_27.pkl +pkl_frame/train0255_split_280.pkl +pkl_frame/train0255_split_281.pkl +pkl_frame/train0255_split_282.pkl +pkl_frame/train0255_split_283.pkl +pkl_frame/train0255_split_284.pkl +pkl_frame/train0255_split_285.pkl +pkl_frame/train0255_split_286.pkl +pkl_frame/train0255_split_287.pkl +pkl_frame/train0255_split_288.pkl +pkl_frame/train0255_split_289.pkl +pkl_frame/train0255_split_28.pkl +pkl_frame/train0255_split_290.pkl +pkl_frame/train0255_split_291.pkl +pkl_frame/train0255_split_292.pkl +pkl_frame/train0255_split_293.pkl +pkl_frame/train0255_split_294.pkl +pkl_frame/train0255_split_295.pkl +pkl_frame/train0255_split_296.pkl +pkl_frame/train0255_split_297.pkl +pkl_frame/train0255_split_298.pkl +pkl_frame/train0255_split_299.pkl +pkl_frame/train0255_split_29.pkl +pkl_frame/train0255_split_2.pkl +pkl_frame/train0255_split_300.pkl +pkl_frame/train0255_split_301.pkl +pkl_frame/train0255_split_302.pkl +pkl_frame/train0255_split_303.pkl +pkl_frame/train0255_split_304.pkl +pkl_frame/train0255_split_305.pkl +pkl_frame/train0255_split_306.pkl +pkl_frame/train0255_split_307.pkl +pkl_frame/train0255_split_308.pkl +pkl_frame/train0255_split_309.pkl +pkl_frame/train0255_split_30.pkl +pkl_frame/train0255_split_310.pkl +pkl_frame/train0255_split_311.pkl +pkl_frame/train0255_split_312.pkl +pkl_frame/train0255_split_313.pkl +pkl_frame/train0255_split_314.pkl +pkl_frame/train0255_split_315.pkl +pkl_frame/train0255_split_316.pkl +pkl_frame/train0255_split_317.pkl +pkl_frame/train0255_split_318.pkl +pkl_frame/train0255_split_319.pkl +pkl_frame/train0255_split_31.pkl +pkl_frame/train0255_split_320.pkl +pkl_frame/train0255_split_321.pkl +pkl_frame/train0255_split_322.pkl +pkl_frame/train0255_split_323.pkl +pkl_frame/train0255_split_324.pkl +pkl_frame/train0255_split_325.pkl +pkl_frame/train0255_split_326.pkl +pkl_frame/train0255_split_327.pkl +pkl_frame/train0255_split_328.pkl +pkl_frame/train0255_split_329.pkl +pkl_frame/train0255_split_32.pkl +pkl_frame/train0255_split_330.pkl +pkl_frame/train0255_split_331.pkl +pkl_frame/train0255_split_332.pkl +pkl_frame/train0255_split_333.pkl +pkl_frame/train0255_split_334.pkl +pkl_frame/train0255_split_335.pkl +pkl_frame/train0255_split_336.pkl +pkl_frame/train0255_split_337.pkl +pkl_frame/train0255_split_338.pkl +pkl_frame/train0255_split_339.pkl +pkl_frame/train0255_split_33.pkl +pkl_frame/train0255_split_340.pkl +pkl_frame/train0255_split_341.pkl +pkl_frame/train0255_split_342.pkl +pkl_frame/train0255_split_343.pkl +pkl_frame/train0255_split_344.pkl +pkl_frame/train0255_split_345.pkl +pkl_frame/train0255_split_346.pkl +pkl_frame/train0255_split_347.pkl +pkl_frame/train0255_split_348.pkl +pkl_frame/train0255_split_349.pkl +pkl_frame/train0255_split_34.pkl +pkl_frame/train0255_split_350.pkl +pkl_frame/train0255_split_351.pkl +pkl_frame/train0255_split_352.pkl +pkl_frame/train0255_split_353.pkl +pkl_frame/train0255_split_354.pkl +pkl_frame/train0255_split_355.pkl +pkl_frame/train0255_split_356.pkl +pkl_frame/train0255_split_357.pkl +pkl_frame/train0255_split_358.pkl +pkl_frame/train0255_split_359.pkl +pkl_frame/train0255_split_35.pkl +pkl_frame/train0255_split_360.pkl +pkl_frame/train0255_split_361.pkl +pkl_frame/train0255_split_362.pkl +pkl_frame/train0255_split_363.pkl +pkl_frame/train0255_split_364.pkl +pkl_frame/train0255_split_365.pkl +pkl_frame/train0255_split_366.pkl +pkl_frame/train0255_split_367.pkl +pkl_frame/train0255_split_368.pkl +pkl_frame/train0255_split_369.pkl +pkl_frame/train0255_split_36.pkl +pkl_frame/train0255_split_370.pkl +pkl_frame/train0255_split_371.pkl +pkl_frame/train0255_split_372.pkl +pkl_frame/train0255_split_373.pkl +pkl_frame/train0255_split_374.pkl +pkl_frame/train0255_split_375.pkl +pkl_frame/train0255_split_376.pkl +pkl_frame/train0255_split_377.pkl +pkl_frame/train0255_split_378.pkl +pkl_frame/train0255_split_379.pkl +pkl_frame/train0255_split_37.pkl +pkl_frame/train0255_split_380.pkl +pkl_frame/train0255_split_381.pkl +pkl_frame/train0255_split_382.pkl +pkl_frame/train0255_split_383.pkl +pkl_frame/train0255_split_384.pkl +pkl_frame/train0255_split_385.pkl +pkl_frame/train0255_split_386.pkl +pkl_frame/train0255_split_387.pkl +pkl_frame/train0255_split_388.pkl +pkl_frame/train0255_split_389.pkl +pkl_frame/train0255_split_38.pkl +pkl_frame/train0255_split_390.pkl +pkl_frame/train0255_split_391.pkl +pkl_frame/train0255_split_392.pkl +pkl_frame/train0255_split_393.pkl +pkl_frame/train0255_split_394.pkl +pkl_frame/train0255_split_395.pkl +pkl_frame/train0255_split_396.pkl +pkl_frame/train0255_split_397.pkl +pkl_frame/train0255_split_398.pkl +pkl_frame/train0255_split_399.pkl +pkl_frame/train0255_split_39.pkl +pkl_frame/train0255_split_3.pkl +pkl_frame/train0255_split_400.pkl +pkl_frame/train0255_split_401.pkl +pkl_frame/train0255_split_402.pkl +pkl_frame/train0255_split_403.pkl +pkl_frame/train0255_split_404.pkl +pkl_frame/train0255_split_405.pkl +pkl_frame/train0255_split_406.pkl +pkl_frame/train0255_split_407.pkl +pkl_frame/train0255_split_408.pkl +pkl_frame/train0255_split_409.pkl +pkl_frame/train0255_split_40.pkl +pkl_frame/train0255_split_410.pkl +pkl_frame/train0255_split_411.pkl +pkl_frame/train0255_split_412.pkl +pkl_frame/train0255_split_413.pkl +pkl_frame/train0255_split_414.pkl +pkl_frame/train0255_split_415.pkl +pkl_frame/train0255_split_416.pkl +pkl_frame/train0255_split_417.pkl +pkl_frame/train0255_split_418.pkl +pkl_frame/train0255_split_419.pkl +pkl_frame/train0255_split_41.pkl +pkl_frame/train0255_split_420.pkl +pkl_frame/train0255_split_421.pkl +pkl_frame/train0255_split_422.pkl +pkl_frame/train0255_split_423.pkl +pkl_frame/train0255_split_424.pkl +pkl_frame/train0255_split_425.pkl +pkl_frame/train0255_split_426.pkl +pkl_frame/train0255_split_427.pkl +pkl_frame/train0255_split_428.pkl +pkl_frame/train0255_split_429.pkl +pkl_frame/train0255_split_42.pkl +pkl_frame/train0255_split_430.pkl +pkl_frame/train0255_split_431.pkl +pkl_frame/train0255_split_432.pkl +pkl_frame/train0255_split_433.pkl +pkl_frame/train0255_split_434.pkl +pkl_frame/train0255_split_435.pkl +pkl_frame/train0255_split_436.pkl +pkl_frame/train0255_split_437.pkl +pkl_frame/train0255_split_438.pkl +pkl_frame/train0255_split_439.pkl +pkl_frame/train0255_split_43.pkl +pkl_frame/train0255_split_440.pkl +pkl_frame/train0255_split_441.pkl +pkl_frame/train0255_split_442.pkl +pkl_frame/train0255_split_443.pkl +pkl_frame/train0255_split_444.pkl +pkl_frame/train0255_split_445.pkl +pkl_frame/train0255_split_446.pkl +pkl_frame/train0255_split_447.pkl +pkl_frame/train0255_split_448.pkl +pkl_frame/train0255_split_449.pkl +pkl_frame/train0255_split_44.pkl +pkl_frame/train0255_split_450.pkl +pkl_frame/train0255_split_451.pkl +pkl_frame/train0255_split_452.pkl +pkl_frame/train0255_split_453.pkl +pkl_frame/train0255_split_454.pkl +pkl_frame/train0255_split_455.pkl +pkl_frame/train0255_split_456.pkl +pkl_frame/train0255_split_457.pkl +pkl_frame/train0255_split_458.pkl +pkl_frame/train0255_split_459.pkl +pkl_frame/train0255_split_45.pkl +pkl_frame/train0255_split_460.pkl +pkl_frame/train0255_split_461.pkl +pkl_frame/train0255_split_462.pkl +pkl_frame/train0255_split_463.pkl +pkl_frame/train0255_split_464.pkl +pkl_frame/train0255_split_465.pkl +pkl_frame/train0255_split_466.pkl +pkl_frame/train0255_split_467.pkl +pkl_frame/train0255_split_468.pkl +pkl_frame/train0255_split_469.pkl +pkl_frame/train0255_split_46.pkl +pkl_frame/train0255_split_470.pkl +pkl_frame/train0255_split_471.pkl +pkl_frame/train0255_split_472.pkl +pkl_frame/train0255_split_473.pkl +pkl_frame/train0255_split_474.pkl +pkl_frame/train0255_split_475.pkl +pkl_frame/train0255_split_476.pkl +pkl_frame/train0255_split_477.pkl +pkl_frame/train0255_split_478.pkl +pkl_frame/train0255_split_479.pkl +pkl_frame/train0255_split_47.pkl +pkl_frame/train0255_split_480.pkl +pkl_frame/train0255_split_481.pkl +pkl_frame/train0255_split_482.pkl +pkl_frame/train0255_split_483.pkl +pkl_frame/train0255_split_484.pkl +pkl_frame/train0255_split_485.pkl +pkl_frame/train0255_split_486.pkl +pkl_frame/train0255_split_487.pkl +pkl_frame/train0255_split_488.pkl +pkl_frame/train0255_split_489.pkl +pkl_frame/train0255_split_48.pkl +pkl_frame/train0255_split_490.pkl +pkl_frame/train0255_split_491.pkl +pkl_frame/train0255_split_492.pkl +pkl_frame/train0255_split_493.pkl +pkl_frame/train0255_split_494.pkl +pkl_frame/train0255_split_495.pkl +pkl_frame/train0255_split_496.pkl +pkl_frame/train0255_split_497.pkl +pkl_frame/train0255_split_498.pkl +pkl_frame/train0255_split_499.pkl +pkl_frame/train0255_split_49.pkl +pkl_frame/train0255_split_4.pkl +pkl_frame/train0255_split_500.pkl +pkl_frame/train0255_split_501.pkl +pkl_frame/train0255_split_502.pkl +pkl_frame/train0255_split_503.pkl +pkl_frame/train0255_split_504.pkl +pkl_frame/train0255_split_505.pkl +pkl_frame/train0255_split_506.pkl +pkl_frame/train0255_split_507.pkl +pkl_frame/train0255_split_508.pkl +pkl_frame/train0255_split_509.pkl +pkl_frame/train0255_split_50.pkl +pkl_frame/train0255_split_510.pkl +pkl_frame/train0255_split_511.pkl +pkl_frame/train0255_split_512.pkl +pkl_frame/train0255_split_513.pkl +pkl_frame/train0255_split_514.pkl +pkl_frame/train0255_split_515.pkl +pkl_frame/train0255_split_516.pkl +pkl_frame/train0255_split_517.pkl +pkl_frame/train0255_split_518.pkl +pkl_frame/train0255_split_519.pkl +pkl_frame/train0255_split_51.pkl +pkl_frame/train0255_split_520.pkl +pkl_frame/train0255_split_521.pkl +pkl_frame/train0255_split_522.pkl +pkl_frame/train0255_split_523.pkl +pkl_frame/train0255_split_524.pkl +pkl_frame/train0255_split_525.pkl +pkl_frame/train0255_split_526.pkl +pkl_frame/train0255_split_527.pkl +pkl_frame/train0255_split_528.pkl +pkl_frame/train0255_split_529.pkl +pkl_frame/train0255_split_52.pkl +pkl_frame/train0255_split_530.pkl +pkl_frame/train0255_split_531.pkl +pkl_frame/train0255_split_532.pkl +pkl_frame/train0255_split_533.pkl +pkl_frame/train0255_split_534.pkl +pkl_frame/train0255_split_535.pkl +pkl_frame/train0255_split_536.pkl +pkl_frame/train0255_split_537.pkl +pkl_frame/train0255_split_538.pkl +pkl_frame/train0255_split_539.pkl +pkl_frame/train0255_split_53.pkl +pkl_frame/train0255_split_540.pkl +pkl_frame/train0255_split_541.pkl +pkl_frame/train0255_split_542.pkl +pkl_frame/train0255_split_543.pkl +pkl_frame/train0255_split_544.pkl +pkl_frame/train0255_split_545.pkl +pkl_frame/train0255_split_546.pkl +pkl_frame/train0255_split_547.pkl +pkl_frame/train0255_split_548.pkl +pkl_frame/train0255_split_549.pkl +pkl_frame/train0255_split_54.pkl +pkl_frame/train0255_split_550.pkl +pkl_frame/train0255_split_551.pkl +pkl_frame/train0255_split_552.pkl +pkl_frame/train0255_split_553.pkl +pkl_frame/train0255_split_554.pkl +pkl_frame/train0255_split_555.pkl +pkl_frame/train0255_split_556.pkl +pkl_frame/train0255_split_557.pkl +pkl_frame/train0255_split_558.pkl +pkl_frame/train0255_split_559.pkl +pkl_frame/train0255_split_55.pkl +pkl_frame/train0255_split_560.pkl +pkl_frame/train0255_split_561.pkl +pkl_frame/train0255_split_562.pkl +pkl_frame/train0255_split_563.pkl +pkl_frame/train0255_split_564.pkl +pkl_frame/train0255_split_565.pkl +pkl_frame/train0255_split_566.pkl +pkl_frame/train0255_split_567.pkl +pkl_frame/train0255_split_568.pkl +pkl_frame/train0255_split_569.pkl +pkl_frame/train0255_split_56.pkl +pkl_frame/train0255_split_570.pkl +pkl_frame/train0255_split_571.pkl +pkl_frame/train0255_split_572.pkl +pkl_frame/train0255_split_573.pkl +pkl_frame/train0255_split_574.pkl +pkl_frame/train0255_split_575.pkl +pkl_frame/train0255_split_576.pkl +pkl_frame/train0255_split_577.pkl +pkl_frame/train0255_split_578.pkl +pkl_frame/train0255_split_579.pkl +pkl_frame/train0255_split_57.pkl +pkl_frame/train0255_split_580.pkl +pkl_frame/train0255_split_581.pkl +pkl_frame/train0255_split_582.pkl +pkl_frame/train0255_split_583.pkl +pkl_frame/train0255_split_584.pkl +pkl_frame/train0255_split_585.pkl +pkl_frame/train0255_split_586.pkl +pkl_frame/train0255_split_587.pkl +pkl_frame/train0255_split_588.pkl +pkl_frame/train0255_split_589.pkl +pkl_frame/train0255_split_58.pkl +pkl_frame/train0255_split_590.pkl +pkl_frame/train0255_split_591.pkl +pkl_frame/train0255_split_592.pkl +pkl_frame/train0255_split_593.pkl +pkl_frame/train0255_split_594.pkl +pkl_frame/train0255_split_595.pkl +pkl_frame/train0255_split_596.pkl +pkl_frame/train0255_split_597.pkl +pkl_frame/train0255_split_598.pkl +pkl_frame/train0255_split_599.pkl +pkl_frame/train0255_split_59.pkl +pkl_frame/train0255_split_5.pkl +pkl_frame/train0255_split_600.pkl +pkl_frame/train0255_split_601.pkl +pkl_frame/train0255_split_602.pkl +pkl_frame/train0255_split_603.pkl +pkl_frame/train0255_split_604.pkl +pkl_frame/train0255_split_605.pkl +pkl_frame/train0255_split_606.pkl +pkl_frame/train0255_split_607.pkl +pkl_frame/train0255_split_608.pkl +pkl_frame/train0255_split_609.pkl +pkl_frame/train0255_split_60.pkl +pkl_frame/train0255_split_610.pkl +pkl_frame/train0255_split_611.pkl +pkl_frame/train0255_split_612.pkl +pkl_frame/train0255_split_613.pkl +pkl_frame/train0255_split_614.pkl +pkl_frame/train0255_split_615.pkl +pkl_frame/train0255_split_616.pkl +pkl_frame/train0255_split_617.pkl +pkl_frame/train0255_split_618.pkl +pkl_frame/train0255_split_619.pkl +pkl_frame/train0255_split_61.pkl +pkl_frame/train0255_split_620.pkl +pkl_frame/train0255_split_621.pkl +pkl_frame/train0255_split_622.pkl +pkl_frame/train0255_split_623.pkl +pkl_frame/train0255_split_624.pkl +pkl_frame/train0255_split_625.pkl +pkl_frame/train0255_split_626.pkl +pkl_frame/train0255_split_627.pkl +pkl_frame/train0255_split_628.pkl +pkl_frame/train0255_split_629.pkl +pkl_frame/train0255_split_62.pkl +pkl_frame/train0255_split_630.pkl +pkl_frame/train0255_split_631.pkl +pkl_frame/train0255_split_632.pkl +pkl_frame/train0255_split_633.pkl +pkl_frame/train0255_split_634.pkl +pkl_frame/train0255_split_635.pkl +pkl_frame/train0255_split_636.pkl +pkl_frame/train0255_split_637.pkl +pkl_frame/train0255_split_638.pkl +pkl_frame/train0255_split_639.pkl +pkl_frame/train0255_split_63.pkl +pkl_frame/train0255_split_640.pkl +pkl_frame/train0255_split_641.pkl +pkl_frame/train0255_split_642.pkl +pkl_frame/train0255_split_643.pkl +pkl_frame/train0255_split_644.pkl +pkl_frame/train0255_split_645.pkl +pkl_frame/train0255_split_646.pkl +pkl_frame/train0255_split_647.pkl +pkl_frame/train0255_split_648.pkl +pkl_frame/train0255_split_649.pkl +pkl_frame/train0255_split_64.pkl +pkl_frame/train0255_split_650.pkl +pkl_frame/train0255_split_651.pkl +pkl_frame/train0255_split_652.pkl +pkl_frame/train0255_split_653.pkl +pkl_frame/train0255_split_654.pkl +pkl_frame/train0255_split_655.pkl +pkl_frame/train0255_split_656.pkl +pkl_frame/train0255_split_657.pkl +pkl_frame/train0255_split_658.pkl +pkl_frame/train0255_split_659.pkl +pkl_frame/train0255_split_65.pkl +pkl_frame/train0255_split_660.pkl +pkl_frame/train0255_split_661.pkl +pkl_frame/train0255_split_662.pkl +pkl_frame/train0255_split_663.pkl +pkl_frame/train0255_split_664.pkl +pkl_frame/train0255_split_665.pkl +pkl_frame/train0255_split_666.pkl +pkl_frame/train0255_split_667.pkl +pkl_frame/train0255_split_668.pkl +pkl_frame/train0255_split_669.pkl +pkl_frame/train0255_split_66.pkl +pkl_frame/train0255_split_670.pkl +pkl_frame/train0255_split_671.pkl +pkl_frame/train0255_split_672.pkl +pkl_frame/train0255_split_673.pkl +pkl_frame/train0255_split_674.pkl +pkl_frame/train0255_split_675.pkl +pkl_frame/train0255_split_676.pkl +pkl_frame/train0255_split_677.pkl +pkl_frame/train0255_split_678.pkl +pkl_frame/train0255_split_679.pkl +pkl_frame/train0255_split_67.pkl +pkl_frame/train0255_split_680.pkl +pkl_frame/train0255_split_681.pkl +pkl_frame/train0255_split_682.pkl +pkl_frame/train0255_split_683.pkl +pkl_frame/train0255_split_684.pkl +pkl_frame/train0255_split_685.pkl +pkl_frame/train0255_split_686.pkl +pkl_frame/train0255_split_687.pkl +pkl_frame/train0255_split_688.pkl +pkl_frame/train0255_split_689.pkl +pkl_frame/train0255_split_68.pkl +pkl_frame/train0255_split_690.pkl +pkl_frame/train0255_split_691.pkl +pkl_frame/train0255_split_692.pkl +pkl_frame/train0255_split_693.pkl +pkl_frame/train0255_split_694.pkl +pkl_frame/train0255_split_695.pkl +pkl_frame/train0255_split_696.pkl +pkl_frame/train0255_split_697.pkl +pkl_frame/train0255_split_698.pkl +pkl_frame/train0255_split_699.pkl +pkl_frame/train0255_split_69.pkl +pkl_frame/train0255_split_6.pkl +pkl_frame/train0255_split_700.pkl +pkl_frame/train0255_split_701.pkl +pkl_frame/train0255_split_702.pkl +pkl_frame/train0255_split_703.pkl +pkl_frame/train0255_split_704.pkl +pkl_frame/train0255_split_705.pkl +pkl_frame/train0255_split_706.pkl +pkl_frame/train0255_split_707.pkl +pkl_frame/train0255_split_708.pkl +pkl_frame/train0255_split_709.pkl +pkl_frame/train0255_split_70.pkl +pkl_frame/train0255_split_710.pkl +pkl_frame/train0255_split_711.pkl +pkl_frame/train0255_split_712.pkl +pkl_frame/train0255_split_713.pkl +pkl_frame/train0255_split_714.pkl +pkl_frame/train0255_split_715.pkl +pkl_frame/train0255_split_716.pkl +pkl_frame/train0255_split_717.pkl +pkl_frame/train0255_split_718.pkl +pkl_frame/train0255_split_719.pkl +pkl_frame/train0255_split_71.pkl +pkl_frame/train0255_split_720.pkl +pkl_frame/train0255_split_721.pkl +pkl_frame/train0255_split_722.pkl +pkl_frame/train0255_split_723.pkl +pkl_frame/train0255_split_724.pkl +pkl_frame/train0255_split_725.pkl +pkl_frame/train0255_split_726.pkl +pkl_frame/train0255_split_727.pkl +pkl_frame/train0255_split_728.pkl +pkl_frame/train0255_split_729.pkl +pkl_frame/train0255_split_72.pkl +pkl_frame/train0255_split_730.pkl +pkl_frame/train0255_split_731.pkl +pkl_frame/train0255_split_732.pkl +pkl_frame/train0255_split_733.pkl +pkl_frame/train0255_split_734.pkl +pkl_frame/train0255_split_735.pkl +pkl_frame/train0255_split_736.pkl +pkl_frame/train0255_split_737.pkl +pkl_frame/train0255_split_738.pkl +pkl_frame/train0255_split_739.pkl +pkl_frame/train0255_split_73.pkl +pkl_frame/train0255_split_740.pkl +pkl_frame/train0255_split_741.pkl +pkl_frame/train0255_split_742.pkl +pkl_frame/train0255_split_743.pkl +pkl_frame/train0255_split_744.pkl +pkl_frame/train0255_split_745.pkl +pkl_frame/train0255_split_746.pkl +pkl_frame/train0255_split_747.pkl +pkl_frame/train0255_split_748.pkl +pkl_frame/train0255_split_749.pkl +pkl_frame/train0255_split_74.pkl +pkl_frame/train0255_split_750.pkl +pkl_frame/train0255_split_751.pkl +pkl_frame/train0255_split_752.pkl +pkl_frame/train0255_split_753.pkl +pkl_frame/train0255_split_754.pkl +pkl_frame/train0255_split_755.pkl +pkl_frame/train0255_split_756.pkl +pkl_frame/train0255_split_757.pkl +pkl_frame/train0255_split_758.pkl +pkl_frame/train0255_split_759.pkl +pkl_frame/train0255_split_75.pkl +pkl_frame/train0255_split_760.pkl +pkl_frame/train0255_split_761.pkl +pkl_frame/train0255_split_762.pkl +pkl_frame/train0255_split_763.pkl +pkl_frame/train0255_split_764.pkl +pkl_frame/train0255_split_765.pkl +pkl_frame/train0255_split_766.pkl +pkl_frame/train0255_split_767.pkl +pkl_frame/train0255_split_768.pkl +pkl_frame/train0255_split_769.pkl +pkl_frame/train0255_split_76.pkl +pkl_frame/train0255_split_770.pkl +pkl_frame/train0255_split_771.pkl +pkl_frame/train0255_split_772.pkl +pkl_frame/train0255_split_773.pkl +pkl_frame/train0255_split_774.pkl +pkl_frame/train0255_split_775.pkl +pkl_frame/train0255_split_776.pkl +pkl_frame/train0255_split_777.pkl +pkl_frame/train0255_split_778.pkl +pkl_frame/train0255_split_779.pkl +pkl_frame/train0255_split_77.pkl +pkl_frame/train0255_split_780.pkl +pkl_frame/train0255_split_781.pkl +pkl_frame/train0255_split_782.pkl +pkl_frame/train0255_split_783.pkl +pkl_frame/train0255_split_784.pkl +pkl_frame/train0255_split_785.pkl +pkl_frame/train0255_split_786.pkl +pkl_frame/train0255_split_787.pkl +pkl_frame/train0255_split_788.pkl +pkl_frame/train0255_split_789.pkl +pkl_frame/train0255_split_78.pkl +pkl_frame/train0255_split_790.pkl +pkl_frame/train0255_split_791.pkl +pkl_frame/train0255_split_792.pkl +pkl_frame/train0255_split_793.pkl +pkl_frame/train0255_split_794.pkl +pkl_frame/train0255_split_795.pkl +pkl_frame/train0255_split_796.pkl +pkl_frame/train0255_split_797.pkl +pkl_frame/train0255_split_798.pkl +pkl_frame/train0255_split_799.pkl +pkl_frame/train0255_split_79.pkl +pkl_frame/train0255_split_7.pkl +pkl_frame/train0255_split_800.pkl +pkl_frame/train0255_split_801.pkl +pkl_frame/train0255_split_802.pkl +pkl_frame/train0255_split_803.pkl +pkl_frame/train0255_split_804.pkl +pkl_frame/train0255_split_805.pkl +pkl_frame/train0255_split_806.pkl +pkl_frame/train0255_split_807.pkl +pkl_frame/train0255_split_808.pkl +pkl_frame/train0255_split_809.pkl +pkl_frame/train0255_split_80.pkl +pkl_frame/train0255_split_810.pkl +pkl_frame/train0255_split_811.pkl +pkl_frame/train0255_split_812.pkl +pkl_frame/train0255_split_813.pkl +pkl_frame/train0255_split_814.pkl +pkl_frame/train0255_split_815.pkl +pkl_frame/train0255_split_816.pkl +pkl_frame/train0255_split_817.pkl +pkl_frame/train0255_split_818.pkl +pkl_frame/train0255_split_819.pkl +pkl_frame/train0255_split_81.pkl +pkl_frame/train0255_split_820.pkl +pkl_frame/train0255_split_821.pkl +pkl_frame/train0255_split_822.pkl +pkl_frame/train0255_split_823.pkl +pkl_frame/train0255_split_824.pkl +pkl_frame/train0255_split_825.pkl +pkl_frame/train0255_split_826.pkl +pkl_frame/train0255_split_827.pkl +pkl_frame/train0255_split_828.pkl +pkl_frame/train0255_split_829.pkl +pkl_frame/train0255_split_82.pkl +pkl_frame/train0255_split_830.pkl +pkl_frame/train0255_split_831.pkl +pkl_frame/train0255_split_832.pkl +pkl_frame/train0255_split_833.pkl +pkl_frame/train0255_split_834.pkl +pkl_frame/train0255_split_835.pkl +pkl_frame/train0255_split_836.pkl +pkl_frame/train0255_split_837.pkl +pkl_frame/train0255_split_838.pkl +pkl_frame/train0255_split_839.pkl +pkl_frame/train0255_split_83.pkl +pkl_frame/train0255_split_840.pkl +pkl_frame/train0255_split_841.pkl +pkl_frame/train0255_split_842.pkl +pkl_frame/train0255_split_843.pkl +pkl_frame/train0255_split_844.pkl +pkl_frame/train0255_split_845.pkl +pkl_frame/train0255_split_846.pkl +pkl_frame/train0255_split_847.pkl +pkl_frame/train0255_split_848.pkl +pkl_frame/train0255_split_849.pkl +pkl_frame/train0255_split_84.pkl +pkl_frame/train0255_split_850.pkl +pkl_frame/train0255_split_851.pkl +pkl_frame/train0255_split_852.pkl +pkl_frame/train0255_split_853.pkl +pkl_frame/train0255_split_854.pkl +pkl_frame/train0255_split_855.pkl +pkl_frame/train0255_split_856.pkl +pkl_frame/train0255_split_857.pkl +pkl_frame/train0255_split_858.pkl +pkl_frame/train0255_split_859.pkl +pkl_frame/train0255_split_85.pkl +pkl_frame/train0255_split_860.pkl +pkl_frame/train0255_split_861.pkl +pkl_frame/train0255_split_862.pkl +pkl_frame/train0255_split_863.pkl +pkl_frame/train0255_split_864.pkl +pkl_frame/train0255_split_865.pkl +pkl_frame/train0255_split_866.pkl +pkl_frame/train0255_split_867.pkl +pkl_frame/train0255_split_868.pkl +pkl_frame/train0255_split_869.pkl +pkl_frame/train0255_split_86.pkl +pkl_frame/train0255_split_870.pkl +pkl_frame/train0255_split_871.pkl +pkl_frame/train0255_split_872.pkl +pkl_frame/train0255_split_873.pkl +pkl_frame/train0255_split_874.pkl +pkl_frame/train0255_split_875.pkl +pkl_frame/train0255_split_876.pkl +pkl_frame/train0255_split_877.pkl +pkl_frame/train0255_split_878.pkl +pkl_frame/train0255_split_879.pkl +pkl_frame/train0255_split_87.pkl +pkl_frame/train0255_split_880.pkl +pkl_frame/train0255_split_881.pkl +pkl_frame/train0255_split_882.pkl +pkl_frame/train0255_split_883.pkl +pkl_frame/train0255_split_884.pkl +pkl_frame/train0255_split_885.pkl +pkl_frame/train0255_split_886.pkl +pkl_frame/train0255_split_887.pkl +pkl_frame/train0255_split_888.pkl +pkl_frame/train0255_split_889.pkl +pkl_frame/train0255_split_88.pkl +pkl_frame/train0255_split_890.pkl +pkl_frame/train0255_split_891.pkl +pkl_frame/train0255_split_892.pkl +pkl_frame/train0255_split_893.pkl +pkl_frame/train0255_split_894.pkl +pkl_frame/train0255_split_895.pkl +pkl_frame/train0255_split_896.pkl +pkl_frame/train0255_split_897.pkl +pkl_frame/train0255_split_898.pkl +pkl_frame/train0255_split_899.pkl +pkl_frame/train0255_split_89.pkl +pkl_frame/train0255_split_8.pkl +pkl_frame/train0255_split_900.pkl +pkl_frame/train0255_split_901.pkl +pkl_frame/train0255_split_902.pkl +pkl_frame/train0255_split_903.pkl +pkl_frame/train0255_split_904.pkl +pkl_frame/train0255_split_905.pkl +pkl_frame/train0255_split_906.pkl +pkl_frame/train0255_split_907.pkl +pkl_frame/train0255_split_908.pkl +pkl_frame/train0255_split_909.pkl +pkl_frame/train0255_split_90.pkl +pkl_frame/train0255_split_910.pkl +pkl_frame/train0255_split_911.pkl +pkl_frame/train0255_split_912.pkl +pkl_frame/train0255_split_913.pkl +pkl_frame/train0255_split_914.pkl +pkl_frame/train0255_split_915.pkl +pkl_frame/train0255_split_916.pkl +pkl_frame/train0255_split_917.pkl +pkl_frame/train0255_split_918.pkl +pkl_frame/train0255_split_919.pkl +pkl_frame/train0255_split_91.pkl +pkl_frame/train0255_split_920.pkl +pkl_frame/train0255_split_921.pkl +pkl_frame/train0255_split_922.pkl +pkl_frame/train0255_split_923.pkl +pkl_frame/train0255_split_924.pkl +pkl_frame/train0255_split_925.pkl +pkl_frame/train0255_split_926.pkl +pkl_frame/train0255_split_927.pkl +pkl_frame/train0255_split_928.pkl +pkl_frame/train0255_split_929.pkl +pkl_frame/train0255_split_92.pkl +pkl_frame/train0255_split_930.pkl +pkl_frame/train0255_split_931.pkl +pkl_frame/train0255_split_932.pkl +pkl_frame/train0255_split_933.pkl +pkl_frame/train0255_split_934.pkl +pkl_frame/train0255_split_935.pkl +pkl_frame/train0255_split_936.pkl +pkl_frame/train0255_split_937.pkl +pkl_frame/train0255_split_938.pkl +pkl_frame/train0255_split_939.pkl +pkl_frame/train0255_split_93.pkl +pkl_frame/train0255_split_940.pkl +pkl_frame/train0255_split_941.pkl +pkl_frame/train0255_split_942.pkl +pkl_frame/train0255_split_943.pkl +pkl_frame/train0255_split_944.pkl +pkl_frame/train0255_split_945.pkl +pkl_frame/train0255_split_946.pkl +pkl_frame/train0255_split_947.pkl +pkl_frame/train0255_split_948.pkl +pkl_frame/train0255_split_949.pkl +pkl_frame/train0255_split_94.pkl +pkl_frame/train0255_split_950.pkl +pkl_frame/train0255_split_951.pkl +pkl_frame/train0255_split_952.pkl +pkl_frame/train0255_split_953.pkl +pkl_frame/train0255_split_954.pkl +pkl_frame/train0255_split_955.pkl +pkl_frame/train0255_split_956.pkl +pkl_frame/train0255_split_957.pkl +pkl_frame/train0255_split_958.pkl +pkl_frame/train0255_split_959.pkl +pkl_frame/train0255_split_95.pkl +pkl_frame/train0255_split_960.pkl +pkl_frame/train0255_split_961.pkl +pkl_frame/train0255_split_962.pkl +pkl_frame/train0255_split_963.pkl +pkl_frame/train0255_split_964.pkl +pkl_frame/train0255_split_965.pkl +pkl_frame/train0255_split_966.pkl +pkl_frame/train0255_split_967.pkl +pkl_frame/train0255_split_968.pkl +pkl_frame/train0255_split_969.pkl +pkl_frame/train0255_split_96.pkl +pkl_frame/train0255_split_970.pkl +pkl_frame/train0255_split_971.pkl +pkl_frame/train0255_split_972.pkl +pkl_frame/train0255_split_973.pkl +pkl_frame/train0255_split_974.pkl +pkl_frame/train0255_split_975.pkl +pkl_frame/train0255_split_976.pkl +pkl_frame/train0255_split_977.pkl +pkl_frame/train0255_split_978.pkl +pkl_frame/train0255_split_979.pkl +pkl_frame/train0255_split_97.pkl +pkl_frame/train0255_split_980.pkl +pkl_frame/train0255_split_981.pkl +pkl_frame/train0255_split_982.pkl +pkl_frame/train0255_split_983.pkl +pkl_frame/train0255_split_984.pkl +pkl_frame/train0255_split_985.pkl +pkl_frame/train0255_split_986.pkl +pkl_frame/train0255_split_987.pkl +pkl_frame/train0255_split_988.pkl +pkl_frame/train0255_split_989.pkl +pkl_frame/train0255_split_98.pkl +pkl_frame/train0255_split_990.pkl +pkl_frame/train0255_split_991.pkl +pkl_frame/train0255_split_992.pkl +pkl_frame/train0255_split_993.pkl +pkl_frame/train0255_split_994.pkl +pkl_frame/train0255_split_995.pkl +pkl_frame/train0255_split_996.pkl +pkl_frame/train0255_split_997.pkl +pkl_frame/train0255_split_998.pkl +pkl_frame/train0255_split_999.pkl +pkl_frame/train0255_split_99.pkl +pkl_frame/train0255_split_9.pkl diff --git a/deploy/cpp_infer/CMakeLists.txt b/deploy/cpp_infer/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..3e709c7723a33e20a567f314462cd1c670c070d7 --- /dev/null +++ b/deploy/cpp_infer/CMakeLists.txt @@ -0,0 +1,227 @@ +project(ppvideo CXX C) +cmake_minimum_required(VERSION 3.14) + +option(WITH_MKL "Compile demo with MKL/OpenBlas support, default use MKL." ON) +option(WITH_GPU "Compile demo with GPU/CPU, default use CPU." OFF) +option(WITH_STATIC_LIB "Compile demo with static/shared library, default use static." ON) +option(WITH_TENSORRT "Compile demo with TensorRT." OFF) + +SET(PADDLE_LIB "" CACHE PATH "Location of libraries") +SET(OPENCV_DIR "" CACHE PATH "Location of libraries") +SET(CUDA_LIB "" CACHE PATH "Location of libraries") +SET(CUDNN_LIB "" CACHE PATH "Location of libraries") +SET(TENSORRT_DIR "" CACHE PATH "Compile demo with TensorRT") + +set(DEMO_NAME "ppvideo") + + +macro(safe_set_static_flag) + foreach(flag_var + CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO) + if(${flag_var} MATCHES "/MD") + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + endif(${flag_var} MATCHES "/MD") + endforeach(flag_var) +endmacro() + +if (WITH_MKL) + ADD_DEFINITIONS(-DUSE_MKL) +endif() + +if(NOT DEFINED PADDLE_LIB) + message(FATAL_ERROR "please set PADDLE_LIB with -DPADDLE_LIB=/path/paddle/lib") +endif() + +if(NOT DEFINED OPENCV_DIR) + message(FATAL_ERROR "please set OPENCV_DIR with -DOPENCV_DIR=/path/opencv") +endif() + + +if (WIN32) + include_directories("${PADDLE_LIB}/paddle/include") + link_directories("${PADDLE_LIB}/paddle/lib") + find_package(OpenCV REQUIRED PATHS ${OPENCV_DIR}/build/ NO_DEFAULT_PATH) + +else () + find_package(OpenCV REQUIRED PATHS ${OPENCV_DIR}/share/OpenCV NO_DEFAULT_PATH) + include_directories("${PADDLE_LIB}/paddle/include") + link_directories("${PADDLE_LIB}/paddle/lib") +endif () +include_directories(${OpenCV_INCLUDE_DIRS}) + +if (WIN32) + add_definitions("/DGOOGLE_GLOG_DLL_DECL=") + if(WITH_MKL) + set(FLAG_OPENMP "/openmp") + endif() + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /bigobj /MTd ${FLAG_OPENMP}") + set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /bigobj /MT ${FLAG_OPENMP}") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj /MTd ${FLAG_OPENMP}") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /bigobj /MT ${FLAG_OPENMP}") + if (WITH_STATIC_LIB) + safe_set_static_flag() + add_definitions(-DSTATIC_LIB) + endif() + message("cmake c debug flags " ${CMAKE_C_FLAGS_DEBUG}) + message("cmake c release flags " ${CMAKE_C_FLAGS_RELEASE}) + message("cmake cxx debug flags " ${CMAKE_CXX_FLAGS_DEBUG}) + message("cmake cxx release flags " ${CMAKE_CXX_FLAGS_RELEASE}) +else() + if(WITH_MKL) + set(FLAG_OPENMP "-fopenmp") + endif() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -o3 ${FLAG_OPENMP} -std=c++11") + set(CMAKE_STATIC_LIBRARY_PREFIX "") + message("cmake cxx flags" ${CMAKE_CXX_FLAGS}) +endif() + +if (WITH_GPU) + if (NOT DEFINED CUDA_LIB OR ${CUDA_LIB} STREQUAL "") + message(FATAL_ERROR "please set CUDA_LIB with -DCUDA_LIB=/path/cuda-8.0/lib64") + endif() + if (NOT WIN32) + if (NOT DEFINED CUDNN_LIB) + message(FATAL_ERROR "please set CUDNN_LIB with -DCUDNN_LIB=/path/cudnn_v7.4/cuda/lib64") + endif() + endif(NOT WIN32) +endif() + +include_directories("${PADDLE_LIB}/third_party/install/protobuf/include") +include_directories("${PADDLE_LIB}/third_party/install/glog/include") +include_directories("${PADDLE_LIB}/third_party/install/gflags/include") +include_directories("${PADDLE_LIB}/third_party/install/xxhash/include") +include_directories("${PADDLE_LIB}/third_party/install/zlib/include") +include_directories("${PADDLE_LIB}/third_party/boost") +include_directories("${PADDLE_LIB}/third_party/eigen3") + +include_directories("${CMAKE_SOURCE_DIR}/") + +if (NOT WIN32) + if (WITH_TENSORRT AND WITH_GPU) + include_directories("${TENSORRT_DIR}/include") + link_directories("${TENSORRT_DIR}/lib") + endif() +endif(NOT WIN32) + +link_directories("${PADDLE_LIB}/third_party/install/zlib/lib") + +link_directories("${PADDLE_LIB}/third_party/install/protobuf/lib") +link_directories("${PADDLE_LIB}/third_party/install/glog/lib") +link_directories("${PADDLE_LIB}/third_party/install/gflags/lib") +link_directories("${PADDLE_LIB}/third_party/install/xxhash/lib") +link_directories("${PADDLE_LIB}/paddle/lib") + + +if(WITH_MKL) + include_directories("${PADDLE_LIB}/third_party/install/mklml/include") + if (WIN32) + set(MATH_LIB ${PADDLE_LIB}/third_party/install/mklml/lib/mklml.lib + ${PADDLE_LIB}/third_party/install/mklml/lib/libiomp5md.lib) + else () + set(MATH_LIB ${PADDLE_LIB}/third_party/install/mklml/lib/libmklml_intel${CMAKE_SHARED_LIBRARY_SUFFIX} + ${PADDLE_LIB}/third_party/install/mklml/lib/libiomp5${CMAKE_SHARED_LIBRARY_SUFFIX}) + execute_process(COMMAND cp -r ${PADDLE_LIB}/third_party/install/mklml/lib/libmklml_intel${CMAKE_SHARED_LIBRARY_SUFFIX} /usr/lib) + endif () + set(MKLDNN_PATH "${PADDLE_LIB}/third_party/install/mkldnn") + if(EXISTS ${MKLDNN_PATH}) + include_directories("${MKLDNN_PATH}/include") + if (WIN32) + set(MKLDNN_LIB ${MKLDNN_PATH}/lib/mkldnn.lib) + else () + set(MKLDNN_LIB ${MKLDNN_PATH}/lib/libmkldnn.so.0) + endif () + endif() +else() + if (WIN32) + set(MATH_LIB ${PADDLE_LIB}/third_party/install/openblas/lib/openblas${CMAKE_STATIC_LIBRARY_SUFFIX}) + else () + set(MATH_LIB ${PADDLE_LIB}/third_party/install/openblas/lib/libopenblas${CMAKE_STATIC_LIBRARY_SUFFIX}) + endif () +endif() + +# Note: libpaddle_inference_api.so/a must put before libpaddle_inference.so/a +if(WITH_STATIC_LIB) + if(WIN32) + set(DEPS + ${PADDLE_LIB}/paddle/lib/paddle_inference${CMAKE_STATIC_LIBRARY_SUFFIX}) + else() + set(DEPS + ${PADDLE_LIB}/paddle/lib/libpaddle_inference${CMAKE_STATIC_LIBRARY_SUFFIX}) + endif() +else() + if(WIN32) + set(DEPS + ${PADDLE_LIB}/paddle/lib/paddle_inference${CMAKE_SHARED_LIBRARY_SUFFIX}) + else() + set(DEPS + ${PADDLE_LIB}/paddle/lib/libpaddle_inference${CMAKE_SHARED_LIBRARY_SUFFIX}) + endif() +endif(WITH_STATIC_LIB) + +if (NOT WIN32) + set(DEPS ${DEPS} + ${MATH_LIB} ${MKLDNN_LIB} + glog gflags protobuf z xxhash + ) + if(EXISTS "${PADDLE_LIB}/third_party/install/snappystream/lib") + set(DEPS ${DEPS} snappystream) + endif() + if (EXISTS "${PADDLE_LIB}/third_party/install/snappy/lib") + set(DEPS ${DEPS} snappy) + endif() +else() + set(DEPS ${DEPS} + ${MATH_LIB} ${MKLDNN_LIB} + glog gflags_static libprotobuf xxhash) + set(DEPS ${DEPS} libcmt shlwapi) + if (EXISTS "${PADDLE_LIB}/third_party/install/snappy/lib") + set(DEPS ${DEPS} snappy) + endif() + if(EXISTS "${PADDLE_LIB}/third_party/install/snappystream/lib") + set(DEPS ${DEPS} snappystream) + endif() +endif(NOT WIN32) + + +if(WITH_GPU) + if(NOT WIN32) + if (WITH_TENSORRT) + set(DEPS ${DEPS} ${TENSORRT_DIR}/lib/libnvinfer${CMAKE_SHARED_LIBRARY_SUFFIX}) + set(DEPS ${DEPS} ${TENSORRT_DIR}/lib/libnvinfer_plugin${CMAKE_SHARED_LIBRARY_SUFFIX}) + endif() + set(DEPS ${DEPS} ${CUDA_LIB}/libcudart${CMAKE_SHARED_LIBRARY_SUFFIX}) + set(DEPS ${DEPS} ${CUDNN_LIB}/libcudnn${CMAKE_SHARED_LIBRARY_SUFFIX}) + else() + set(DEPS ${DEPS} ${CUDA_LIB}/cudart${CMAKE_STATIC_LIBRARY_SUFFIX} ) + set(DEPS ${DEPS} ${CUDA_LIB}/cublas${CMAKE_STATIC_LIBRARY_SUFFIX} ) + set(DEPS ${DEPS} ${CUDNN_LIB}/cudnn${CMAKE_STATIC_LIBRARY_SUFFIX}) + endif() +endif() + + +if (NOT WIN32) + set(EXTERNAL_LIB "-ldl -lrt -lgomp -lz -lm -lpthread") + set(DEPS ${DEPS} ${EXTERNAL_LIB}) +endif() + +set(DEPS ${DEPS} ${OpenCV_LIBS}) + +include(FetchContent) +include(external-cmake/auto-log.cmake) +include_directories(${FETCHCONTENT_BASE_DIR}/extern_autolog-src) + +AUX_SOURCE_DIRECTORY(./src SRCS) +add_executable(${DEMO_NAME} ${SRCS}) +target_link_libraries(${DEMO_NAME} ${DEPS}) + +if (WIN32 AND WITH_MKL) + add_custom_command(TARGET ${DEMO_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PADDLE_LIB}/third_party/install/mklml/lib/mklml.dll ./mklml.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PADDLE_LIB}/third_party/install/mklml/lib/libiomp5md.dll ./libiomp5md.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PADDLE_LIB}/third_party/install/mkldnn/lib/mkldnn.dll ./mkldnn.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PADDLE_LIB}/third_party/install/mklml/lib/mklml.dll ./release/mklml.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PADDLE_LIB}/third_party/install/mklml/lib/libiomp5md.dll ./release/libiomp5md.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PADDLE_LIB}/third_party/install/mkldnn/lib/mkldnn.dll ./release/mkldnn.dll + ) +endif() diff --git a/deploy/cpp_infer/example_video_dir/example01.avi b/deploy/cpp_infer/example_video_dir/example01.avi new file mode 100644 index 0000000000000000000000000000000000000000..ded660f086d8c488fdae736e2bb3ada547b64908 Binary files /dev/null and b/deploy/cpp_infer/example_video_dir/example01.avi differ diff --git a/deploy/cpp_infer/external-cmake/auto-log.cmake b/deploy/cpp_infer/external-cmake/auto-log.cmake new file mode 100644 index 0000000000000000000000000000000000000000..9be9c2fb3b6d61207fad0d9bf216f4b9ad715fe3 --- /dev/null +++ b/deploy/cpp_infer/external-cmake/auto-log.cmake @@ -0,0 +1,12 @@ +find_package(Git REQUIRED) +include(FetchContent) + +set(FETCHCONTENT_BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}/third-party") + +FetchContent_Declare( + extern_Autolog + PREFIX autolog + GIT_REPOSITORY https://github.com/LDOUBLEV/AutoLog.git + GIT_TAG main +) +FetchContent_MakeAvailable(extern_Autolog) diff --git a/deploy/cpp_infer/imgs/PPTSM_pred_result.png b/deploy/cpp_infer/imgs/PPTSM_pred_result.png new file mode 100644 index 0000000000000000000000000000000000000000..9bcfe70c8c2c43808ec651d8340ca22fb378feeb Binary files /dev/null and b/deploy/cpp_infer/imgs/PPTSM_pred_result.png differ diff --git a/deploy/cpp_infer/include/postprocess_op.h b/deploy/cpp_infer/include/postprocess_op.h new file mode 100644 index 0000000000000000000000000000000000000000..d250432b4521ecced16a104641259c763128be5c --- /dev/null +++ b/deploy/cpp_infer/include/postprocess_op.h @@ -0,0 +1,43 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "include/utility.h" + + +namespace PaddleVideo +{ + + class Softmax + { + public: + virtual void Inplace_Run(const std::vector::iterator &_begin, const std::vector::iterator &_end); + virtual std::vector Run(const std::vector::iterator &_begin, const std::vector::iterator &_end); + }; + +} // namespace PaddleVideo diff --git a/deploy/cpp_infer/include/preprocess_op.h b/deploy/cpp_infer/include/preprocess_op.h new file mode 100644 index 0000000000000000000000000000000000000000..2c397937259a9f387b95ad4e9d3c08d84799adc2 --- /dev/null +++ b/deploy/cpp_infer/include/preprocess_op.h @@ -0,0 +1,74 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace std; +using namespace paddle; + +namespace PaddleVideo +{ + + class Normalize + { + public: + virtual void Run(cv::Mat *im, const std::vector &mean, + const std::vector &scale, const bool is_scale = true); + }; + + // RGB -> CHW + class Permute + { + public: + virtual void Run(const cv::Mat *img, float *data); + }; + + class Scale + { + public: + virtual void Run(const cv::Mat &img, cv::Mat &resize_img, + bool use_tensorrt = false, + const int &short_size = 256); + }; + + class CenterCrop + { + public: + virtual void Run(const cv::Mat &img, cv::Mat &crop_img, + bool use_tensorrt = false, + const int &target_size = 224); + }; + + class TenCrop + { + public: + virtual void Run(const cv::Mat &img, std::vector &crop_frames, + const int &begin_index, + bool use_tensorrt = false, + const int &target_size = 224); + }; +} // namespace PaddleVideo diff --git a/deploy/cpp_infer/include/utility.h b/deploy/cpp_infer/include/utility.h new file mode 100644 index 0000000000000000000000000000000000000000..d26d8175d2295fd6fb582d88a23f6a105dc80427 --- /dev/null +++ b/deploy/cpp_infer/include/utility.h @@ -0,0 +1,54 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "opencv2/opencv.hpp" + +namespace PaddleVideo +{ + + class Utility + { + public: + static std::vector ReadDict(const std::string &path); + + static void GetAllFiles(const char *dir_name, std::vector &all_inputs); + + static cv::Mat GetRotateCropImage(const cv::Mat &srcimage, std::vector> box); + + template inline static size_t argmax(ForwardIterator first, ForwardIterator last) + { + return std::distance(first, std::max_element(first, last)); + } + + static std::vector SampleFramesFromVideo(const std::string &VideoPath, const int &num_seg, const int &seg_len); + }; + +} // namespace PaddleVideo diff --git a/deploy/cpp_infer/include/video_rec.h b/deploy/cpp_infer/include/video_rec.h new file mode 100644 index 0000000000000000000000000000000000000000..c1ac2abfe0729c569317508c82ac30103a676fa8 --- /dev/null +++ b/deploy/cpp_infer/include/video_rec.h @@ -0,0 +1,105 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "paddle_api.h" +#include "paddle_inference_api.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace paddle_infer; + +namespace PaddleVideo +{ + + class VideoRecognizer + { + public: + explicit VideoRecognizer(const std::string &model_dir, const std::string &inference_model_name, const bool &use_gpu, const int &num_seg, + const int &rec_batch_num, const int &gpu_id, + const int &gpu_mem, const int &cpu_math_library_num_threads, + const bool &use_mkldnn, const std::string &label_path, + const bool &use_tensorrt, const std::string &precision, const std::vector &_mean = {0.406, 0.456, 0.485}, + const std::vector &_scale = {0.225, 0.224, 0.229}) + { + this->inference_model_name = inference_model_name; + this->use_gpu_ = use_gpu; + this->num_seg = num_seg; + this->rec_batch_num = rec_batch_num; + this->gpu_id_ = gpu_id; + this->gpu_mem_ = gpu_mem; + this->cpu_math_library_num_threads_ = cpu_math_library_num_threads; + this->use_mkldnn_ = use_mkldnn; + this->use_tensorrt_ = use_tensorrt; + this->precision_ = precision; + this->mean_ = _mean; + this->scale_ = _scale; + this->label_list_ = Utility::ReadDict(label_path); + LoadModel(model_dir); + } + + // Load Paddle inference model + void LoadModel(const std::string &model_dir); + + void Run(const std::vector &frames_batch_path, const std::vector > &frames_batch, std::vector *times); + + private: + std::string inference_model_name; + std::shared_ptr predictor_; + + bool use_gpu_ = false; + int gpu_id_ = 0; + + int rec_batch_num = 1; + int gpu_mem_ = 4000; + int cpu_math_library_num_threads_ = 4; + bool use_mkldnn_ = false; + int num_seg = 8; + std::vector label_list_; + std::vector mean_ = {0.406, 0.456, 0.485}; + std::vector scale_ = {0.225, 0.224, 0.229}; + bool is_scale_ = true; + bool use_tensorrt_ = false; + std::string precision_ = "fp32"; + + // Instantiate pre-process operation object(s) + Scale scale_op_; + + CenterCrop centercrop_op_; + TenCrop tencrop_op_; + + Normalize normalize_op_; + Permute permute_op_; + + // Instantiate post-process operation object(s) + Softmax softmax_op_; + + }; // class VideoRecognizer + +} // namespace PaddleVideo diff --git a/deploy/cpp_infer/readme.md b/deploy/cpp_infer/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..1bd1f1dec561b496a7a7144bf2245f14725458d4 --- /dev/null +++ b/deploy/cpp_infer/readme.md @@ -0,0 +1,324 @@ +[English](./readme_en.md) | 简体中文 + +# 服务器端C++预测 + +本章节介绍PaddleVideo模型的的C++部署方法,python预测部署方法请参考各自模型的**模型推理**章节。 +C++在性能计算上优于python,因此,在大多数CPU、GPU部署场景,多采用C++的部署方式,本节将介绍如何在Linux(CPU/GPU)环境下配置C++环境并完成 +PaddleVideo模型部署。 + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install git+https://github.com/LDOUBLEV/AutoLog +``` + +## 1. 准备环境 + +- Linux环境,推荐使用docker。 + +- Windows环境,目前支持基于`Visual Studio 2019 Community`进行编译(TODO) + +* 该文档主要介绍基于Linux环境的PaddleVideo C++预测流程,如果需要在Windows下基于预测库进行C++预测,具体编译方法请参考[Windows下编译教程](./docs/windows_vs2019_build.md)(TODO) +* **准备环境的目的是得到编译好的opencv库与paddle预测库**。 + +### 1.1 编译opencv库 + +* 首先需要从opencv官网上下载在Linux环境下源码编译的压缩包,并解压成文件夹。以opencv3.4.7为例,下载命令如下: + + ```bash + cd deploy/cpp_infer + wget https://github.com/opencv/opencv/archive/3.4.7.tar.gz + tar -xf 3.4.7.tar.gz + ``` + + 解压完毕后在`deploy/cpp_infer`目录下可以得到解压出的`opencv-3.4.7`的文件夹。 + +* 安装ffmpeg + + opencv配合ffmpeg才能在linux下正常读取视频,否则可能遇到视频帧数返回为0或无法读取任何视频帧的情况 + + 采用较为简单的apt安装,安装命令如下: + + ```bash + apt-get update + + apt install libavformat-dev + apt install libavcodec-dev + apt install libswresample-dev + apt install libswscale-dev + apt install libavutil-dev + apt install libsdl1.2-dev + + apt-get install ffmpeg + ``` + +* 准备编译opencv,首先进入`opencv-3.4.7`的文件夹,然后设置opencv源码路径`root_path`以及安装路径`install_path`。执行命令如下: + + ```bash + cd opencv-3.4.7 + + root_path=$PWD # 当前所在路径即为opencv-3.4.7的绝对路径 + install_path=${root_path}/opencv3 + + rm -rf build + mkdir build + cd build + + cmake .. \ + -DCMAKE_INSTALL_PREFIX=${install_path} \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DWITH_IPP=OFF \ + -DBUILD_IPP_IW=OFF \ + -DWITH_LAPACK=OFF \ + -DWITH_EIGEN=OFF \ + -DCMAKE_INSTALL_LIBDIR=lib64 \ + -DWITH_ZLIB=ON \ + -DBUILD_ZLIB=ON \ + -DWITH_JPEG=ON \ + -DBUILD_JPEG=ON \ + -DWITH_PNG=ON \ + -DBUILD_PNG=ON \ + -DWITH_TIFF=ON \ + -DBUILD_TIFF=ON \ + -DWITH_FFMPEG=ON + + make -j + make install + ``` + + `make install`完成之后,会在该文件夹下生成opencv头文件和库文件,用于后面的Video推理C++代码编译。 + + 最终会以安装路径`install_path`为指定路径,得到一个`opencv3`的文件夹,其文件结构如下所示。 + + ```shell + opencv-3.4.7/ + ├── opencv3/ # 安装在opencv3目录下 + │ ├── bin/ + │ ├── include/ + │ ├── lib/ + │ ├── lib64/ + │ └── share/ + ``` + +### 1.2 下载或者编译Paddle预测库 + +有2种方式获取Paddle预测库,下面进行详细介绍。 + + +#### 1.2.1 直接下载安装 + +* [Paddle预测库官网](https://paddleinference.paddlepaddle.org.cn/v2.2/user_guides/download_lib.html) 上提供了不同cuda版本的Linux预测库,可以在官网查看并**选择合适的预测库版本**(建议选择paddle版本>=2.0.1版本的预测库,推荐使用2.2.2的预测库)。 + +* 下载得到一个`paddle_inference.tgz`压缩包,然后将它解压成文件夹,命令如下(以机器环境为gcc8.2为例): + + ```bash + wget https://paddle-inference-lib.bj.bcebos.com/2.2.2/cxx_c/Linux/GPU/x86-64_gcc8.2_avx_mkl_cuda10.1_cudnn7.6.5_trt6.0.1.5/paddle_inference.tgz + tar -xf paddle_inference.tgz + ``` + + 最终会在当前的文件夹中生成`paddle_inference/`的子文件夹。 + +#### 1.2.2 预测库源码编译 +* 如果希望获取最新预测库特性,可以从Paddle github上克隆最新代码,源码编译预测库。 +* 可以参考[Paddle预测库安装编译说明](https://www.paddlepaddle.org.cn/documentation/docs/zh/2.0/guides/05_inference_deployment/inference/build_and_install_lib_cn.html#congyuanmabianyi) 的说明,从github上获取Paddle代码,然后进行编译,生成最新的预测库。使用git获取代码方法如下。 + + ```shell + git clone https://github.com/PaddlePaddle/Paddle.git + git checkout release/2.2 + ``` + +* 进入Paddle目录后,编译方法如下。 + + ```shell + rm -rf build + mkdir build + cd build + + cmake .. \ + -DWITH_CONTRIB=OFF \ + -DWITH_MKL=ON \ + -DWITH_MKLDNN=ON \ + -DWITH_TESTING=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DWITH_INFERENCE_API_TEST=OFF \ + -DON_INFER=ON \ + -DWITH_PYTHON=ON + make -j4 + make inference_lib_dist -j4 # 4为编译时使用核数,可根据机器情况自行修改 + ``` + + 更多编译参数选项介绍可以参考[文档说明](https://www.paddlepaddle.org.cn/documentation/docs/zh/2.0/guides/05_inference_deployment/inference/build_and_install_lib_cn.html#congyuanmabianyi)。 + + +* 编译完成之后,可以在`build/paddle_inference_install_dir/`文件下看到生成了以下文件及文件夹。 + + ```bash + build/ + └── paddle_inference_install_dir/ + ├── CMakeCache.txt + ├── paddle/ + ├── third_party/ + └── version.txt + ``` + + 其中`paddle`就是C++预测所需的Paddle库,`version.txt`中包含当前预测库的版本信息。 + +## 2. 编译并运行预测demo + +### 2.1 将模型导出为inference model + +* 该步骤与python部署方式下的导出预测模型相同,可以参考各自模型的模型预测章节。导出的几个相关inference model文件用于模型预测。**以PP-TSM为例**,导出预测模型的目录结构如下。 + + ``` + inference/ + └── ppTSM/ + ├── ppTSM.pdiparams + ├── ppTSM.pdiparamsinfo + └── ppTSM.pdmodel + ``` + + +### 2.2 编译PaddleVideo C++预测demo + +* 进入到`deploy/cpp_infer`目录下,执行以下编译命令 + + ```shell + bash tools/build.sh + ``` + + `tools/build.sh`中的Paddle C++预测库、opencv等其他依赖库的地址需要换成自己机器上的实际地址。 + +* 具体地,需要修改`tools/build.sh`中的环境路径,相关内容如下: + + ```shell + OPENCV_DIR=your_opencv_dir + LIB_DIR=your_paddle_inference_dir + CUDA_LIB_DIR=your_cuda_lib_dir + CUDNN_LIB_DIR=your_cudnn_lib_dir + ``` + + 上述参数如下(以下路径用户可根据自己机器的情况对应修改) + + ```bash + OPENCV_DIR=/path/to/opencv3 + LIB_DIR=/path/to/paddle_inference + CUDA_LIB_DIR=/usr/local/cuda/lib64 + CUDNN_LIB_DIR=/usr/lib/x86_64-linux-gnu/ + ``` + + `OPENCV_DIR`为opencv编译安装的地址 + `LIB_DIR`为下载(`paddle_inference`文件夹)或者编译生成的Paddle预测库地址(`build/paddle_inference_install_dir`文件夹) + `CUDA_LIB_DIR`为cuda库文件地址,在docker中为`/usr/local/cuda/lib64` + `CUDNN_LIB_DIR`为cudnn库文件地址,在docker中为`/usr/lib/x86_64-linux-gnu/`。 + **如果希望预测时开启TensorRT加速功能,那么还需要修改`tools/build.sh`3处代码** + 1. 设置`DWITH_GPU=ON` + 2. 设置`DWITH_TENSORRT=ON` + 3. 设置`TENSORRT_DIR=/path/to/TensorRT-x.x.x.x` + + **以上路径都写绝对路径,不要写相对路径** + + +* 编译完成之后,会在`cpp_infer/build`文件夹下生成一个名为`ppvideo`的可执行文件。 + + +### 2.3 运行PaddleVideo C++预测demo + +运行方式: + +```bash +./build/ppvideo [--param1] [--param2] [...] +``` + +其中,`mode`为必选参数,表示选择的功能,取值范围['rec'],表示**视频识别**(更多功能会陆续加入)。 + + +##### 1. 调用视频识别: +```bash +# 调用PP-TSM识别 +./build/ppvideo rec \ +--rec_model_dir=../../inference/ppTSM \ +--inference_model_name=ppTSM \ +--video_dir=./example_video_dir \ +--num_seg=8 \ +--seg_len=1 + +# 调用PP-TSN识别 +./build/ppvideo rec \ +--rec_model_dir=../../inference/ppTSN \ +--inference_model_name=ppTSN \ +--video_dir=./example_video_dir \ +--num_seg=25 \ +--seg_len=1 +``` +更多参数如下: + +- 通用参数 + + | 参数名称 | 类型 | 默认参数 | 意义 | + | ------------- | ---- | --------------- | ------------------------------------------------------------ | + | use_gpu | bool | false | 是否使用GPU | + | gpu_id | int | 0 | GPU id,使用GPU时有效 | + | gpu_mem | int | 4000 | 申请的GPU内存 | + | cpu_threads | int | 10 | CPU预测时的线程数,在机器核数充足的情况下,该值越大,预测速度越快 | + | enable_mkldnn | bool | false | 是否使用mkldnn库 | + | use_tensorrt | bool | false | 是否使用tensorrt库 | + | precision | str | "fp32" | 使用fp32/fp16/uint8精度来预测 | + | benchmark | bool | true | 预测时是否开启benchmark,开启后会在最后输出配置、模型、耗时等信息。 | + + +- 视频识别模型相关 + + | 参数名称 | 类型 | 默认参数 | 意义 | + | -------------- | ------ | --------------------------------------------- | ------------------------------------ | + | video_dir | string | "../example_video_dir" | 存放将要识别的视频的文件夹路径 | + | rec_model_dir | string | "" | 存放导出的预测模型的文件夹路径 | + | inference_model_name | string | "ppTSM" | 预测模型的名称 | + | num_seg | int | 8 | 视频分段的段数 | + | seg_len | int | 1 | 视频每段抽取的帧数 | + | rec_batch_num | int | 1 | 模型预测时的batch size | + | char_list_file | str | "../../data/k400/Kinetics-400_label_list.txt" | 存放所有类别标号和对应名字的文本路径 | + +​ 以example_video_dir下的样例视频`example01.avi`为输入视频为例,最终屏幕上会输出检测结果如下。 + +```bash +[./inference/ppTSM] +[./deploy/cpp_infer/example_video_dir] +total videos num: 1 +./example_video_dir/example01.avi class: 5 archery score: 0.999556 +I1125 08:10:45.834288 13955 autolog.h:50] ----------------------- Config info ----------------------- +I1125 08:10:45.834458 13955 autolog.h:51] runtime_device: cpu +I1125 08:10:45.834467 13955 autolog.h:52] ir_optim: True +I1125 08:10:45.834475 13955 autolog.h:53] enable_memory_optim: True +I1125 08:10:45.834483 13955 autolog.h:54] enable_tensorrt: 0 +I1125 08:10:45.834518 13955 autolog.h:55] enable_mkldnn: False +I1125 08:10:45.834525 13955 autolog.h:56] cpu_math_library_num_threads: 10 +I1125 08:10:45.834532 13955 autolog.h:57] ----------------------- Data info ----------------------- +I1125 08:10:45.834540 13955 autolog.h:58] batch_size: 1 +I1125 08:10:45.834547 13955 autolog.h:59] input_shape: dynamic +I1125 08:10:45.834556 13955 autolog.h:60] data_num: 1 +I1125 08:10:45.834564 13955 autolog.h:61] ----------------------- Model info ----------------------- +I1125 08:10:45.834573 13955 autolog.h:62] model_name: rec +I1125 08:10:45.834579 13955 autolog.h:63] precision: fp32 +I1125 08:10:45.834586 13955 autolog.h:64] ----------------------- Perf info ------------------------ +I1125 08:10:45.834594 13955 autolog.h:65] Total time spent(ms): 2739 +I1125 08:10:45.834602 13955 autolog.h:67] preprocess_time(ms): 10.6524, inference_time(ms): 1269.55, postprocess_time(ms): 0.009118 +``` + +### 3 FAQ + +1. 编译demo过程中出现以下错误 + + ```shell + make[2]: *** No rule to make target '/usr/lib/x86_64-linux-gn/libcudnn.so', needed by 'ppvideo'. Stop. + make[2]: *** Waiting for unfinished jobs.... + [ 16%] Building CXX object CMakeFiles/ppvideo.dir/src/main.cpp.o + [ 50%] Building CXX object CMakeFiles/ppvideo.dir/src/preprocess_op.cpp.o + [ 50%] Building CXX object CMakeFiles/ppvideo.dir/src/postprocess_op.cpp.o + [ 83%] Building CXX object CMakeFiles/ppvideo.dir/src/utility.cpp.o + [ 83%] Building CXX object CMakeFiles/ppvideo.dir/src/video_rec.cpp.o + CMakeFiles/Makefile2:95: recipe for target 'CMakeFiles/ppvideo.dir/all' failed + make[1]: *** [CMakeFiles/ppvideo.dir/all] Error 2 + Makefile:83: recipe for target 'all' failed + make: *** [all] Error 2 + ``` + 可能是`CUDNN_LIB_DIR`设置的不对,导致找不到该目录下的`libcudnn.so`。 diff --git a/deploy/cpp_infer/readme_en.md b/deploy/cpp_infer/readme_en.md new file mode 100644 index 0000000000000000000000000000000000000000..1752c260f1e1feed778c212aad1e385a0ebd3715 --- /dev/null +++ b/deploy/cpp_infer/readme_en.md @@ -0,0 +1,316 @@ +English | [简体中文](./readme.md) + +# Server-side C++ prediction + +This chapter introduces the C++ deployment method of the PaddleVideo model. For the python prediction deployment method, please refer to the **Model Reasoning** chapter of the respective model. +C++ is better than python in terms of performance calculation. Therefore, in most CPU and GPU deployment scenarios, C++ deployment methods are mostly used. This section will introduce how to configure the C++ environment in the Linux (CPU/GPU) environment and complete it. +PaddleVideo model deployment. + +Before getting started, you need to install additional dependencies as follows: +```bash +python -m pip install [paddledet](git+https://github.com/LDOUBLEV/AutoLog) +``` + +## 1. Prepare the environment + +- For Linux environment, docker is recommended. + +- Windows environment, currently supports compilation based on `Visual Studio 2019 Community` (TODO) + +* This document mainly introduces the PaddleVideo C++ prediction process based on the Linux environment. If you need to perform C++ prediction based on the prediction library under Windows, please refer to [Windows Compilation Tutorial](./docs/windows_vs2019_build.md)(TODO) for the specific compilation method +* **The purpose of preparing the environment is to get the compiled opencv library and paddle prediction library**. + +### 1.1 Compile opencv library + +* First, you need to download the compressed package compiled from the source code in the Linux environment from the opencv official website, and unzip it into a folder. Take opencv3.4.7 as an example, the download command is as follows: + + ```bash + cd deploy/cpp_infer + wget https://github.com/opencv/opencv/archive/3.4.7.tar.gz + tar -xf 3.4.7.tar.gz + ``` + + After decompression, you can get the decompressed folder of `opencv-3.4.7` in the `deploy/cpp_infer` directory. + +* Install ffmpeg + + Opencv and ffmpeg can read the video normally under linux, otherwise it may encounter the situation that the number of video frames returns to 0 or no video frame can be read + + Using a relatively simple apt installation, the installation command is as follows: + + ```bash + apt-get update + + apt install libavformat-dev + apt install libavcodec-dev + apt install libswresample-dev + apt install libswscale-dev + apt install libavutil-dev + apt install libsdl1.2-dev + + apt-get install ffmpeg + ``` + +* To prepare to compile opencv, first enter the `opencv-3.4.7` folder, and then set the opencv source path `root_path` and the installation path `install_path`. The execution command is as follows: + + ```bash + cd opencv-3.4.7 + + root_path=$PWD # That is the absolute path of opencv-3.4.7 + install_path=${root_path}/opencv3 + + rm -rf build + mkdir build + cd build + + cmake .. \ + -DCMAKE_INSTALL_PREFIX=${install_path} \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DWITH_IPP=OFF \ + -DBUILD_IPP_IW=OFF \ + -DWITH_LAPACK=OFF \ + -DWITH_EIGEN=OFF \ + -DCMAKE_INSTALL_LIBDIR=lib64 \ + -DWITH_ZLIB=ON \ + -DBUILD_ZLIB=ON \ + -DWITH_JPEG=ON \ + -DBUILD_JPEG=ON \ + -DWITH_PNG=ON \ + -DBUILD_PNG=ON \ + -DWITH_TIFF=ON \ + -DBUILD_TIFF=ON \ + -DWITH_FFMPEG=ON + + make -j + make install + ``` + + After the completion of `make install`, opencv header files and library files will be generated in this folder, which will be used to compile the Video inference C++ code later. + + Finally, the installation path `install_path` will be used as the specified path, and a folder of `opencv3` will be obtained. The file structure is shown below. + + ```shell + opencv-3.4.7/ + ├── opencv3/ + │ ├── bin/ + │ ├── include/ + │ ├── lib/ + │ ├── lib64/ + │ └── share/ + ``` + +### 1.2 Download or compile Paddle prediction library + +There are two ways to obtain the Paddle prediction library, which will be described in detail below. + + +#### 1.2.1 Download and install directly + +* [Paddle prediction library official website](https://paddleinference.paddlepaddle.org.cn/v2.2/user_guides/download_lib.html) provides different cuda versions of Linux prediction libraries, you can Check and **select the appropriate prediction library version** on the official website (it is recommended to select the prediction library with paddle version>=2.0.1, and the prediction library of 2.2.2 is recommended). + +* Download and get a `paddle_inference.tgz` compressed package, and then unzip it into a folder, the command is as follows (taking the machine environment as gcc8.2 as an example): + + ```bash + wget https://paddle-inference-lib.bj.bcebos.com/2.2.2/cxx_c/Linux/GPU/x86-64_gcc8.2_avx_mkl_cuda10.1_cudnn7.6.5_trt6.0.1.5/paddle_inference.tgz + tar -xf paddle_inference.tgz + ``` + + Eventually, a subfolder of `paddle_inference/` will be generated in the current folder. + +#### 1.2.2 Prediction library source code compilation +* If you want to get the latest prediction library features, you can clone the latest code from Paddle github and compile the prediction library from source code. +* You can refer to [Paddle prediction library installation and compilation instructions](https://paddleinference.paddlepaddle.org.cn/user_guides/source_compile.html) instructions from github Obtain the Paddle code, and then compile it to generate the latest prediction library. The method of using git to get the code is as follows. + + ```shell + git clone https://github.com/PaddlePaddle/Paddle.git + git checkout release/2.2 + ``` + +* After entering the Paddle directory, the compilation method is as follows. + + ```shell + rm -rf build + mkdir build + cd build + + cmake .. \ + -DWITH_CONTRIB=OFF \ + -DWITH_MKL=ON \ + -DWITH_MKLDNN=ON \ + -DWITH_TESTING=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DWITH_INFERENCE_API_TEST=OFF \ + -DON_INFER=ON \ + -DWITH_PYTHON=ON + make -j + make inference_lib_dist -j4 # 4为编译时使用核数,可根据机器情况自行修改 + ``` + + You can refer to [documentation](https://www.paddlepaddle.org.cn/documentation/docs/zh/2.0/guides/05_inference_deployment/inference/build_and_install_lib_cn.html#congyuanmabianyi) for more introduction of compilation parameter options. + + +* After the compilation is complete, you can see the following files and folders are generated under the file `build/paddle_inference_install_dir/`. + + ``` + build/ + └── paddle_inference_install_dir/ + ├── CMakeCache.txt + ├── paddle/ + ├── third_party/ + └── version.txt + ``` + + Among them, `paddle` is the Paddle library required for C++ prediction, and `version.txt` contains the version information of the current prediction library. + +## 2. Compile and run the prediction demo + +### 2.1 Export the model as an inference model + +* This step is the same as the export prediction model under the python deployment mode. You can refer to the model prediction chapter of the respective model. Several related inference model files exported are used for model prediction. **Taking PP-TSM as an example**, the directory structure of the derived prediction model is as follows. + + ``` + inference/ + └── ppTSM/ + ├── ppTSM.pdiparams + ├── ppTSM.pdiparamsinfo + └── ppTSM.pdmodel + ``` + + +### 2.2 Compile PaddleVideo C++ prediction demo + +* Enter the `deploy/cpp_infer` directory and execute the following compile command + + ```shell + bash tools/build.sh + ``` + + The addresses of the Paddle C++ prediction library, opencv and other dependent libraries in `tools/build.sh` need to be replaced with the actual addresses on your own machine. + +* Specifically, you need to modify the environment path in `tools/build.sh`, the relevant content is as follows: + + ```shell + OPENCV_DIR=your_opencv_dir + LIB_DIR=your_paddle_inference_dir + CUDA_LIB_DIR=/usr/local/cuda/lib64 + CUDNN_LIB_DIR=/usr/lib/x86_64-linux-gnu/ + ``` + + The above parameters are as follows (the following path users can modify according to their own machine conditions) + + `OPENCV_DIR` is the address where opencv is compiled and installed + `LIB_DIR` is the download (`paddle_inference` folder) or the generated Paddle prediction library address (`build/paddle_inference_install_dir` folder) + `CUDA_LIB_DIR` is the address of the cuda library file, which is `/usr/local/cuda/lib64` in docker + `CUDNN_LIB_DIR` is the cudnn library file address, which is `/usr/lib/x86_64-linux-gnu/` in docker. + **If you want to enable TensorRT acceleration during prediction, you need to modify the code at `tools/build.sh`3** + 1. Set `DWITH_GPU=ON` + 2. Set `DWITH_TENSORRT=ON` + 3. Set `TENSORRT_DIR=/path/to/TensorRT-x.x.x.x` + + **The above paths are all absolute paths, do not use relative paths** + +* After the compilation is complete, an executable file named `ppvideo` will be generated in the `cpp_infer/build` folder. + + +### 2.3 Run PaddleVideo C++ prediction demo + +Operation mode: + +```bash +./build/ppvideo [--param1] [--param2] [...] +``` + +Among them, `mode` is a required parameter, which means the selected function, and the value range is ['rec'], which means **video recognition** (more functions will be added in succession). + + +##### 1. Call video recognition: + +```bash +# run PP-TSM inference +./build/ppvideo rec \ +--rec_model_dir=../../inference/ppTSM \ +--inference_model_name=ppTSM \ +--video_dir=./example_video_dir \ +--num_seg=8 \ +--seg_len=1 + +# run PP-TSN inference +./build/ppvideo rec \ +--rec_model_dir=../../inference/ppTSN \ +--inference_model_name=ppTSN \ +--video_dir=./example_video_dir \ +--num_seg=25 \ +--seg_len=1 +``` +More parameters are as follows: + +- General parameters + + | Parameter name | Type | Default parameter | Meaning | + | ------------- | ---- | --------------- | ------------------------------------------------------------ | + | use_gpu | bool | false | Whether to use GPU | + | gpu_id | int | 0 | GPU id, valid when using GPU | + | gpu_mem | int | 4000 | GPU memory requested | + | cpu_threads | int | 10 | The number of threads for CPU prediction. When the number of machine cores is sufficient, the larger the value, the faster the prediction speed | + | enable_mkldnn | bool | false | Whether to use mkldnn library | + | use_tensorrt | bool | false | Whether to use the tensorrt library | + | precision | str | "fp32" | Use fp32/fp16/uint8 precision to predict | + | benchmark | bool | true | Whether to enable benchmark during prediction, after enabling it, the configuration, model, time-consuming and other information will be output at the end. | + +- Video recognition model related + + | Parameter name | Type | Default parameter | Meaning | + | -------------- | ------ | --------------------------------------------- | ------------------------------------ | + | video_dir | string | "../example_video_dir" | The path of the folder where the video to be recognized is stored | + | rec_model_dir | string | "" | The folder path where the exported prediction model is stored | + | inference_model_name | string | "ppTSM" | The name of the model used in the prediction | + | num_seg | int | 8 | Number of video segments | + | seg_len | int | 1 | The number of frames extracted in each segment of the video | + | rec_batch_num | int | 1 | Batch size during model prediction | + | char_list_file | str | "../../data/k400/Kinetics-400_label_list.txt" | The text path for storing all category labels and corresponding names | + +​ Take the sample video `example01.avi` under example_video_dir as the input video as an example, the final screen will output the detection results as follows. + +```bash +[./inference/ppTSM] +[./deploy/cpp_infer/example_video_dir] +total videos num: 1 +./example_video_dir/example01.avi class: 5 archery score: 0.999556 +I1125 08:10:45.834288 13955 autolog.h:50] ----------------------- Config info ----------------------- +I1125 08:10:45.834458 13955 autolog.h:51] runtime_device: cpu +I1125 08:10:45.834467 13955 autolog.h:52] ir_optim: True +I1125 08:10:45.834475 13955 autolog.h:53] enable_memory_optim: True +I1125 08:10:45.834483 13955 autolog.h:54] enable_tensorrt: 0 +I1125 08:10:45.834518 13955 autolog.h:55] enable_mkldnn: False +I1125 08:10:45.834525 13955 autolog.h:56] cpu_math_library_num_threads: 10 +I1125 08:10:45.834532 13955 autolog.h:57] ----------------------- Data info ----------------------- +I1125 08:10:45.834540 13955 autolog.h:58] batch_size: 1 +I1125 08:10:45.834547 13955 autolog.h:59] input_shape: dynamic +I1125 08:10:45.834556 13955 autolog.h:60] data_num: 1 +I1125 08:10:45.834564 13955 autolog.h:61] ----------------------- Model info ----------------------- +I1125 08:10:45.834573 13955 autolog.h:62] model_name: rec +I1125 08:10:45.834579 13955 autolog.h:63] precision: fp32 +I1125 08:10:45.834586 13955 autolog.h:64] ----------------------- Perf info ------------------------ +I1125 08:10:45.834594 13955 autolog.h:65] Total time spent(ms): 2739 +I1125 08:10:45.834602 13955 autolog.h:67] preprocess_time(ms): 10.6524, inference_time(ms): 1269.55, postprocess_time(ms): 0.009118 +``` + +### 3 FAQ + +1. The following error occurred during the compilation of the demo + + ```shell + make[2]: *** No rule to make target '/usr/lib/x86_64-linux-gn/libcudnn.so', needed by 'ppvideo'. Stop. + make[2]: *** Waiting for unfinished jobs.... + [ 16%] Building CXX object CMakeFiles/ppvideo.dir/src/main.cpp.o + [ 50%] Building CXX object CMakeFiles/ppvideo.dir/src/preprocess_op.cpp.o + [ 50%] Building CXX object CMakeFiles/ppvideo.dir/src/postprocess_op.cpp.o + [83%] Building CXX object CMakeFiles/ppvideo.dir/src/utility.cpp.o + [ 83%] Building CXX object CMakeFiles/ppvideo.dir/src/video_rec.cpp.o + CMakeFiles/Makefile2:95: recipe for target 'CMakeFiles/ppvideo.dir/all' failed + make[1]: *** [CMakeFiles/ppvideo.dir/all] Error 2 + Makefile:83: recipe for target 'all' failed + make: *** [all] Error 2 + ```` + It may be that `CUDNN_LIB_DIR` is set incorrectly, resulting in that `libcudnn.so` in this directory cannot be found. diff --git a/deploy/cpp_infer/src/main.cpp b/deploy/cpp_infer/src/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e91ea63ca9832fe3014a5fa5744355a32f5ec47d --- /dev/null +++ b/deploy/cpp_infer/src/main.cpp @@ -0,0 +1,173 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "glog/logging.h" +#include "omp.h" +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include "auto_log/autolog.h" + +// general parameters +DEFINE_bool(use_gpu, false, "Infering with GPU or CPU."); +DEFINE_int32(gpu_id, 0, "Device id of GPU to execute."); +DEFINE_int32(gpu_mem, 4000, "GPU id when infering with GPU."); +DEFINE_int32(cpu_threads, 10, "Num of threads with CPU."); +DEFINE_bool(enable_mkldnn, false, "Whether use mkldnn with CPU."); +DEFINE_bool(use_tensorrt, false, "Whether use tensorrt."); +DEFINE_string(precision, "fp32", "Precision be one of fp32/fp16/int8."); +DEFINE_bool(benchmark, true, "Whether to log and report benchmark information during inference."); + + +// video recognition related +DEFINE_string(video_dir, "", "Dir of input video(s)."); +DEFINE_string(rec_model_dir, "../example_video_dir", "Path of video rec inference model."); +DEFINE_string(inference_model_name, "ppTSM", "The name of the model used in the prediction."); +DEFINE_int32(num_seg, 8, "number of frames input to model, which are extracted from a video."); +DEFINE_int32(seg_len, 1, "number of frames from a segment."); +DEFINE_int32(rec_batch_num, 1, "rec_batch_num."); +DEFINE_string(char_list_file, "../../data/k400/Kinetics-400_label_list.txt", "Path of dictionary."); + + +using namespace std; +using namespace cv; +using namespace PaddleVideo; + + +static bool PathExists(const std::string& path) +{ +#ifdef _WIN32 + struct _stat buffer; + return (_stat(path.c_str(), &buffer) == 0); +#else + struct stat buffer; + return (stat(path.c_str(), &buffer) == 0); +#endif // !_WIN32 +} + + +int main_rec(std::vector &cv_all_video_names) +{ + std::vector time_info = {0, 0, 0}; // Statement time statistics vector + VideoRecognizer rec(FLAGS_rec_model_dir, FLAGS_inference_model_name, FLAGS_use_gpu, FLAGS_num_seg, + FLAGS_rec_batch_num, FLAGS_gpu_id, + FLAGS_gpu_mem, FLAGS_cpu_threads, + FLAGS_enable_mkldnn, FLAGS_char_list_file, + FLAGS_use_tensorrt, FLAGS_precision); // Instantiate a video recognition object + + int batch_num = FLAGS_rec_batch_num; + for (int i = 0, n = cv_all_video_names.size(); i < n; i += batch_num) // Process each video + { + int start_idx = i; + int end_idx = min(i + batch_num, n); + std::vector > frames_batch; + for (int j = start_idx; j < end_idx; ++j) + { + std::vector frames = Utility::SampleFramesFromVideo(cv_all_video_names[i], FLAGS_num_seg, FLAGS_seg_len); + frames_batch.emplace_back(frames); + } + std::vector rec_times; // Initialization time consumption statistics + + // Take the read several video frames and send them to the run method of the recognition class to predict + rec.Run(std::vector(cv_all_video_names.begin() + start_idx, cv_all_video_names.begin() + end_idx), frames_batch, &rec_times); + + time_info[0] += rec_times[0]; + time_info[1] += rec_times[1]; + time_info[2] += rec_times[2]; + } + if (FLAGS_benchmark) + { + AutoLogger autolog("rec", + FLAGS_use_gpu, + FLAGS_use_tensorrt, + FLAGS_enable_mkldnn, + FLAGS_cpu_threads, + FLAGS_rec_batch_num, + "dynamic", + FLAGS_precision, + time_info, + cv_all_video_names.size()); // Generate detailed information on the run + autolog.report(); // Print running details + } + + return 0; +} + + +void check_params(char* mode) +{ + if (strcmp(mode, "rec") == 0) + { + std::cout << "[" << FLAGS_rec_model_dir << "]" << std::endl; + std::cout << "[" << FLAGS_video_dir << "]" << std::endl; + if (FLAGS_rec_model_dir.empty() || FLAGS_video_dir.empty()) + { + std::cout << "Usage[rec]: ./ppvideo --rec_model_dir=/PATH/TO/REC_INFERENCE_MODEL/ " + << "--video_dir=/PATH/TO/INPUT/VIDEO/" << std::endl; + exit(1); + } + } + if (FLAGS_precision != "fp32" && FLAGS_precision != "fp16" && FLAGS_precision != "int8") + { + cout << "precison should be 'fp32'(default), 'fp16' or 'int8'. " << endl; + exit(1); + } +} + + +int main(int argc, char **argv) +{ + if (argc <= 1 || (strcmp(argv[1], "rec") != 0)) //Get user input and check + { + std::cout << "Please choose one mode of [rec] !" << std::endl; + return -1; + } + std::cout << "mode: " << argv[1] << endl; // Type of inference task required for output + + // Parsing command-line + google::ParseCommandLineFlags(&argc, &argv, true); + check_params(argv[1]); + + if (!PathExists(FLAGS_video_dir)) // Determine whether the directory where the video exists + { + std::cerr << "[ERROR] video path not exist! video_dir: " << FLAGS_video_dir << endl; + exit(1); + } + + std::vector cv_all_video_names; // Store all video paths + + cv::glob(FLAGS_video_dir, cv_all_video_names); // Search all videos under FLAGS_video_dir, save in cv_all_video_names + std::cout << "total videos num: " << cv_all_video_names.size() << endl; // 输出搜索到的视频个数 + + if (strcmp(argv[1], "rec") == 0) + { + return main_rec(cv_all_video_names); // Output the number of videos searched + } + return 0; +} diff --git a/deploy/cpp_infer/src/postprocess_op.cpp b/deploy/cpp_infer/src/postprocess_op.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fdc6c5a6e737bf9ea4739a3e40bb24f081cc96db --- /dev/null +++ b/deploy/cpp_infer/src/postprocess_op.cpp @@ -0,0 +1,50 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace PaddleVideo +{ + void Softmax::Inplace_Run(const std::vector::iterator &_begin, const std::vector::iterator &_end) + { + const float max_value = *std::max_element(_begin, _end); + float denominator = 0.0f; + for (auto it = _begin; it != _end; ++it) + { + *it = std::exp((*it) - max_value); + denominator += (*it); + } + for (auto it = _begin; it != _end; ++it) + { + *it /= denominator; + } + } + std::vector Softmax::Run(const std::vector::iterator &_begin, const std::vector::iterator &_end) + { + std::vector prob(_begin, _end); + const float max_value = *std::max_element(prob.begin(), prob.end()); + float denominator = 0.0f; + for (auto it = _begin, it_p = prob.begin(); it != _end; ++it, ++it_p) + { + (*it_p) = std::exp((*it) - max_value); + denominator += (*it_p); + } + for (auto it = prob.begin(); it != prob.end(); ++it) + { + (*it) /= denominator; + } + return prob; + } + +} // namespace PaddleVideo diff --git a/deploy/cpp_infer/src/preprocess_op.cpp b/deploy/cpp_infer/src/preprocess_op.cpp new file mode 100644 index 0000000000000000000000000000000000000000..951f7b02c3d0b200e0dee5ce2dfb35eb991c834e --- /dev/null +++ b/deploy/cpp_infer/src/preprocess_op.cpp @@ -0,0 +1,135 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "opencv2/core.hpp" +#include "opencv2/imgcodecs.hpp" +#include "opencv2/imgproc.hpp" +#include "paddle_api.h" +#include "paddle_inference_api.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + + +namespace PaddleVideo +{ + + void Permute::Run(const cv::Mat *im, float *data) + { + int rh = im->rows; + int rw = im->cols; + int rc = im->channels(); + for (int i = 0; i < rc; ++i) + { + // Extract the i-th channel of im and write it into the array with (data + i * rh * rw) as the starting address + cv::extractChannel(*im, cv::Mat(rh, rw, CV_32FC1, data + i * rh * rw), rc - 1 - i); + } + } + + void Normalize::Run(cv::Mat *im, const std::vector &mean, + const std::vector &scale, const bool is_scale) + { + double e = 1.0; + if (is_scale) + { + e /= 255.0; + } + (*im).convertTo(*im, CV_32FC3, e); + std::vector bgr_channels(3); + cv::split(*im, bgr_channels); + for (auto i = 0; i < bgr_channels.size(); i++) + { + bgr_channels[i].convertTo(bgr_channels[i], CV_32FC1, 1.0 / scale[i], (0.0 - mean[i]) / scale[i]); + } + cv::merge(bgr_channels, *im); + } + + void Scale::Run(const cv::Mat &img, cv::Mat &resize_img, bool use_tensorrt, const int &short_size) + { + int h = img.rows; + int w = img.cols; + if ((w <= h && w == short_size) || (h <= w && h == short_size)) + { + img.copyTo(resize_img); + } + else + { + int oh, ow; + if (w < h) + { + ow = short_size; + oh = h * ow / w; + } + else + { + oh = short_size; + ow = w * oh / h; + } + cv::resize(img, resize_img, cv::Size(ow, oh), 0.0f, 0.0f, cv::INTER_LINEAR); + } + } + + void CenterCrop::Run(const cv::Mat &img, cv::Mat &crop_img, bool use_tensorrt, const int &target_size) + { + int h = img.rows; + int w = img.cols; + int crop_h = target_size; + int crop_w = target_size; + if (w < crop_w || h < crop_h) + { + printf("[Error] image width (%d) and height (%d) should be larger than crop size (%d)", + w, h, target_size); + } + else + { + int x1 = (w - crop_w) / 2; + int y1 = (h - crop_h) / 2; + crop_img = img(cv::Rect(x1, y1, crop_w, crop_h)); + } + } + + void TenCrop::Run(const cv::Mat &img, std::vector &crop_imgs, const int &begin_index, bool use_tensorrt, const int &target_size) + { + int h = img.rows; + int w = img.cols; + int crop_h = target_size; + int crop_w = target_size; + int w_step = (w - crop_w) / 4; + int h_step = (h - crop_h) / 4; + pairoffsets[5] = + { + {0, 0}, + {4 * w_step, 0}, + {0, 4 * h_step}, + {4 * w_step, 4 * h_step}, + {2 * w_step, 2 * h_step} + }; + for (int i = 0; i < 5; ++i) + { + const int &j = i * 2; + const int &x1 = offsets[i].first; + const int &y1 = offsets[i].second; + crop_imgs[begin_index + j] = img(cv::Rect(x1, y1, crop_w, crop_h)); // cropped + cv::flip(img(cv::Rect(x1, y1, crop_w, crop_h)), crop_imgs[begin_index + j + 1], 0); // cropped + } + } +} // namespace PaddleVideo diff --git a/deploy/cpp_infer/src/utility.cpp b/deploy/cpp_infer/src/utility.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b95988ff11a4c0aa629d5cf1654dc9fa99d1986c --- /dev/null +++ b/deploy/cpp_infer/src/utility.cpp @@ -0,0 +1,192 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include + +namespace PaddleVideo +{ + + std::vector Utility::ReadDict(const std::string &path) + { + std::ifstream in(path); + std::string line; + std::vector m_vec; + if (in) + { + while (getline(in, line)) + { + m_vec.push_back(line); + } + } + else + { + std::cout << "no such label file: " << path << ", exit the program..." + << std::endl; + exit(1); + } + return m_vec; // Use fstream to read the category list and return with vector + } + + void Utility::GetAllFiles(const char *dir_name, std::vector &all_inputs) + { + if (NULL == dir_name) + { + std::cout << " dir_name is null ! " << std::endl; + return; + } + struct stat s; + lstat(dir_name, &s); + if (!S_ISDIR(s.st_mode)) + { + std::cout << "dir_name is not a valid directory !" << std::endl; + all_inputs.push_back(dir_name); + return; + } + else + { + struct dirent *filename; // return value for readdir() + DIR *dir; // return value for opendir() + dir = opendir(dir_name); + if (NULL == dir) + { + std::cout << "Can not open dir " << dir_name << std::endl; + return; + } + std::cout << "Successfully opened the dir !" << std::endl; + while ((filename = readdir(dir)) != NULL) + { + if (strcmp(filename->d_name, ".") == 0 || + strcmp(filename->d_name, "..") == 0) + continue; + // img_dir + std::string("/") + all_inputs[0]; + all_inputs.push_back(dir_name + std::string("/") + + std::string(filename->d_name)); + } + } + } + + cv::Mat Utility::GetRotateCropImage(const cv::Mat &srcimage, std::vector> box) + { + cv::Mat image; + srcimage.copyTo(image); + std::vector> points = box; + + int x_collect[4] = {box[0][0], box[1][0], box[2][0], box[3][0]}; + int y_collect[4] = {box[0][1], box[1][1], box[2][1], box[3][1]}; + int left = int(*std::min_element(x_collect, x_collect + 4)); + int right = int(*std::max_element(x_collect, x_collect + 4)); + int top = int(*std::min_element(y_collect, y_collect + 4)); + int bottom = int(*std::max_element(y_collect, y_collect + 4)); + + cv::Mat img_crop; + image(cv::Rect(left, top, right - left, bottom - top)).copyTo(img_crop); + + for (int i = 0; i < points.size(); i++) + { + points[i][0] -= left; + points[i][1] -= top; + } + + int img_crop_width = int(sqrt(pow(points[0][0] - points[1][0], 2) + + pow(points[0][1] - points[1][1], 2))); + int img_crop_height = int(sqrt(pow(points[0][0] - points[3][0], 2) + + pow(points[0][1] - points[3][1], 2))); + + cv::Point2f pts_std[4]; + pts_std[0] = cv::Point2f(0., 0.); + pts_std[1] = cv::Point2f(img_crop_width, 0.); + pts_std[2] = cv::Point2f(img_crop_width, img_crop_height); + pts_std[3] = cv::Point2f(0.f, img_crop_height); + + cv::Point2f pointsf[4]; + pointsf[0] = cv::Point2f(points[0][0], points[0][1]); + pointsf[1] = cv::Point2f(points[1][0], points[1][1]); + pointsf[2] = cv::Point2f(points[2][0], points[2][1]); + pointsf[3] = cv::Point2f(points[3][0], points[3][1]); + + cv::Mat M = cv::getPerspectiveTransform(pointsf, pts_std); + + cv::Mat dst_img; + cv::warpPerspective(img_crop, dst_img, M, + cv::Size(img_crop_width, img_crop_height), + cv::BORDER_REPLICATE); + + if (float(dst_img.rows) >= float(dst_img.cols) * 1.5) + { + cv::Mat srcCopy = cv::Mat(dst_img.rows, dst_img.cols, dst_img.depth()); + cv::transpose(dst_img, srcCopy); + cv::flip(srcCopy, srcCopy, 0); + return srcCopy; + } + else + { + return dst_img; + } + } + + std::vector Utility::SampleFramesFromVideo(const std::string &VideoPath, const int &num_seg, const int &seg_len) + { + cv::VideoCapture capture(VideoPath); // Create a video object + if (!capture.isOpened()) + { + printf("[Error] video cannot be opened, please check the video [%s]\n", VideoPath.c_str()); + capture.release(); + exit(1); + } + + int frames_len = capture.get(cv::CAP_PROP_FRAME_COUNT); // Get the total number of video frames + + int average_dur = int(frames_len / num_seg); + std::vector frames_idx; + + for (int i = 0; i < num_seg; ++i) + { + int idx = 0; + if (average_dur >= seg_len) + { + idx = (average_dur - 1) / 2; + idx += i * average_dur; + } + else if (average_dur >= 1) + { + idx += i * average_dur; + } + else + { + idx = i; + } + for (int j = idx; j < idx + seg_len; ++j) + { + frames_idx.emplace_back(j % frames_len); + } + } + std::vector sampled_frames; + cv::Mat frame; // Create an object for storing sampled frames + for (int i = 0; i < num_seg; ++i) + { + const int &frame_idx = frames_idx[i]; + capture.set(cv::CAP_PROP_POS_FRAMES, frame_idx); // Set to frame_idx frame + capture >> frame; + sampled_frames.push_back(frame); + } + capture.release(); // Release the video object + return sampled_frames; + } +} // namespace PaddleVideo diff --git a/deploy/cpp_infer/src/video_rec.cpp b/deploy/cpp_infer/src/video_rec.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5f7e19b97bde01257fe760052c5e6f85392e061e --- /dev/null +++ b/deploy/cpp_infer/src/video_rec.cpp @@ -0,0 +1,304 @@ +// Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace PaddleVideo +{ + void VideoRecognizer::Run(const std::vector &frames_batch_path, const std::vector > &frames_batch, std::vector *times) + { + // Copy parameters to the function + int real_batch_num = frames_batch.size(); + + std::vector srcframes(real_batch_num * this->num_seg, cv::Mat()); + + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + frames_batch[i][j].copyTo(srcframes[i * this->num_seg + j]); + } + } + + auto preprocess_start = std::chrono::steady_clock::now(); + /* Preprocess */ + std::vector resize_frames; + std::vector crop_frames; + std::vector input; + int num_views = 1; + + if (this->inference_model_name == "ppTSM") + { + num_views = 1; + // 1. Scale + resize_frames = std::vector(real_batch_num * this->num_seg, cv::Mat()); + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + this->scale_op_.Run(srcframes[i * this->num_seg + j], resize_frames[i * this->num_seg + j], this->use_tensorrt_, 256); + } + } + + // 2. CenterCrop + crop_frames = std::vector(real_batch_num * num_views * this->num_seg, cv::Mat()); + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + this->centercrop_op_.Run(resize_frames[i * this->num_seg + j], crop_frames[i * this->num_seg + j], this->use_tensorrt_, 224); + } + } + + // 3. Normalization(inplace operation) + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + for (int k = 0; k < num_views; ++k) + { + this->normalize_op_.Run(&crop_frames[i * num_views * this->num_seg + j * num_views + k], this->mean_, this->scale_, this->is_scale_); + } + } + } + + // 4. Image2Array + int rh = crop_frames[0].rows; + int rw = crop_frames[0].cols; + int rc = crop_frames[0].channels(); + input = std::vector(real_batch_num * num_views * this->num_seg * crop_frames[0].rows * crop_frames[0].cols * rc, 0.0f); + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + for (int k = 0; k < num_views; ++k) + { + this->permute_op_.Run(&crop_frames[i * num_views * this->num_seg + j * num_views + k], input.data() + (i * num_views * this->num_seg + j * num_views + k) * (rh * rw * rc)); + } + } + } + } + else if(this->inference_model_name == "ppTSN") + { + num_views = 10; + // 1. Scale + resize_frames = std::vector(real_batch_num * this->num_seg, cv::Mat()); + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + this->scale_op_.Run(srcframes[i * this->num_seg + j], resize_frames[i * this->num_seg + j], this->use_tensorrt_, 256); + } + } + + // 2. TenCrop + crop_frames = std::vector(real_batch_num * this->num_seg * num_views, cv::Mat()); + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + this->tencrop_op_.Run(resize_frames[i * this->num_seg + j], crop_frames, (i * this->num_seg + j) * num_views, this->use_tensorrt_, 224); + } + } + + // 3. Normalization(inplace operation) + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + for (int k = 0; k < num_views; ++k) + { + this->normalize_op_.Run(&crop_frames[i * this->num_seg * num_views + j * num_views + k], this->mean_, this->scale_, this->is_scale_); + } + } + } + + // 4. Image2Array + int rh = crop_frames[0].rows; + int rw = crop_frames[0].cols; + int rc = crop_frames[0].channels(); + input = std::vector(real_batch_num * this->num_seg * num_views * crop_frames[0].rows * crop_frames[0].cols * rc, 0.0f); + for (int i = 0; i < real_batch_num; ++i) + { + for (int j = 0; j < this->num_seg; ++j) + { + for (int k = 0; k < num_views; ++k) + { + this->permute_op_.Run(&crop_frames[i * this->num_seg * num_views + j * num_views + k], input.data() + (i * this->num_seg * num_views + j * num_views + k) * (rh * rw * rc)); + } + } + } + } + else + { + throw "[Error] Not implemented yet"; + } + auto preprocess_end = std::chrono::steady_clock::now(); + + /* Inference */ + auto input_names = this->predictor_->GetInputNames(); + auto input_t = this->predictor_->GetInputHandle(input_names[0]); + input_t->Reshape({real_batch_num * num_views * this->num_seg, 3, crop_frames[0].rows, crop_frames[0].cols}); + auto inference_start = std::chrono::steady_clock::now(); + input_t->CopyFromCpu(input.data()); + this->predictor_->Run(); // Use the inference library to predict + + std::vector predict_batch; + auto output_names = this->predictor_->GetOutputNames(); + auto output_t = this->predictor_->GetOutputHandle(output_names[0]); + auto predict_shape = output_t->shape(); + + // Get the number of class + int class_num = predict_shape[1]; + + int out_numel = std::accumulate(predict_shape.begin(), predict_shape.end(), 1, std::multiplies()); + predict_batch.resize(out_numel); // NxC + output_t->CopyToCpu(predict_batch.data()); // Copy the model output to predict_batch + + // Convert output (logits) into probabilities + for (int i = 0; i < real_batch_num; ++i) + { + this->softmax_op_.Inplace_Run(predict_batch.begin() + i * class_num, predict_batch.begin() + (i + 1) * class_num); + } + + auto inference_end = std::chrono::steady_clock::now(); + + // output decode + auto postprocess_start = std::chrono::steady_clock::now(); + std::vector str_res; + std::vectorscores; + + for (int i = 0; i < real_batch_num; ++i) + { + int argmax_idx = int(Utility::argmax(predict_batch.begin() + i * class_num, predict_batch.begin() + (i + 1) * class_num)); + float score = predict_batch[argmax_idx]; + scores.push_back(score); + str_res.push_back(this->label_list_[argmax_idx]); + } + auto postprocess_end = std::chrono::steady_clock::now(); + for (int i = 0; i < str_res.size(); i++) + { + std::cout << frames_batch_path[i] << "\tclass: " << str_res[i] << "\tscore: " << scores[i] << endl; + } + + std::chrono::duration preprocess_diff = preprocess_end - preprocess_start; + times->push_back(double(preprocess_diff.count() * 1000)); + std::chrono::duration inference_diff = inference_end - inference_start; + times->push_back(double(inference_diff.count() * 1000)); + std::chrono::duration postprocess_diff = postprocess_end - postprocess_start; + times->push_back(double(postprocess_diff.count() * 1000)); + } + + void VideoRecognizer::LoadModel(const std::string &model_dir) + { + // AnalysisConfig config; + paddle_infer::Config config; + config.SetModel(model_dir + "/" + this->inference_model_name + ".pdmodel", + model_dir + "/" + this->inference_model_name + ".pdiparams"); + + if (this->use_gpu_) + { + config.EnableUseGpu(this->gpu_mem_, this->gpu_id_); + if (this->use_tensorrt_) + { + auto precision = paddle_infer::Config::Precision::kFloat32; + if (this->precision_ == "fp16") + { + precision = paddle_infer::Config::Precision::kHalf; + } + else if (this->precision_ == "int8") + { + precision = paddle_infer::Config::Precision::kInt8; + } + + if (this->inference_model_name == "ppTSM" || this->inference_model_name == "TSM") + { + config.EnableTensorRtEngine( + 1 << 30, // workspaceSize + this->rec_batch_num * this->num_seg * 1, // maxBatchSize + 3, // minSubgraphSize + precision, // precision + false,// useStatic + false //useCalibMode + ); + } + else if(this->inference_model_name == "ppTSN" || this->inference_model_name == "TSN") + { + config.EnableTensorRtEngine( + 1 << 30, + this->rec_batch_num * this->num_seg * 10, + 3, // minSubgraphSize + precision,// precision + false,// useStatic + false //useCalibMode + ); + } + else + { + config.EnableTensorRtEngine( + 1 << 30, // workspaceSize + this->rec_batch_num, // maxBatchSize + 3, // minSubgraphSize + precision,// precision + false,// useStatic + false //useCalibMode + ); + } + + std::cout << "Enable TensorRT is: " << config.tensorrt_engine_enabled() << std::endl; + + /* some model dose not suppport dynamic shape with TRT, deactivate it by default */ + + // std::map > min_input_shape = + // { + // {"data_batch_0", {1, this->num_seg, 3, 1, 1}} + // }; + // std::map > max_input_shape = + // { + // {"data_batch_0", {1, this->num_seg, 3, 256, 256}} + // }; + // std::map > opt_input_shape = + // { + // {"data_batch_0", {this->rec_batch_num, this->num_seg, 3, 224, 224}} + // }; + + // config.SetTRTDynamicShapeInfo(min_input_shape, max_input_shape, + // opt_input_shape); + } + } + else + { + config.DisableGpu(); + if (this->use_mkldnn_) + { + config.EnableMKLDNN(); + // cache 10 different shapes for mkldnn to avoid memory leak + config.SetMkldnnCacheCapacity(10); + } + config.SetCpuMathLibraryNumThreads(this->cpu_math_library_num_threads_); + } + + config.SwitchUseFeedFetchOps(false); + // true for multiple input + config.SwitchSpecifyInputNames(true); + + config.SwitchIrOptim(true); + + config.EnableMemoryOptim(); + config.DisableGlogInfo(); + + this->predictor_ = CreatePredictor(config); + } + +} // namespace PaddleVideo diff --git a/deploy/cpp_infer/tools/build.sh b/deploy/cpp_infer/tools/build.sh new file mode 100644 index 0000000000000000000000000000000000000000..c04ede0912abe934613489db43ca490c6b17d51b --- /dev/null +++ b/deploy/cpp_infer/tools/build.sh @@ -0,0 +1,22 @@ +OPENCV_DIR=your_opencv_dir +LIB_DIR=your_paddle_inference_dir +CUDA_LIB_DIR=your_cuda_lib_dir +CUDNN_LIB_DIR=your_cudnn_lib_dir +TENSORRT_DIR=your_tensorRT_dir + +BUILD_DIR=build +rm -rf ${BUILD_DIR} +mkdir ${BUILD_DIR} +cd ${BUILD_DIR} +cmake .. \ + -DPADDLE_LIB=${LIB_DIR} \ + -DWITH_MKL=ON \ + -DWITH_GPU=OFF \ + -DWITH_STATIC_LIB=OFF \ + -DWITH_TENSORRT=OFF \ + -DOPENCV_DIR=${OPENCV_DIR} \ + -DCUDNN_LIB=${CUDNN_LIB_DIR} \ + -DCUDA_LIB=${CUDA_LIB_DIR} \ + -DTENSORRT_DIR=${TENSORRT_DIR} \ + +make -j diff --git a/deploy/paddle2onnx/predict_onnx.py b/deploy/paddle2onnx/predict_onnx.py new file mode 100644 index 0000000000000000000000000000000000000000..47a223cd45afe17fd30eecf9eb342077a793b421 --- /dev/null +++ b/deploy/paddle2onnx/predict_onnx.py @@ -0,0 +1,171 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import sys +from os import path as osp + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../../tools'))) + +from utils import build_inference_helper, get_config + + +def parse_args(): + def str2bool(v): + return v.lower() in ("true", "t", "1") + + # general params + parser = argparse.ArgumentParser("PaddleVideo Inference model script") + parser.add_argument('-c', + '--config', + type=str, + default='configs/example.yaml', + help='config file path') + parser.add_argument("-i", "--input_file", type=str, help="input file path") + parser.add_argument("--onnx_file", type=str, help="onnx model file path") + + # params for onnx predict + parser.add_argument("-b", "--batch_size", type=int, default=1) + parser.add_argument("--use_gpu", + type=str2bool, + default=False, + help="set to False when using onnx") + parser.add_argument("--precision", type=str, default="fp32") + parser.add_argument("--ir_optim", type=str2bool, default=True) + parser.add_argument("--enable_benchmark", + type=str2bool, + default=False, + help="set to False when using onnx") + parser.add_argument("--cpu_threads", type=int, default=4) + + return parser.parse_args() + + +def create_onnx_predictor(args, cfg=None): + import onnxruntime as ort + onnx_file = args.onnx_file + config = ort.SessionOptions() + if args.use_gpu: + raise ValueError( + "onnx inference now only supports cpu! please set `use_gpu` to False." + ) + else: + config.intra_op_num_threads = args.cpu_threads + if args.ir_optim: + config.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + predictor = ort.InferenceSession(onnx_file, sess_options=config) + return config, predictor + + +def parse_file_paths(input_path: str) -> list: + if osp.isfile(input_path): + files = [ + input_path, + ] + else: + files = os.listdir(input_path) + files = [ + file for file in files + if (file.endswith(".avi") or file.endswith(".mp4")) + ] + files = [osp.join(input_path, file) for file in files] + return files + + +def main(): + """predict using onnx model + """ + args = parse_args() + cfg = get_config(args.config, show=False) + + model_name = cfg.model_name + + print(f"Inference model({model_name})...") + InferenceHelper = build_inference_helper(cfg.INFERENCE) + + inference_config, predictor = create_onnx_predictor(args) + + # get input_tensor and output_tensor + input_names = predictor.get_inputs()[0].name + output_names = predictor.get_outputs()[0].name + + # get the absolute file path(s) to be processed + files = parse_file_paths(args.input_file) + if args.enable_benchmark: + test_video_num = 12 + num_warmup = 3 + # instantiate auto log + try: + import auto_log + except ImportError as e: + print(f"{e}, [git+https://github.com/LDOUBLEV/AutoLog] " + f"package and it's dependencies is required for " + f"python-inference when enable_benchmark=True.") + pid = os.getpid() + autolog = auto_log.AutoLogger( + model_name=cfg.model_name, + model_precision=args.precision, + batch_size=args.batch_size, + data_shape="dynamic", + save_path="./output/auto_log.lpg", + inference_config=inference_config, + pids=pid, + process_name=None, + gpu_ids=None, + time_keys=['preprocess_time', 'inference_time', 'postprocess_time'], + warmup=num_warmup) + files = [args.input_file for _ in range(test_video_num + num_warmup)] + + # Inferencing process + batch_num = args.batch_size + for st_idx in range(0, len(files), batch_num): + ed_idx = min(st_idx + batch_num, len(files)) + + # auto log start + if args.enable_benchmark: + autolog.times.start() + + # Pre process batched input + batched_inputs = InferenceHelper.preprocess_batch(files[st_idx:ed_idx]) + + # get pre process time cost + if args.enable_benchmark: + autolog.times.stamp() + + # run inference + batched_outputs = predictor.run( + output_names=[output_names], + input_feed={input_names: batched_inputs[0]}) + + # get inference process time cost + if args.enable_benchmark: + autolog.times.stamp() + + InferenceHelper.postprocess(batched_outputs, not args.enable_benchmark) + + # get post process time cost + if args.enable_benchmark: + autolog.times.end(stamp=True) + + # time.sleep(0.01) # sleep for T4 GPU + + # report benchmark log if enabled + if args.enable_benchmark: + autolog.report() + + +if __name__ == "__main__": + main() diff --git a/deploy/paddle2onnx/readme.md b/deploy/paddle2onnx/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..04bb9e77d887f8b0ce4615b8351ab883d42c3b2f --- /dev/null +++ b/deploy/paddle2onnx/readme.md @@ -0,0 +1,70 @@ +# paddle2onnx 模型转化与预测 + +本章节介绍 PP-TSN 模型如何转化为 ONNX 模型,并基于 ONNX 引擎预测。 + +## 1. 环境准备 + +需要准备 Paddle2ONNX 模型转化环境,和 ONNX 模型预测环境。 + +Paddle2ONNX 支持将 PaddlePaddle 模型格式转化到 ONNX 模型格式,算子目前稳定支持导出 ONNX Opset 9~11,部分Paddle算子支持更低的ONNX Opset转换。 +更多细节可参考 [Paddle2ONNX](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/README_zh.md) + +- 安装 Paddle2ONNX +```bash +python3.7 -m pip install paddle2onnx +``` + +- 安装 ONNXRuntime +```bash +# 建议安装 1.9.0 版本,可根据环境更换版本号 +python3.7 -m pip install onnxruntime==1.9.0 +``` + +## 2. 模型转换 + +- PP-TSN inference模型下载 + + ```bash + # 下载inference模型到PaddleVideo/inference/ppTSN/ 目录下 + mkdir -p ./inference + wget -P ./inference/ https://videotag.bj.bcebos.com/PaddleVideo-release2.3/ppTSN.zip + + # 解压inference模型 + pushd ./inference + unzip ppTSN.zip + popd + ``` + +- 模型转换 + + 使用 Paddle2ONNX 将 Paddle inference模型转换为 ONNX 格式模型: + + ```bash + paddle2onnx \ + --model_dir=./inference/ppTSN \ + --model_filename=ppTSN.pdmodel \ + --params_filename=ppTSN.pdiparams \ + --save_file=./inference/ppTSN/ppTSN.onnx \ + --opset_version=10 \ + --enable_onnx_checker=True + ``` +执行完毕后,可以发现 `./inference/ppTSN` 目录下生成了一个 ONNX 格式的模型文件 `ppTSN.onnx` + +## 3. onnx 预测 + +接下来就可以用 ONNX 格式模型进行预测,其用法与paddle 预测模型类似 +执行如下命令: +```bash +python3.7 deploy/paddle2onnx/predict_onnx.py \ +--input_file data/example.avi \ +--config configs/recognition/pptsn/pptsn_k400_videos.yaml \ +--onnx_file=./inference/ppTSN/ppTSN.onnx +``` + +结果如下: +```bash +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9998553991317749 +``` +可以验证该结果与Paddle inference的预测结果完全一致 diff --git a/deploy/paddle2onnx/readme_en.md b/deploy/paddle2onnx/readme_en.md new file mode 100644 index 0000000000000000000000000000000000000000..6fe67726fbc0ed996480e59eb28d99350254feae --- /dev/null +++ b/deploy/paddle2onnx/readme_en.md @@ -0,0 +1,70 @@ +# paddle2onnx model conversion and prediction + +This chapter describes how the PP-TSN model is transformed into an ONNX model and predicted based on the ONNX engine. + +## 1. Environment preparation + +Need to prepare Paddle2ONNX model conversion environment, and ONNX model prediction environment. + +Paddle2ONNX supports converting the PaddlePaddle model format to the ONNX model format. The operator currently supports exporting ONNX Opset 9~11 stably, and some Paddle operators support lower ONNX Opset conversion. +For more details, please refer to [Paddle2ONNX](https://github.com/PaddlePaddle/Paddle2ONNX/blob/develop/README_zh.md) + +- Install Paddle2ONNX +```bash +python3.7 -m pip install paddle2onnx +``` + +- Install ONNXRuntime +```bash +# It is recommended to install version 1.9.0, and the version number can be changed according to the environment +python3.7 -m pip install onnxruntime==1.9.0 +``` + +## 2. Model conversion + +- PP-TSN inference model download + + ```bash + # Download the inference model to the PaddleVideo/inference/ppTSN/ directory + mkdir -p ./inference + wget -P ./inference/ https://videotag.bj.bcebos.com/PaddleVideo-release2.3/ppTSN.zip + + # Decompress the inference model + pushd ./inference + unzip ppTSN.zip + popd + ``` + +- Model conversion + + Convert Paddle inference models to ONNX format models using Paddle2ONNX: + + ```bash + paddle2onnx \ + --model_dir=./inference/ppTSN \ + --model_filename=ppTSN.pdmodel \ + --params_filename=ppTSN.pdiparams \ + --save_file=./inference/ppTSN/ppTSN.onnx \ + --opset_version=10 \ + --enable_onnx_checker=True + ``` +After execution, you can find that a model file `ppTSN.onnx` in ONNX format is generated in the `./inference/ppTSN` directory + +## 3. onnx prediction + +Next, you can use the ONNX format model for prediction, which is similar to the paddle prediction model +Execute the following command: +```bash +python3.7 deploy/paddle2onnx/predict_onnx.py \ +--input_file data/example.avi \ +--config configs/recognition/pptsn/pptsn_k400_videos.yaml \ +--onnx_file=./inference/ppTSN/ppTSN.onnx +``` + +The result is as follows: +```bash +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9998553991317749 +``` +It can be verified that the result is completely consistent with the prediction result of Paddle inference diff --git a/deploy/slim/quant_post_static.py b/deploy/slim/quant_post_static.py new file mode 100644 index 0000000000000000000000000000000000000000..84b2803e1c60bb4f674c2cf504b9dde1f50c20d5 --- /dev/null +++ b/deploy/slim/quant_post_static.py @@ -0,0 +1,120 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import os.path as osp +import sys + +import numpy as np +import paddle +from paddleslim.quant import quant_post_static + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../../'))) + +from paddlevideo.loader.builder import build_dataloader, build_dataset +from paddlevideo.utils import get_config, get_logger + + +def parse_args(): + def str2bool(v): + return v.lower() in ("true", "t", "1") + + parser = argparse.ArgumentParser("PaddleVideo Inference model script") + parser.add_argument( + '-c', + '--config', + type=str, + default= + '../../configs/recognition/pptsm/pptsm_k400_frames_uniform_quantization.yaml', + help='quantization config file path') + parser.add_argument('-o', + '--override', + action='append', + default=[], + help='config options to be overridden') + parser.add_argument("--use_gpu", + type=str2bool, + default=True, + help="whether use gpui during quantization") + + return parser.parse_args() + + +def post_training_quantization(cfg, use_gpu: bool = True): + """Quantization entry + + Args: + cfg (dict): quntization configuration. + use_gpu (bool, optional): whether to use gpu during quantization. Defaults to True. + """ + logger = get_logger("paddlevideo") + + place = paddle.CUDAPlace(0) if use_gpu else paddle.CPUPlace() + + # get defined params + batch_size = cfg.DATASET.get('batch_size', 1) + num_workers = cfg.DATASET.get('num_workers', 0) + inference_file_name = cfg.get('model_name', 'inference') + inference_model_dir = cfg.get('inference_model_dir', + f'./inference/{inference_file_name}') + quant_output_dir = cfg.get('quant_output_dir', + osp.join(inference_model_dir, 'quant_model')) + batch_nums = cfg.get('batch_nums', 10) + + # build dataloader for quantization, lite data is enough + slim_dataset = build_dataset((cfg.DATASET.quant, cfg.PIPELINE.quant)) + slim_dataloader_setting = dict(batch_size=batch_size, + num_workers=num_workers, + places=place, + drop_last=False, + shuffle=False) + slim_loader = build_dataloader(slim_dataset, **slim_dataloader_setting) + + logger.info("Build slim_loader finished") + + def sample_generator(loader): + def __reader__(): + for indx, data in enumerate(loader): + # must return np.ndarray, not paddle.Tensor + videos = np.array(data[0]) + yield videos + + return __reader__ + + # execute quantization in static graph mode + paddle.enable_static() + + exe = paddle.static.Executor(place) + + logger.info("Staring Post-Training Quantization...") + + quant_post_static(executor=exe, + model_dir=inference_model_dir, + quantize_model_path=quant_output_dir, + sample_generator=sample_generator(slim_loader), + model_filename=f'{inference_file_name}.pdmodel', + params_filename=f'{inference_file_name}.pdiparams', + batch_size=batch_size, + batch_nums=batch_nums, + algo='KL') + + logger.info("Post-Training Quantization finished...") + + +if __name__ == '__main__': + args = parse_args() + cfg = get_config(args.config, overrides=args.override) + post_training_quantization(cfg, args.use_gpu) diff --git a/deploy/slim/readme.md b/deploy/slim/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..cc9f764f8678b5ab8c69352a18401b324b35a5eb --- /dev/null +++ b/deploy/slim/readme.md @@ -0,0 +1,133 @@ + +## Slim功能介绍 +复杂的模型有利于提高模型的性能,但也导致模型中存在一定冗余。此部分提供精简模型的功能,包括两部分:模型量化(量化训练、离线量化)、模型剪枝。 + +其中模型量化将全精度缩减到定点数减少这种冗余,达到减少模型计算复杂度,提高模型推理性能的目的。 +模型量化可以在基本不损失模型的精度的情况下,将FP32精度的模型参数转换为Int8精度,减小模型参数大小并加速计算,使用量化后的模型在移动端等部署时更具备速度优势。 + +模型剪枝将CNN中不重要的卷积核裁剪掉,减少模型参数量,从而降低模型计算复杂度。 + +本教程将介绍如何使用飞桨模型压缩库PaddleSlim做PaddleVideo模型的压缩。 +[PaddleSlim](https://github.com/PaddlePaddle/PaddleSlim) 集成了模型剪枝、量化(包括量化训练和离线量化)、蒸馏和神经网络搜索等多种业界常用且领先的模型压缩功能,如果您感兴趣,可以关注并了解。 + +在开始本教程之前,建议先了解[PaddleVideo模型的训练方法](../../docs/zh-CN/usage.md)以及[PaddleSlim](https://paddleslim.readthedocs.io/zh_CN/latest/index.html) + + +## 快速开始 +当训练出一个模型后,如果希望进一步的压缩模型大小并加速预测,可使用量化或者剪枝的方法压缩模型。 + +模型压缩主要包括五个步骤: +1. 安装 PaddleSlim +2. 准备训练好的模型 +3. 模型压缩 +4. 导出量化推理模型 +5. 量化模型预测部署 + +### 1. 安装PaddleSlim + +* 可以通过pip install的方式进行安装。 + +```bash +python3.7 -m pip install paddleslim -i https://pypi.tuna.tsinghua.edu.cn/simple +``` + +* 如果获取PaddleSlim的最新特性,可以从源码安装。 + +```bash +git clone https://github.com/PaddlePaddle/PaddleSlim.git +cd Paddleslim +python3.7 setup.py install +``` + +### 2. 准备训练好的模型 + +PaddleVideo提供了一系列训练好的[模型](../../docs/zh-CN/model_zoo/README.md),如果待量化的模型不在列表中,需要按照[常规训练](../../docs/zh-CN/usage.md)方法得到训练好的模型。 + +### 3. 模型压缩 + +进入PaddleVideo根目录 + +```bash +cd PaddleVideo +``` + +离线量化代码位于`deploy/slim/quant_post_static.py`。 + +#### 3.1 模型量化 + +量化训练包括离线量化训练和在线量化训练(TODO),在线量化训练效果更好,需加载预训练模型,在定义好量化策略后即可对模型进行量化。 + +##### 3.1.1 在线量化训练 +TODO + +##### 3.1.2 离线量化 + +**注意**:目前离线量化,必须使用已经训练好的模型导出的`inference model`进行量化。一般模型导出`inference model`可参考[教程](../../docs/zh-CN/usage.md#5-模型推理). + +一般来说,离线量化损失模型精度较多。 + +以PP-TSM模型为例,生成`inference model`后,离线量化运行方式如下 + +```bash +# 下载并解压出少量数据用于离线量化的校准 +pushd ./data/k400 +wget -nc https://videotag.bj.bcebos.com/Data/k400_rawframes_small.tar +tar -xf k400_rawframes_small.tar +popd + +# 然后进入deploy/slim目录下 +cd deploy/slim + +# 执行离线量化命令 +python3.7 quant_post_static.py \ +-c ../../configs/recognition/pptsm/pptsm_k400_frames_uniform_quantization.yaml \ +--use_gpu=True +``` + +除`use_gpu`外,所有的量化环境参数都在`pptsm_k400_frames_uniform_quantization.yaml`文件中进行配置 +其中`inference_model_dir`表示上一步导出的`inference model`目录路径,`quant_output_dir`表示量化模型的输出目录路径 + +执行成功后,在`quant_output_dir`的目录下生成了`__model__`文件和`__params__`文件,这二者用于存储生成的离线量化模型 +类似`inference model`的使用方法,接下来可以直接用这两个文件进行预测部署,无需再重新导出模型。 + +```bash +# 使用PP-TSM离线量化模型进行预测 +# 回到PaddleVideo目录下 +cd ../../ + +# 使用量化模型进行预测 +python3.7 tools/predict.py \ +--input_file data/example.avi \ +--config configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml \ +--model_file ./inference/ppTSM/quant_model/__model__ \ +--params_file ./inference/ppTSM/quant_model/__params__ \ +--use_gpu=True \ +--use_tensorrt=False +``` + +输出如下: +```bash +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9997928738594055 +``` +#### 3.2 模型剪枝 +TODO + + +### 4. 导出模型 +TODO + + +### 5. 模型部署 + +上述步骤导出的模型可以通过PaddleLite的opt模型转换工具完成模型转换。 +模型部署的可参考 +[Serving Python部署](../python_serving/readme.md) +[Serving C++部署](../cpp_serving/readme.md) + + +## 训练超参数建议 + +* 量化训练时,建议加载常规训练得到的预训练模型,加速量化训练收敛。 +* 量化训练时,建议初始学习率修改为常规训练的`1/20~1/10`,同时将训练epoch数修改为常规训练的`1/5~1/2`,学习率策略方面,加上Warmup,其他配置信息不建议修改。 diff --git a/deploy/slim/readme_en.md b/deploy/slim/readme_en.md new file mode 100644 index 0000000000000000000000000000000000000000..c7f6ba20ad2ecc7bc62807b3efc0c09bbe62d6ea --- /dev/null +++ b/deploy/slim/readme_en.md @@ -0,0 +1,132 @@ +## Slim function introduction +A complex model is beneficial to improve the performance of the model, but it also leads to some redundancy in the model. This part provides the function of reducing the model, including two parts: model quantization (quantization training, offline quantization), model pruning. + +Among them, model quantization reduces the full precision to fixed-point numbers to reduce this redundancy, so as to reduce the computational complexity of the model and improve the inference performance of the model. +Model quantization can convert FP32-precision model parameters to Int8-precision without losing the accuracy of the model, reducing the size of model parameters and speeding up the calculation. Using the quantized model has a speed advantage when deploying on mobile terminals. + +Model pruning cuts out the unimportant convolution kernels in the CNN, reduces the amount of model parameters, and thus reduces the computational complexity of the model. + +This tutorial will introduce how to use PaddleSlim, a paddle model compression library, to compress PaddleVideo models. +[PaddleSlim](https://github.com/PaddlePaddle/PaddleSlim) integrates model pruning, quantization (including quantization training and offline quantization), distillation and neural network search and other commonly used and leading model compression functions in the industry. If you are interested, you can follow and understand. + +Before starting this tutorial, it is recommended to understand [PaddleVideo model training method](../../docs/zh-CN/usage.md) and [PaddleSlim](https://paddleslim.readthedocs.io/zh_CN/ latest/index.html) + + +## quick start +After training a model, if you want to further compress the model size and speed up prediction, you can use quantization or pruning to compress the model. + +Model compression mainly includes five steps: +1. Install PaddleSlim +2. Prepare the trained model +3. Model Compression +4. Export the quantitative inference model +5. Quantitative Model Prediction Deployment + +### 1. Install PaddleSlim + +* It can be installed by pip install. + +```bash +python3.7 -m pip install paddleslim -i https://pypi.tuna.tsinghua.edu.cn/simple +``` + +* If you get the latest features of PaddleSlim, you can install it from source. + +```bash +git clone https://github.com/PaddlePaddle/PaddleSlim.git +cd Paddleslim +python3.7 setup.py install +``` + +### 2. Prepare the trained model + +PaddleVideo provides a series of trained [models](../../docs/zh-CN/model_zoo/README.md). If the model to be quantized is not in the list, you need to follow the [regular training](../ ../docs/zh-CN/usage.md) method to get the trained model. + +### 3. Model Compression + +Go to PaddleVideo root directory + +```bash +cd PaddleVideo +``` + +The offline quantization code is located in `deploy/slim/quant_post_static.py`. + +#### 3.1 Model Quantization + +Quantization training includes offline quantization training and online quantization training (TODO). The effect of online quantization training is better. The pre-training model needs to be loaded, and the model can be quantized after the quantization strategy is defined. + +##### 3.1.1 Online quantitative training +TODO + +##### 3.1.2 Offline Quantization + +**Note**: For offline quantization, you must use the `inference model` exported from the trained model for quantization. For general model export `inference model`, please refer to [Tutorial](../../docs/zh-CN/usage.md#5-Model Inference). + +Generally speaking, the offline quantization loss model has more accuracy. + +Taking the PP-TSM model as an example, after generating the `inference model`, the offline quantization operation is as follows + +```bash +# download a small amount of data for calibration +pushd ./data/k400 +wget -nc https://videotag.bj.bcebos.com/Data/k400_rawframes_small.tar +tar -xf k400_rawframes_small.tar +popd + +# then switch to deploy/slim +cd deploy/slim + +# execute quantization script +python3.7 quant_post_static.py \ +-c ../../configs/recognition/pptsm/pptsm_k400_frames_uniform_quantization.yaml \ +--use_gpu=True +``` + +All quantization environment parameters except `use_gpu` are configured in `pptsm_k400_frames_uniform_quantization.yaml` file +Where `inference_model_dir` represents the directory path of the `inference model` exported in the previous step, and `quant_output_dir` represents the output directory path of the quantization model + +After successful execution, the `__model__` file and the `__params__` file are generated in the `quant_output_dir` directory, which are used to store the generated offline quantization model +Similar to the usage of `inference model`, you can directly use these two files for prediction deployment without re-exporting the model. + +```bash +# Use PP-TSM offline quantization model for prediction +# Go back to the PaddleVideo directory +cd ../../ + +# Use the quantized model to make predictions +python3.7 tools/predict.py \ +--input_file data/example.avi \ +--config configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml \ +--model_file ./inference/ppTSM/quant_model/__model__ \ +--params_file ./inference/ppTSM/quant_model/__params__ \ +--use_gpu=True \ +--use_tensorrt=False +``` + +The output is as follows: +```bash +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9997928738594055 +``` +#### 3.2 Model pruning +TODO + + +### 4. Export the model +TODO + + +### 5. Model Deployment + +The model exported in the above steps can be converted through the opt model conversion tool of PaddleLite. +Reference for model deployment +[Serving Python Deployment](../python_serving/readme.md) +[Serving C++ Deployment](../cpp_serving/readme.md) + + +## Training hyperparameter suggestions + +* During quantitative training, it is recommended to load the pre-trained model obtained from regular training to accelerate the convergence of quantitative training. +* During quantitative training, it is recommended to modify the initial learning rate to `1/20~1/10` of conventional training, and modify the number of training epochs to `1/5~1/2` of conventional training. In terms of learning rate strategy, add On Warmup, other configuration information is not recommended to be modified. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..353a649f5d2bb4f8d23d834288dc7b08046a62df --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contribution Guidelines + +We appreciate all contributions. If you are planning to contribute back bug-fixes, docs fixes, please do so without any further discussion. If you plan to contribute new features, utility functions or extensions, please first open an issue and discuss the feature with us. diff --git a/docs/en/benchmark.md b/docs/en/benchmark.md new file mode 100644 index 0000000000000000000000000000000000000000..bc41dfe12c2ee3b6f6383dd7782274860efd7cb1 --- /dev/null +++ b/docs/en/benchmark.md @@ -0,0 +1,69 @@ +[简体中文](../zh-CN/benchmark.md) | English +# Benchmark + +We compare our results with some popular frameworks and official releases in terms of speed. + +## Environment + +### Hardware + +- 8 NVIDIA Tesla V100 (16G) GPUs +- Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz + +### Software + +- Python 3.7 +- PaddlePaddle2.0 +- CUDA 10.1 +- CUDNN 7.6.3 +- NCCL 2.1.15 +- GCC 8.2.0 + +## Experiments and Statistics +The statistic is the average training time, including data processing and model training time, and the training speed is measured with ips(instance per second). Note that we skip the first 50 iters as they may contain the device warmup time. + +Here we compare PaddleVideo with the other video understanding toolkits in the same data and model settings. + +To ensure the fairness of the comparison, the comparison experiments were conducted under the same hardware environment and using the same dataset. The dataset we used is generated by the [data preparation](dataset/k400.md), and in each model setting, the same data preprocessing methods are applied to make sure the same feature input. + +Significant improvement can be observed when comparing with other video understanding framework as shown in the table below, Especially the [Slowfast](../../configs/recognition/slowfast/slowfast.yaml) model is nearly 2x faster than the counterparts. + + + +## Results +### Recognizers + +| Model | batch size x gpus | PaddleVideo(ips) | Reference(ips) | MMAction2 (ips) | PySlowFast (ips)| +| :------: | :-------------------:|:---------------:|:---------------: | :---------------: |:---------------: | +| [TSM](../../configs/recognition/tsm/tsm.yaml) | 16x8 | 58.1 | 46.04(temporal-shift-module) | To do | X | +| [PPTSM](../../configs/recognition/tsm/pptsm.yaml) | 16x8 | 57.6 | X | X | X | +| [TSN](../../configs/recognition/tsn/tsn.yaml) | 16x8 | 841.1 | To do (tsn-pytorch) | To do | X | +| [Slowfast](../../configs/recognition/slowfast/slowfast.yaml)| 16x8 | 99.5 | X | To do | 43.2 | +| [Attention_LSTM](../../configs/recognition/attention_lstm/attention_lstm.yaml) | 128x8 | 112.6 | X | X | X | + + +### Localizers + +| Model | PaddleVideo(ips) |MMAction2 (ips) |BMN(boundary matching network) (ips)| +| :--- | :---------------: | :-------------------------------------: | :-------------------------------------: | +| [BMN](../../configs/localization/bmn.yaml) | 43.84 | x | x | + + +### Segmenters + +This repo provides performance and accuracy comparison between classical and popular sequential action segmentation models + +| Model | Metrics | Value | Flops(M) |Params(M) | test time(ms) bs=1 | test time(ms) bs=2 | inference time(ms) bs=1 | inference time(ms) bs=2 | +| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| MS-TCN | F1@0.5 | 38.8% | 791.360 | 0.8 | 170 | - | 10.68 | - | +| ASRF | F1@0.5 | 55.7% | 1,283.328 | 1.3 | 190 | - | 16.34 | - | + +* Model: model name, for example: PP-TSM +* Metrics: Fill in the indicators used in the model test, and the data set used is **breakfast** +* Value: Fill in the value corresponding to the metrics index, and generally keep two decimal places +* Flops(M): The floating-point computation required for one forward operation of the model can be called `paddlevideo/tools/summary.py`script calculation (different models may need to be modified slightly), keep one decimal place, and measure it with data **input tensor with shape of (1, 2048, 1000)** +* Params(M): The model parameter quantity, together with flops, will be calculated by the script, and one decimal place will be reserved +* test time(ms) bs=1: When the python script starts the batchsize = 1 test, the time required for a sample is kept to two decimal places. The data set used in the test is **breakfast**. +* test time(ms) bs=2: When the python script starts the batchsize = 2 test, the time required for a sample is kept to two decimal places. The sequential action segmentation model is generally a full convolution network, so the batch of training, testing and reasoning_ Size is 1. The data set used in the test is **breakfast**. +* inference time(ms) bs=1: When the reasoning model is tested with GPU (default V100) with batchsize = 1, the time required for a sample is reserved to two decimal places. The dataset used for reasoning is **breakfast**. +* inference time(ms) bs=2: When the reasoning model is tested with GPU (default V100) with batchsize = 1, the time required for a sample is reserved to two decimal places. The sequential action segmentation model is generally a full convolution network, so the batch of training, testing and reasoning_ Size is 1. The dataset used for reasoning is **breakfast**. diff --git a/docs/en/dataset/AVA.md b/docs/en/dataset/AVA.md new file mode 100644 index 0000000000000000000000000000000000000000..ee95a57755cba7261cb9e34e665c27d6c1e37845 --- /dev/null +++ b/docs/en/dataset/AVA.md @@ -0,0 +1,113 @@ +[简体中文](../../zh-CN/dataset/k400.md) | English +# AVA Data Preparation +This document mainly introduces the preparation process of AVA dataset. +It mainly includes five parts: Video Data Download, Prepare Annotations, Cut video files, +Extract the RGB frames, Pulling Proposal Files,et al. +Before we start, please make sure that the directory is located at `$PaddleVideo/data/ava/script`. + + +--- + + +## 1. Video data Download +For basic dataset information, you can refer to the official website [AVA](https://research.google.com/ava/index.html). +For the dataset download, you can refer to the [AVA Download](https://github.com/cvdfoundation/ava-dataset) , +which introduce the way to download the dataset. We also provide the shell script for downloading the video files + +```shell +bash download_videos.sh +``` + +Furthermore,considering the difficulty in downloading, +we upload the video files to Baidu cloud disk in the form of zip packages, and users can download it by themselves according to their needs. +[Link]() coming soon. + + +**Note: the video files should be placed in `data/ava/videos`** + +--- +## 2.Prepare Annotations + +Next, you can run the following script to prepare annotations. + +```shell +bash download_annotations.sh +``` + +This command will download `ava_v2.1.zip` for AVA `v2.1` annotation. If you need the AVA `v2.2` annotation, you can try the following script. + +```shell +VERSION=2.2 bash download_annotations.sh +``` + +**Note: In fact,we will also provide the annotation zip files in Baidu cloud disk** + +--- +## 3. cut video files + +Cut each video from its 15th to 30th minute and make them at 30 fps. + +```shell +bash cut_videos.sh +``` +--- + +## 4. Extract RGB Frames + +you can use the ffmpeg to extract RGB frames by the following script. + +```shell +bash extract_rgb_frames.sh +``` + +--- + +## 5.Pulling Proposal Files + +The scripts are adapted from FAIR's [Long-Term Feature Banks](https://github.com/facebookresearch/video-long-term-feature-banks). + +Run the follow scripts to fetch pre-computed proposal list. + +```shell +bash fetch_ava_proposals.sh +``` + +--- +## 6.Folder Structure + +After the whole data pipeline for AVA preparation. +you can get the rawframes (RGB), videos and annotation files for AVA. + +In the context of the whole project (for AVA only), the folder structure will look like: + +``` +PaddleVideo +├── configs +├── paddlevideo +├── docs +├── tools +├── data +│ ├── ava +│ │ ├── annotations +│ │ | ├── ava_dense_proposals_train.FAIR.recall_93.9.pkl +│ │ | ├── ava_dense_proposals_val.FAIR.recall_93.9.pkl +│ │ | ├── ava_dense_proposals_test.FAIR.recall_93.9.pkl +│ │ | ├── ava_train_v2.1.csv +│ │ | ├── ava_val_v2.1.csv +│ │ | ├── ava_train_excluded_timestamps_v2.1.csv +│ │ | ├── ava_val_excluded_timestamps_v2.1.csv +│ │ | ├── ava_action_list_v2.1_for_activitynet_2018.pbtxt +│ │ ├── videos +│ │ │ ├── 053oq2xB3oU.mkv +│ │ │ ├── 0f39OWEqJ24.mp4 +│ │ │ ├── ... +│ │ ├── videos_15min +│ │ │ ├── 053oq2xB3oU.mkv +│ │ │ ├── 0f39OWEqJ24.mp4 +│ │ │ ├── ... +│ │ ├── rawframes +│ │ │ ├── 053oq2xB3oU +| │ │ │ ├── img_00001.jpg +| │ │ │ ├── img_00002.jpg +| │ │ │ ├── ... +``` diff --git a/docs/en/dataset/ActivityNet.md b/docs/en/dataset/ActivityNet.md new file mode 100644 index 0000000000000000000000000000000000000000..006a93670f76d533e91732e5c1d2b4cb15a56efb --- /dev/null +++ b/docs/en/dataset/ActivityNet.md @@ -0,0 +1,80 @@ +[简体中文](../../zh-CN/dataset/ActivityNet.md) | English + +# ActivityNet data preparation + +- [Introduction](#Introduction) +- [Download](#Download) + +## Introduction + +ActivityNet is a dataset for large-scale video understanding tasks, which can be used for tasks such as action localization, action recognition, etc. + + +## Download +1. The BMN model uses the processed ActivityNet 1.3 dataset. There are two ways to use it: + - Using our processed ActivityNet 1.3 dataset (compressed package is about 5.5G), each video has corresponding action labels, duration intervals, duration frames, duration seconds and other information + Download with the following command: + ```bash + wget https://paddlemodels.bj.bcebos.com/video_detection/bmn_feat.tar.gz # Download the processed video feature data + wget https://paddlemodels.bj.bcebos.com/video_detection/activitynet_1.3_annotations.json # Download the processed label data + ``` + + Or click the following hyperlinks to download: + + [Video feature data](https://paddlemodels.bj.bcebos.com/video_detection/bmn_feat.tar.gz) + [Video feature data](https://paddlemodels.bj.bcebos.com/video_detection/activitynet_1.3_annotations.json) + + then decompression `bmn_feat.tar.gz` + ```bash + tar -xf bmn_feat.tar.gz + ``` + + - Extract features by yourself + + First refer to [Download Instructions](https://github.com/activitynet/ActivityNet/tree/master/Crawler) to download the original dataset. When training this model, you need to use TSN to extract features from the source files first. You can [self-extract](https://github.com/yjxiong/temporal-segment-networks) video frame and optical flow information, and the pre-trained TSN model can be downloaded from [here](https://github.com/ yjxiong/anet2016-cuhk) download. + + + The information in the `activitynet_1.3_annotations.json` tag file is as follows: + ```json + { + "v_QOlSCBRmfWY": { + "duration_second": 82.73, + "subset": "training", + "duration_frame": 2067, + "annotations": [{ + "segment": [6.195294851794072, 77.73085420904837], + "label": "Ballet" + }], + "feature_frame": 2064 + }, + "v_ehGHCYKzyZ8": { + "duration_second": 61.7189999999999994, + "subset": "training", + "duration_frame": 1822, + "annotations": [{ + "segment": [43.95990729267573, 45.401932082395355], + "label": "Doing crunches" + }], + "feature_frame": 1808 + }, + ..., + ... + } + ``` + + In the end, `19228` video feature npy files are obtained, corresponding to the `19228` label information in the `activitynet_1.3_annotations.json` file. + +2. Create a new `data/bmn_data` folder, and then unzip the video feature data after downloading and put it in this folder, and finally it should be organized into the following form: + ``` + PaddleVideo + ├── data + │ ├── bmn_data + │ │ ├── fix_feat_100 + │ │ │ ├── v___c8enCfzqw.npy + │ │ │ ├── v___dXUJsj3yo.npy + │ │ │ ├── ... + │ │ │ + │ │ └── activitynet_1.3_annotations.json + ``` + +3. Finally, modify the `feat_path` field in the configuration file configs/localization/bmn.yaml to specify the feature directory path, and the `file_path` field to specify the label file path. diff --git a/docs/en/dataset/Oxford_RobotCar.md b/docs/en/dataset/Oxford_RobotCar.md new file mode 100644 index 0000000000000000000000000000000000000000..c02b54a0132f08b923c1c7ec5093ae225e75d15d --- /dev/null +++ b/docs/en/dataset/Oxford_RobotCar.md @@ -0,0 +1,162 @@ +[简体中文](../../zh-CN/dataset/Oxford_RobotCar.md) | English + +# Oxford-RobotCar-for-ADDS data preparation + +- [Introduction](#Introduction) +- [Data Set Download](#Download) +- [Preprocessing](#Preprocessing) +- [1. Image De-distortion](#1-Image-de-distortion) +- [2. Dynamic frame filter](#2-Dynamic-frame-filter) +- [3. Image Rename](#3-Image-Rename) +- [4. Preparation for Day-Pseudo Night Image Pair](#4-Day-Pseudo-Night-Image-Pair-Preparation) + + +## Introduction + +[Oxford RobotCar Dataset](https://robotcar-dataset.robots.ox.ac.uk/) is a large-scale autonomous driving data set that contains a large amount of data in different autonomous driving scenarios. + +What is used here is to filter a part of the data used for day-night depth estimation from the original Oxford RobotCar data set, namely Oxford-RobotCar-for-ADDS. + +If you want to use Oxford-RobotCar-for-ADDS, please cite the following papers: +```latex +@article{maddern20171, + title={1 year, 1000 km: The oxford robotcar dataset}, + author={Maddern, Will and Pascoe, Geoffrey and Linegar, Chris and Newman, Paul}, + journal={The International Journal of Robotics Research}, + volume={36}, + number={1}, + pages={3--15}, + year={2017}, + publisher={SAGE Publications Sage UK: London, England} +} +``` +```latex +@inproceedings{liu2021self, + title={Self-supervised Monocular Depth Estimation for All Day Images using Domain Separation}, + author={Liu, Lina and Song, Xibin and Wang, Mengmeng and Liu, Yong and Zhang, Liangjun}, + booktitle={Proceedings of the IEEE/CVF International Conference on Computer Vision}, + pages={12737--12746}, + year={2021} +} +``` + +## Download + +1. Download the left eye image of Bumblebee XB3 in the sequence [2014-12-09](https://robotcar-dataset.robots.ox.ac.uk/datasets/2014-12-09-13-21-02/) as For the training set of the daytime scene, the downloaded images are decompressed in the same folder. +2. Download the left eye image of Bumblebee XB3 in the sequence [2014-12-16](https://robotcar-dataset.robots.ox.ac.uk/datasets/2014-12-16-18-44-24/) as The training set of the night scene, the downloaded images are unzipped in the same folder. +3. The images and depth truth values ​​of the validation set are filtered from the original data set and downloaded from the link we gave. (The data download links are below) + ```shell + https://videotag.bj.bcebos.com/Data/ADDS/1209_all_files.txt + https://videotag.bj.bcebos.com/Data/ADDS/1216_all_files.txt + https://videotag.bj.bcebos.com/Data/ADDS/day_train_all.7z.001 + https://videotag.bj.bcebos.com/Data/ADDS/day_train_all.7z.002 + https://videotag.bj.bcebos.com/Data/ADDS/day_train_all_fake_night.7z.001 + https://videotag.bj.bcebos.com/Data/ADDS/day_train_all_fake_night.7z.002 + https://videotag.bj.bcebos.com/Data/ADDS/day_val_451.7z + https://videotag.bj.bcebos.com/Data/ADDS/day_val_451_gt.7z + https://videotag.bj.bcebos.com/Data/ADDS/night_val_411.7z + https://videotag.bj.bcebos.com/Data/ADDS/night_val_411_gt.7z + ``` + the original raw data download links: + ```shell + # data in day + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.001 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.002 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.003 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.004 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.005 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.006 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.007 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.008 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.009 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.010 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.011 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.012 + + # data in night + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.001 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.002 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.003 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.004 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.005 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.006 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.007 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.008 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.009 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.010 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.011 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.012 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.013 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.014 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.015 + ``` + +## Preprocessing + +### 1-Image-de-distortion + +Use the official toolbox [robotcar-dataset-sdk](https://github.com/ori-mrg/robotcar-dataset-sdk/tree/master/python) to pair the sequence 2014-12-09 and 2014-12- The image of 16 is de-distorted. + + +### 2-Dynamic-frame-filter + +Since we use the self-supervised method, we need to filter out dynamic frames for training. The filtering principle is that the inter-frame pose change is greater than 0.1m and it is considered a dynamic frame. After filtering, the sequence of the training set is obtained. + + +### 3-Image-Rename + +Rename the original image timestamp to a continuous number sequence. For daytime scene correspondence, see [1209_all_files.txt](https://videotag.bj.bcebos.com/Data/ADDS/1209_all_files.txt), for night scene correspondence, see [1216_all_files.txt](https://videotag.bj.bcebos.com/Data/ADDS/1216_all_files.txt). The renamed data format is as follows: +``` +├── oxford_processing + ├── day_train_all #Day training image folder (day_train_all.7z.001 ~ day_train_all.7z.012) + ├── night_train_all #Night training image folder (night_train_all.7z.001 ~ day_train_all.7z.015) + ├── day_val_451 #Daytime verification image folder (day_val_451.7z) + ├── day_val_451_gt #Daytime verification depth truth value folder (day_val_451_gt.7z) + ├── night_val_411 #night verification image folder (night_val_411.7z) + └── night_val_411_gt #Night verification depth truth value folder (night_val_411_gt.7z) +``` + +annotation files download links are below: +```shell +https://videotag.bj.bcebos.com/Data/ADDS/train_files.txt +https://videotag.bj.bcebos.com/Data/ADDS/val_day_files.txt +https://videotag.bj.bcebos.com/Data/ADDS/val_night_files.txt +``` + +The sequence used for training and verification is as follows: + +``` +splits/oxford_day/train_files.txt # training sequence during the day +splits/oxford_night/train_files.txt # training sequence at night +splits/oxford_day_451/val_files.txt # verification sequence during the day +splits/oxford_night_411/val_files.txt # night verification sequence +``` + +### 4-Day-Pseudo-Night-Image-Pair-Preparation + +In order to use our framework to extract the common information of day and night images, we use [CycleGAN](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix) to generate day-pseudo-night image pairs, where pseudo-night The night images corresponding to the daytime generated for CycleGAN, all images are scaled to 192x640, the night images are enhanced with histogram equalization, 75 epochs are trained, and the Oxford-RobotCar-for-ADDS is finally obtained. The generated day-pseudo-night image pair The data format is as follows, which can be directly used for training and verification of ADDS-DepthNet: +``` +├── oxford_processing_forADDS + ├── day_train_all #Day training image folder (day_train_all.7z.001 ~ day_train_all.7z.002) + ├── night_train_all #Night training image folder (night_train_all.7z.001 ~ day_train_all.7z.002) + ├── day_val_451 #Daytime verification image folder (day_val_451.7z) + ├── day_val_451_gt #Daytime verification depth truth value folder (day_val_451_gt.7z) + ├── night_val_411 #night verification image folder (night_val_411.7z) + └── night_val_411_gt #Night verification depth truth value folder (night_val_411_gt.7z) +data +└── oxford + ├── splits + ├── train_files.txt + ├── val_day_files.txt + └── val_night_files.txt + └── oxford_processing_forADDS + ├── day_train_all/ #Day training image folder (from day_train_all.7z.001 ~ day_train_all.7z.002) + ├── night_train_all/ #Night training image folder (from night_train_all.7z.001 ~ day_train_all.7z.002) + ├── day_val_451/ #Daytime verification image folder (from day_val_451.7z) + ├── day_val_451_gt/ #Daytime verification depth truth value folder (from day_val_451_gt.7z) + ├── night_val_411/ #night verification image folder (from night_val_411.7z) + └── night_val_411_gt/ #Night verification depth truth value folder (from night_val_411_gt.7z) + +``` + +The sequences used for training and verification are consistent with the foregoing. diff --git a/docs/en/dataset/SegmentationDataset.md b/docs/en/dataset/SegmentationDataset.md new file mode 100644 index 0000000000000000000000000000000000000000..3f67fb2725c7557525e2944ac54a13b749b6bdb1 --- /dev/null +++ b/docs/en/dataset/SegmentationDataset.md @@ -0,0 +1,35 @@ +English | [简体中文](../../zh-CN/dataset/SegmentationDataset.md) + +# Video Action Segmentation Dataset + +The video motion segmentation model uses breakfast, 50salads and gtea data sets. The use method is to use the features extracted by the pre training model, which can be obtained from the ms-tcn official code base.[feat](https://zenodo.org/record/3625992#.Xiv9jGhKhPY) + +- Dataset tree +```txt +─── gtea + ├── features + │ ├── S1_Cheese_C1.npy + │ ├── S1_Coffee_C1.npy + │ ├── S1_CofHoney_C1.npy + │ └── ... + ├── groundTruth + │ ├── S1_Cheese_C1.txt + │ ├── S1_Coffee_C1.txt + │ ├── S1_CofHoney_C1.txt + │ └── ... + ├── splits + │ ├── test.split1.bundle + │ ├── test.split2.bundle + │ ├── test.split3.bundle + │ └── ... + └── mapping.txt +``` + +- data tree +```txt +─── data + ├── 50salads + ├── breakfast + ├── gtea + └── ... +``` diff --git a/docs/en/dataset/fsd.md b/docs/en/dataset/fsd.md new file mode 100644 index 0000000000000000000000000000000000000000..2f16a485ad0adb0d2b020de0a75ce12805c4db08 --- /dev/null +++ b/docs/en/dataset/fsd.md @@ -0,0 +1,55 @@ +[简体中文](../../zh-CN/dataset/fsd.md) | English + +# Figure Skating Dataset + +- [Introduction](#Introduction) +- [Download](#Download) + +--- + + +## Introduction + +In figure skating, compared with other sports, human posture and trajectory show the characteristics of strong complexity, which is helpful to the research of fine-grained action recognition tasks. + +For FSD Dataset, all video materials are collected from the Figure Skating Championships from 2017 to 2018. The frame rate of the video is uniformly standardized to 30 frames per second, and the image size is 1080 * 720 to ensure the relative consistency of the dataset. After that, we use the 2D pose estimation algorithm Open Pose to extract frame by frame key points from the video, and finally save the data in `.npy` format. + +The directory structure of training dataset and test dataset is as follows: + +```txt +train_data.npy # 2922 +train_label.npy # 2922 +test_A_data.npy # 628 +test_B_data.npy # 634 +``` + +`train_label.npy` can be read using `np.load()`, each element is an integer variable with a value between 0-29, representing the label of the action. `data.npy` can be read using `np.load()`, return a tensor with the shape of `N×C×T×V×M`, the specific meaning of each dimension is as follows: + +| Dimension | Size | Meaning | Notes | +| :---- | :----: | :----: | :---- | +| N | N | Number of samples | - | +| C | 3 | The coordinates and confidence of each joint point respectively | rescale to -1~1 | +| T | 1500 | The duration of the action | The actual length of some actions may be less than 1500, in such case we will pad 0 to ensure the unity of T dimension. | +| V | 25 | Number of joint points | See the skeleton example below for the meaning of specific joint points. | +| M | 1 | Number of athletes | - | + + +skeleton example: + +
+
+
+ + + +## Download + +You can get the download link after registering on the [competition homepage](https://www.datafountain.cn/competitions/519). + +| Set | Data | Label | +| :---- | :----: | :----: | +| Train | [train_data.npy](https://videotag.bj.bcebos.com/Data/FSD_train_data.npy) | [train_label.npy](https://videotag.bj.bcebos.com/Data/FSD_train_label.npy) | +| TestA | comming soon | comming soon | + + +> RGB datasets would not be provided for copyright reasons. diff --git a/docs/en/dataset/k400.md b/docs/en/dataset/k400.md new file mode 100644 index 0000000000000000000000000000000000000000..539735513fec907f34b23fcb6d72ca9fbf023e89 --- /dev/null +++ b/docs/en/dataset/k400.md @@ -0,0 +1,78 @@ +[简体中文](../../zh-CN/dataset/k400.md) | English + +# Kinetics-400 Preparation + +- [Introduction](#Introduction) +- [Download](#Download) +- [Frames](#Frames) + +--- + + +## Introduction + +Kinetics-400 is a commonly used benchmark dataset in the video field. Please refer to its official website [Kinetics](https://deepmind.com/research/open-source/kinetics) for details. You can refer to the official address [ActivityNet](https://github.com/activitynet/ActivityNet/tree/master/Crawler/Kinetics), and use the download script provided to download the dataset. + +## Download + +Considering the difficulty of downloading the K400 data set, we provide two download methods: (1) Baidu network disk download (2) Script download + +### Baidu SkyDrive Download + +Netdisk link: https://pan.baidu.com/s/1S_CGBjWOUAuxL_cCX5kMPg +Extraction code: `ppvi` + +### Script download + +- Download the training set link list file [train_link.list](https://ai-rank.bj.bcebos.com/Kinetics400/train_link.list) and the validation set link list file [val_link.list](https://ai-rank.bj.bcebos.com/Kinetics400/val_link.list). + +Write the download script `download.sh` as follows: + +```bash +file=$1 + +while read line +do + wget "$line" +done <$file +``` + +Download training set command: +```bash +bash download.sh train_link.list +``` + +Download verification set command: +```bash +bash download.sh val_link.list +``` + +--- + +|category | Number of data | list file | +| :------: | :----------: | :----: | +|Training set | 234619 | [train.list](https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train.list)| +|Validation set | 19761 | [val.list](https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val.list)| + +- After downloading, unzip and add the data path to list file. + +- Due to the failure of some video link, part of original data is missing. This copies need about 135G of storage space. + +> This copies is only used for academic research. If it is helpful to you, welcome to star [our project](https://github.com/PaddlePaddle/PaddleVideo) + + +## Frames +In order to speed up the training process of the network, we first extract frames from the video file (K400 video file is in mp4 format). Compared with the method of network training directly through video files, the method of frames can greatly accelerate the speed of network training。 + +Enter the following command to extract the frames of the K400 video file + +```python +python extract_rawframes.py ./videos/ ./rawframes/ --level 2 --ext mp4 +``` + +After the video file frames are extracted, they will be stored in the specified `./rawframes` path, and the size is about 2T. + +|category | Number of data | list file | +| :------: | :----------: | :----: | +|Training set | 234619 | [train_frames.list](https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train_frames.list)| +|Validation set | 19761 | [val_frames.list](https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val_frames.list)| diff --git a/docs/en/dataset/msrvtt.md b/docs/en/dataset/msrvtt.md new file mode 100644 index 0000000000000000000000000000000000000000..390ba9d0090126e50c89584eebbe5a3352bfaa23 --- /dev/null +++ b/docs/en/dataset/msrvtt.md @@ -0,0 +1,79 @@ +[简体中文](../../zh-CN/dataset/msrvtt.md) | English + +# MSR-VTT Preparation + +- [Introduction](#1.1) +- [Download for T2VLAD](#1.2) +- [Download for ActBERT](#1.3) +- [Reference](#1.4) + + + +## Introduction + +MSR-VTT(Microsoft Research Video to Text) is a large-scale dataset containing videos and subtitles, which is composed of 10000 video clips from 20 categories, and each video clip is annotated with 20 English sentences. We used 9000 video clips for training and 1000 for testing. For more details, please refer to the website: [MSRVTT](https://www.microsoft.com/en-us/research/publication/msr-vtt-a-large-video-description-dataset-for-bridging-video-and-language/) + + +## Download for T2VLAD + +[T2VLAD doc](../../../applications/T2VLAD/README_en.md) + +For ease of use, we provided extracted features of video. + +First, make sure to enter the following command in the `applications/T2VLAD/data` directory to download the dataset. + +```bash +bash download_features.sh +``` + +After downloading, the files in the data directory are organized as follows: + +``` +├── data +| ├── MSR-VTT +| │ ├── raw-captions.pkl +| │ ├── train_list_jsfusion.txt +| │ ├── val_list_jsfusion.txt +| │ ├── aggregated_text_feats +| | | ├── w2v_MSRVTT_openAIGPT.pickle +| | ├── mmt_feats +| │ │ ├── features.audio.pkl +| │ │ ├── features.face_agg.pkl +| │ │ ├── features.flos_agg.pkl +| │ │ ├── features.ocr.pkl +| │ │ ├── features.rgb_agg.pkl +| │ │ ├── features.s3d.pkl +| │ │ ├── features.scene.pkl +| │ │ ├── features.speech.pkl + +``` + +## Download for ActBERT + +[ActBERT doc](../model_zoo/multimodal/actbert.md) + +Download data features: +``` +wget https://videotag.bj.bcebos.com/Data/ActBERT/msrvtt_test.lmdb.tar +wget https://videotag.bj.bcebos.com/Data/ActBERT/MSRVTT_JSFUSION_test.csv +``` + +Decompress the `msrvtt_test.lmdb.tar`: +``` +tar -zxvf msrvtt_test.lmdb.tar +``` + +The files in the data directory are organized as follows: + +``` +├── data +| ├── MSR-VTT +| │ ├── MSRVTT_JSFUSION_test.csv +| │ ├── msrvtt_test.lmdb +| │ ├── data.mdb +| │ ├── lock.mdb +``` + + +## Reference +- Valentin Gabeur, Chen Sun, Karteek Alahari, and Cordelia Schmid. Multi-modal transformer for video retrieval. In ECCV, 2020. diff --git a/docs/en/dataset/ntu-rgbd.md b/docs/en/dataset/ntu-rgbd.md new file mode 100644 index 0000000000000000000000000000000000000000..0b0056b7d040fce85ecff8d766967bd9eaca447d --- /dev/null +++ b/docs/en/dataset/ntu-rgbd.md @@ -0,0 +1,158 @@ +[简体中文](../../zh-CN/dataset/ntu-rgbd.md) | English + +# NTU-RGB+D Preparation + +- [Introduction](#Introduction) +- [ST-GCN Data Prepare](#ST-GCN_Data_Prepare) +- [CTR-GTCN Data Prepare](#CTR-GCN_Data_Prepare) + +--- + + +## Introduction + +NTU-RGB+D contains 60 action classes and 56,880 video samples for skeleton-based action recognition. Please refer to its official website[NTU-RGB+D](https://rose1.ntu.edu.sg/dataset/actionRecognition/) for more details. + +The dataset contains two splits when dividing the training set and test set. For Cross-subject, the dataset is divided according to character id, with 40320 samples in training set and 16560 samples in test set. For Cross-view, the dataset is divided according to camera division. The samples collected by cameras 2 and 3 are training sets, including 37930 samples, and the samples collected by camera 1 are test sets, including 18960 samples. + +## ST-GCN_Data_Prepare + +ST-GCN data prepare preceduce are introducted follow. + +### Download +We provide the download link of the processed dataset [NTU-RGB-D.tar](https://videotag.bj.bcebos.com/Data/NTU-RGB-D.tar)(~3.1G). Please download and unzip with ```tar -zxvf NTU-RGB-D.tar ``` , the directory structure is as follows: + +```txt +─── NTU-RGB-D + ├── xsub + │ ├── train_data.npy + │ ├── train_label.pkl + │ ├── val_data.npy + │ └── val_label.pkl + └── xview + ├── train_data.npy + ├── train_label.pkl + ├── val_data.npy + └── val_label.pkl +``` + +> This is a copies from [st-gcn](https://github.com/open-mmlab/mmskeleton/blob/master/doc/SKELETON_DATA.md). + +## CTR-GCN_Data_Prepare + +CTR-GCN data prepare preceduce are introducted follow. + +### Download + +There is script `download_dataset.sh` to download the dataset from official website [NTU-RGB+D](https://rose1.ntu.edu.sg/dataset/actionRecognition/) in dictory `data\ntu-rgb-d`. + +```bash +sh data/ntu-rgb-d/download_dataset.sh +``` + +File tree: +```txt +─── ntu-rgb-d + ├── download_dataset.sh + ├── nturgb+d_skeletons + │   ├── S001C001P001R001A001.skeleton + │   ├── S001C001P001R001A002.skeleton + │   ├── S001C001P001R001A003.skeleton + │   ├── S001C001P001R001A004.skeleton + │   ├── S001C001P001R001A005.skeleton + │   ├── S001C001P001R001A006.skeleton + │   ├── S001C001P001R001A007.skeleton + │ ├── .... + │   └── S017C003P020R002A060.skeleton + ├── get_raw_denoised_data.py + ├── get_raw_skes_data.py + ├── seq_transformation.py + └── statistics +    ├── camera.txt +    ├── label.txt +    ├── performer.txt +    ├── replication.txt +    ├── setup.txt +    └── skes_available_name.txt + +``` + +### Prepare + +run follow script, then data will be precessed to the data format need by CTR-GCN. + +> Note:if make dataset by yourself, please prepare `data/ntu-rgb-d/statistics/skes_available_name.txt`, which is the list of skeletons files that will be precessed. + +```bash +cd ./data/ntu-rgb-d +# Get skeleton of each performer +python get_raw_skes_data.py +# Remove the bad skeleton +python get_raw_denoised_data.py +# Transform the skeleton to the center of the first frame +python seq_transformation.py +``` + +File tree: + +```txt +─── ntu-rgb-d + ├── download_dataset.sh + ├── nturgb+d_skeletons + │   ├── S001C001P001R001A001.skeleton + │   ├── S001C001P001R001A002.skeleton + │   ├── S001C001P001R001A003.skeleton + │   ├── S001C001P001R001A004.skeleton + │   ├── S001C001P001R001A005.skeleton + │   ├── S001C001P001R001A006.skeleton + │   ├── S001C001P001R001A007.skeleton + │ ├── .... + │   └── S017C003P020R002A060.skeleton + ├── denoised_data + │   ├── actors_info + │   │   ├── S001C001P001R001A024.txt + │   │   ├── S001C001P001R001A025.txt + │   │   ├── S001C001P001R001A026.txt + │   │   ├── .... + │   │   ├── S017C003P020R002A059.txt + │   │   └── S017C003P020R002A060.txt + │   ├── denoised_failed_1.log + │   ├── denoised_failed_2.log + │   ├── frames_cnt.txt + │   ├── missing_skes_1.log + │   ├── missing_skes_2.log + │   ├── missing_skes.log + │   ├── noise_length.log + │   ├── noise_motion.log + │   ├── noise_spread.log + │   ├── raw_denoised_colors.pkl + │   ├── raw_denoised_joints.pkl + │   └── rgb+ske + ├── raw_data + │   ├── frames_cnt.txt + │   ├── frames_drop.log + │   ├── frames_drop_skes.pkl + │   └── raw_skes_data.pkl + ├── get_raw_denoised_data.py + ├── get_raw_skes_data.py + ├── seq_transformation.py + ├── statistics + │   ├── camera.txt + │   ├── label.txt + │   ├── performer.txt + │   ├── replication.txt + │   ├── setup.txt + │   └── skes_available_name.txt + ├── xview + │ ├── train_data.npy + │ ├── train_label.pkl + │ ├── val_data.npy + │ └── val_label.pkl + └── xsub + ├── train_data.npy + ├── train_label.pkl + ├── val_data.npy + └── val_label.pkl +``` + +> Note:dictory `denoised_data`、`raw_data`and`nturgb+d_skeletons`, that are temporal files, can be deleted, if extracted `xview` and `xsub`. diff --git a/docs/en/dataset/ucf101.md b/docs/en/dataset/ucf101.md new file mode 100644 index 0000000000000000000000000000000000000000..478a306c47fef3a82998211f9d8dc3517054a23c --- /dev/null +++ b/docs/en/dataset/ucf101.md @@ -0,0 +1,86 @@ +# UCF101数据准备 +UCF101数据的相关准备。主要包括UCF101的video文件下载,video文件提取frames,以及生成文件的路径list。 + +--- +## 1. 数据下载 +UCF101数据的详细信息可以参考网站[UCF101](https://www.crcv.ucf.edu/data/UCF101.php)。 为了方便用户使用,我们提供了UCF101数据的annotations文件和videos文件的下载脚本。 + +### 下载annotations文件 +首先,请确保在`./data/dataset/ucf101/`目录下,输入如下UCF101数据集的标注文件的命令。 +```shell +bash download_annotations.sh +``` + +### 下载UCF101的视频文件 +同样需要确保在`./data/dataset/ucf101/`目录下,输入下述命令下载视频文件 +```shell +bash download_videos.sh +``` +下载完成后视频文件会存储在`./data/dataset/ucf101/videos/`文件夹下,视频文件大小为6.8G。 + +--- +## 2. 提取视频文件的frames +为了加速网络的训练过程,我们首先对视频文件(ucf101视频文件为avi格式)提取帧 (frames)。相对于直接通过视频文件进行网络训练的方式,frames的方式能够加快网络训练的速度。 + +直接输入如下命令,即可提取ucf101视频文件的frames +``` python +python extract_rawframes.py ./videos/ ./rawframes/ --level 2 --ext avi +``` +视频文件frames提取完成后,会存储在`./rawframes`文件夹下,大小为56G。 + +--- +## 3. 生成frames文件和视频文件的路径list +生成视频文件的路径list,输入如下命令 + +```python +python build_ucf101_file_list.py videos/ --level 2 --format videos --out_list_path ./ +``` +生成frames文件的路径list,输入如下命令: +```python +python build_ucf101_file_list.py rawframes/ --level 2 --format rawframes --out_list_path ./ +``` + +**参数说明** + +`videos/` 或者 `rawframes/` : 表示视频或者frames文件的存储路径 + +`--level 2` : 表示文件的存储结构 + +`--format`: 表示是针对视频还是frames生成路径list + +`--out_list_path `: 表示生的路径list文件存储位置 + + +# 以上步骤完成后,文件组织形式如下所示 + +``` +├── data +| ├── dataset +| │ ├── ucf101 +| │ │ ├── ucf101_{train,val}_split_{1,2,3}_rawframes.txt +| │ │ ├── ucf101_{train,val}_split_{1,2,3}_videos.txt +| │ │ ├── annotations +| │ │ ├── videos +| │ │ │ ├── ApplyEyeMakeup +| │ │ │ │ ├── v_ApplyEyeMakeup_g01_c01.avi +| +| │ │ │ ├── YoYo +| │ │ │ │ ├── v_YoYo_g25_c05.avi +| │ │ ├── rawframes +| │ │ │ ├── ApplyEyeMakeup +| │ │ │ │ ├── v_ApplyEyeMakeup_g01_c01 +| │ │ │ │ │ ├── img_00001.jpg +| │ │ │ │ │ ├── img_00002.jpg +| │ │ │ │ │ ├── ... +| │ │ │ │ │ ├── flow_x_00001.jpg +| │ │ │ │ │ ├── flow_x_00002.jpg +| │ │ │ │ │ ├── ... +| │ │ │ │ │ ├── flow_y_00001.jpg +| │ │ │ │ │ ├── flow_y_00002.jpg +| │ │ │ ├── ... +| │ │ │ ├── YoYo +| │ │ │ │ ├── v_YoYo_g01_c01 +| │ │ │ │ ├── ... +| │ │ │ │ ├── v_YoYo_g25_c05 + +``` diff --git a/docs/en/dataset/youtube8m.md b/docs/en/dataset/youtube8m.md new file mode 100644 index 0000000000000000000000000000000000000000..77c6860422a15f152a26c722ba2d85f391c8993a --- /dev/null +++ b/docs/en/dataset/youtube8m.md @@ -0,0 +1,56 @@ +English | [简体中文](../../zh-CN/dataset/youtube8m.md) + +# YouTube-8M Data Preparation + +- [Introduction](#Introduction) +- [Download](#Download) +- [Conversion](#Conversion) + + +## Introduction + +YouTube-8M is a large-scale video classification data set, containing more than 8 million video URLs. The tag system covers more than 3800 knowledge graph entities. One video corresponds to multiple tags (3-4 on average) and is labeled by machine. + +**The length of each video is between 120s and 500s +Due to the large amount of video data, the image classification model was used to extract frame-level features in advance, and PCA was used to reduce the dimensionality of the features to obtain multi-frame 1024-dimensional features. Similarly, the audio model was used to obtain multi-frame 128-dimensional features. Audio characteristics. ** +> The dataset used here is the updated YouTube-8M data set in 2018 (May 2018 version (current): 6.1M videos, 3862 classes, 3.0 labels/video, 2.6B audio-visual features). + + +## Download +1. Create a new directory for storing features (take the PaddleVideo directory as an example) + ```bash + cd data/yt8m + mkdir frame + cd frame + ``` +2. Download the training and validation set to the frame folder + ```bash + curl data.yt8m.org/download.py | partition=2/frame/train mirror=asia python + curl data.yt8m.org/download.py | partition=2/frame/validate mirror=asia python + ``` + The download process is shown in the figure + ![image](https://user-images.githubusercontent.com/23737287/140709613-1e2d6ec0-a82e-474d-b220-7803065b0153.png) + + After the data download is complete, you will get 3844 training data files and 3844 verification data files (TFRecord format) + +## Conversion +1. Install tensorflow to read tfrecord data + ```bash + python3.7 -m pip install tensorflow-gpu==1.14.0 + ``` +2. Convert the downloaded TFRecord file into a pickle file for PaddlePaddle to use + ```bash + cd .. # From the frame directory back to the yt8m directory + python3.7 tf2pkl.py ./frame ./pkl_frame/ # Convert train*.tfrecord and validate*.tfrecord in the frame folder to pkl format + ``` +3. Generate a single pkl file path set, and split pkl into multiple small pkl files based on this file, and generate the final split pkl file path required + ```bash + ls pkl_frame/train*.pkl> train.list # Write the path of train*.pkl to train.list + ls pkl_frame/validate*.pkl> val.list # Write the path of validate*.pkl into val.list + + python3.7 split_yt8m.py train.list # Split each train*.pkl into multiple train*_split*.pkl + python3.7 split_yt8m.py val.list # Split each validate*.pkl into multiple validate*_split*.pkl + + ls pkl_frame/train*_split*.pkl> train.list # Rewrite the path of train*_split*.pkl into train.list + ls pkl_frame/validate*_split*.pkl> val.list # Rewrite the path of validate*_split*.pkl into val.list + ``` diff --git a/docs/en/install.md b/docs/en/install.md new file mode 100644 index 0000000000000000000000000000000000000000..b0d3bb239c5b01e48aaf6ad3007222cea4035b56 --- /dev/null +++ b/docs/en/install.md @@ -0,0 +1,71 @@ +[简体中文](../zh-CN/install.md) | English + +# Installation + +--- + +- [Introduction](#Introduction) +- [Install PaddlePaddle](#Install-PaddlePaddle) +- [Install PaddleVideo](#Install-PaddleVideo) + +## Introduction + +This document introduces how to install PaddlePaddle、PaddleVideo and its requirements. + +## Install PaddlePaddle + +Python 3.7, CUDA 10.1, CUDNN7.6.4 nccl2.1.2 and later version are required at first, For now, PaddleVideo only support training on the GPU device. Please follow the instructions in the [Installation](http://www.paddlepaddle.org.cn/install/quick) if the PaddlePaddle on the device is lower than v2.0 + +**Install PaddlePaddle** + +```bash +pip3 install paddlepaddle-gpu --upgrade +``` + +or compile from source code, please refer to [Installation](http://www.paddlepaddle.org.cn/install/quick). + +Verify Installation + +```python +import paddle +paddle.utils.run_check() +``` + +Check PaddlePaddle version: + +```bash +python3 -c "import paddle; print(paddle.__version__)" +``` + +Note: +- Make sure the compiled version is later than PaddlePaddle2.0. +- Indicate **WITH_DISTRIBUTE=ON** when compiling, Please refer to [Instruction](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/install/Tables.html#id3) for more details. +- When running in the docker, in order to ensure that the container has enough shared memory for data read acceleration of Paddle, please set the parameter `--shm_size=32g` at creating a docker container, if conditions permit, you can set it to a larger value. + +--- + +## Install PaddleVideo + +**Clone PaddleVideo:** + +```bash +cd path_to_clone_PaddleVideo +git clone https://github.com/PaddlePaddle/PaddleVideo.git +``` + +**Install requirements** + +```bash +python3.7 -m pip install --upgrade pip +pip3.7 install --upgrade -r requirements.txt +``` + +**Install python package** + +Install PaddleVideo via pip WIP + +**Install docker** + +Install PaddleVideo via docker WIP + + diff --git a/docs/en/model_zoo/README.md b/docs/en/model_zoo/README.md new file mode 100644 index 0000000000000000000000000000000000000000..24ad7badafb550fff862b435b98e20c211cf143e --- /dev/null +++ b/docs/en/model_zoo/README.md @@ -0,0 +1,40 @@ +[简体中文](../../zh-CN/model_zoo/README.md) | English + +# Introduction + +We implemented action recgonition model and action localization model in this repo. + +## Model list + +| Field | Model | Config | Dataset | Metrics | ACC% | Download | +| :--------------- | :--------: | :------------: | :------------: | :------------: | :------------: | :------------: | +| action recognition | [**PP-TSM**](./recognition/pp-tsm.md) | [pptsm.yaml](../../../configs/recognition/pptsm/pptsm_k400_frames_dense.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 76.16 | [PPTSM.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_dense_distill.pdparams) | +| action recognition | [**PP-TSN**](./recognition/pp-tsn.md) | [pptsn.yaml](../../../configs/recognition/pptsn/pptsn_k400_videos.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 75.06 | [PPTSN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSN_k400_8.pdparams) | +| action recognition | [**PP-TimeSformer**](./recognition/pp-timesformer.md) | [pptimesformer.yaml](../../../configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 79.44 | [ppTimeSformer_k400_16f_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTimeSformer_k400_16f_distill.pdparams) | +| action recognition | [AGCN](./recognition/agcn.md) | [agcn.yaml](../../../configs/recognition/agcn/agcn_fsd.yaml) | [FSD](../dataset/fsd.md) | Top-1 | 62.29 | [AGCN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AGCN_fsd.pdparams) | +| action recognition | [ST-GCN](./recognition/stgcn.md) | [stgcn.yaml](../../../configs/recognition/stgcn/stgcn_fsd.yaml) | [FSD](../dataset/fsd.md) | Top-1 | 59.07 | [STGCN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/STGCN_fsd.pdparams) | +| action recognition | [VideoSwin](./recognition/videoswin.md) | [videoswin.yaml](../../../configs/recognition/videoswin/videoswin_k400_videos.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 82.40 | [VideoSwin.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/VideoSwin_k400.pdparams) | +| action recognition | [TimeSformer](./recognition/timesformer.md) | [timesformer.yaml](../../../configs/recognition/timesformer/timesformer_k400_videos.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 77.29 | [TimeSformer.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TimeSformer_k400.pdparams) | +| action recognition | [SlowFast](./recognition/slowfast.md) | [slowfast_multigrid.yaml](../../../configs/recognition/slowfast/slowfast_multigrid.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 75.84 | [SlowFast.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast_8*8.pdparams) | +| action recognition | [TSM](./recognition/tsm.md) | [tsm.yaml](../../../configs/recognition/tsm/tsm_k400_frames.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 70.86 | [TSM.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams) | +| action recognition | [TSN](./recognition/tsn.md) | [tsn.yaml](../../../configs/recognition/tsn/tsn_k400_frames.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 69.81 | [TSN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TSN_k400.pdparams) | +| action recognition | [AttentionLSTM](./recognition/attention_lstm.md) | [attention_lstm.yaml](../../../configs/recognition/attention_lstm/attention_lstm.yaml) | [Youtube-8M](../dataset/youtube8m.md) | Hit@1 | 89.0 | [AttentionLstm.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/AttentionLstm/AttentionLstm.pdparams) | +| action detection| [BMN](./localization/bmn.md) | [bmn.yaml](../../../configs/localization/bmn.yaml) | [ActivityNet](../dataset/ActivityNet.md) | AUC | 67.23 | [BMN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/BMN/BMN.pdparams) | +| shot boundary detection | [TransNetV2](./partition/transnetv2.md) | [transnetv2.yaml](../../../configs/partitioners/transnetv2/transnetv2.yaml) | ClipShots | F1 scores | 76.1 | | +| monocular depth estimation | [ADDS](./estimation/adds.md) | [adds.yaml](../../../configs/estimation/adds/adds.yaml) | Oxford_RobotCar | Abs Rel | 0.209 | [ADDS_car.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ADDS_car.pdparams) | + + +# Reference + +- [Attention Clusters: Purely Attention Based Local Feature Integration for Video Classification](https://arxiv.org/abs/1711.09550), Xiang Long, Chuang Gan, Gerard de Melo, Jiajun Wu, Xiao Liu, Shilei Wen +- [BMN: Boundary-Matching Network for Temporal Action Proposal Generation](https://arxiv.org/abs/1907.09702), Tianwei Lin, Xiao Liu, Xin Li, Errui Ding, Shilei Wen. +- [SlowFast Networks for Video Recognition](https://arxiv.org/abs/1812.03982), Feichtenhofer C, Fan H, Malik J, et al. +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859), Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoou Tang, Luc Van Gool +- [Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/abs/1811.08383v1), Ji Lin, Chuang Gan, Song Han +- [Is Space-Time Attention All You Need for Video Understanding?](https://arxiv.org/pdf/2102.05095.pdf) Gedas Bertasius, Heng Wang, Lorenzo Torresani +- [Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1801.07455), Sijie Yan, Yuanjun Xiong, Dahua Lin +- [Two-Stream Adaptive Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1805.07694), Lei Shi, Yifan Zhang, Jian Cheng, Hanqing Lu +- [Skeleton-Based Action Recognition with Multi-Stream Adaptive Graph Convolutional Networks](https://arxiv.org/abs/1912.06971), Lei Shi, Yifan Zhang, Jian Cheng, Hanqing Lu +- [TransNet V2: An effective deep network architecture for fast shot transition detection](https://arxiv.org/abs/2008.04838), Tomáš Souček, Jakub Lokoč +- [Self-supervised Monocular Depth Estimation for All Day Images using Domain Separation](https://arxiv.org/abs/2108.07628), Lina Liu, Xibin Song, Mengmeng Wang + diff --git a/docs/en/model_zoo/detection/SlowFast_FasterRCNN_en.md b/docs/en/model_zoo/detection/SlowFast_FasterRCNN_en.md new file mode 100644 index 0000000000000000000000000000000000000000..b100f442859748f5441c77b80713fb9681344a28 --- /dev/null +++ b/docs/en/model_zoo/detection/SlowFast_FasterRCNN_en.md @@ -0,0 +1,129 @@ +[简体中文](../../../zh-CN/model_zoo/detection/SlowFast_FasterRCNN.md) | English + +# SlowFast_FasterRCNN + +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) + +Before getting started, you need to install additional dependencies as follows: +```bash +python -m pip install moviepy +python -m pip install et_xmlfile +python -m pip install paddledet +``` + +## Introduction + +The [SlowFast](https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/model_zoo/recognition/slowfast.md) model is one of the high-precision models in the video field. For action detection task, it is also neccessary to detect the person in current frame. Therefore, the SlowFast_FasterRCNN model takes human detection results and video frames as input, extracts spatiotemporal features through the SlowFast model, and then uses FasterRCNN's head gets the actions and positions of humans in the frame. + +The corresponding AI Studio Notebook Link:[基于SlowFast+FasterRCNN的动作识别](https://aistudio.baidu.com/aistudio/projectdetail/3267637?contributionType=1) + +For details, please refer to the paper [SlowFast Networks for Video Recognition](https://arxiv.org/pdf/1812.03982.pdf). + +## Data + +We use [AVA dataset](https://research.google.com/ava/download.html) for action detection. The AVA v2.2 dataset contains 430 videos split into 235 for training, 64 for validation, and 131 for test. Each video has 15 minutes annotated in 1 second intervals. + +### 1 Dowload Videos +``` +bash download_videos.sh +``` + +### 2 Download Annotations +``` +bash download_annotations.sh +``` + +### 3 Download Proposals + +``` +bash fetch_ava_proposals.sh +``` + +### 4 Cut Videos + +``` +bash cut_videos.sh +``` + +### 5 Extract Frames + +``` +bash extract_rgb_frames.sh +``` + +For AVA v2.1, there is a simple introduction to some key files: +* 'ava_videos_15min_frames' dir stores video frames extracted with FPS as the frame rate; +* 'ava_train_v2.1.csv' file stores the trainning annotations; +* 'ava_train_excluded_timestamps_v2.1.csv' file stores excluded timestamps; +* 'ava_dense_proposals_train.FAIR.recall_93.9.pkl' file stores humans' bboxes and scores of key frames; +* 'ava_action_list_v2.1_for_activitynet_2018.pbtxt' file stores为 action list. + +## Train + +* `-c`: config file path; +* `-w`: weights of model. The pretrained model can be downloaded from the table below; +* `--validate`: evaluate model during training. + +``` +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +python -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=logdir.ava main.py --validate -w paddle.init_param.pdparams -c configs/detection/ava/ava.yaml +``` + +## Test + +Test model based on the best model: +``` +python main.py --test \ + -w output/AVA_SlowFast_FastRcnn/AVA_SlowFast_FastRcnn_best.pdparams \ + -c configs/detection/ava/ava.yaml +``` + + +| architecture | depth | Pretrain Model | frame length x sample rate | MAP | AVA version | model | +| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |------------- | +| SlowFast | R50 | [Kinetics 400](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast_8*8.pdparams) | 8 x 8 | 23.2 | 2.1 | [`link`](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/SlowFastRCNN_AVA.pdparams) | + + +## Inference + +The action detection of this project is divided into two stages. In the first stage, humans' proposals are obtained, and then input into the SlowFast+FasterRCNN model for action recognition. + +For human detection,you can use the trained model in [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection). + +Install PaddleDetection: +``` +cd PaddleDetection/ +pip install -r requirements.txt +!python setup.py install +``` + +Download detection model: +``` +# faster_rcnn_r50_fpn_1x_coco as an example +wget https://paddledet.bj.bcebos.com/models/faster_rcnn_r50_fpn_1x_coco.pdparams +``` + +export model: +``` +python tools/export_model.py \ + -c configs/detection/ava/ava.yaml \ + -o inference_output \ + -p output/AVA_SlowFast_FastRcnn/AVA_SlowFast_FastRcnn_best.pdparams +``` + +inference based on the exported model: +``` +python tools/predict.py \ + -c configs/detection/ava/ava.yaml \ + --input_file "data/-IELREHXDEMO.mp4" \ + --model_file "inference_output/AVA_SlowFast_FastRcnn.pdmodel" \ + --params_file "inference_output/AVA_SlowFast_FastRcnn.pdiparams" \ + --use_gpu=True \ + --use_tensorrt=False +``` diff --git a/docs/en/model_zoo/estimation/adds.md b/docs/en/model_zoo/estimation/adds.md new file mode 100644 index 0000000000000000000000000000000000000000..c055db59d066357f5f16e99f163d60be33e2e3a3 --- /dev/null +++ b/docs/en/model_zoo/estimation/adds.md @@ -0,0 +1,133 @@ +[Simplified Chinese](../../../zh-CN/model_zoo/estimation/adds.md) | English + +# ADDS-DepthNet model + +## content + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + +Before getting started, you need to install additional dependencies as follows: +```bash +python -m pip install scikit-image +python -m pip install matplotlib +``` + +## Introduction + +This model is based on the ICCV 2021 paper **[Self-supervised Monocular Depth Estimation for All Day Images using Domain Separation](https://arxiv.org/abs/2108.07628)** of Baidu Robotics and Autonomous Driving Laboratory, +The self-supervised monocular depth estimation model based on day and night images is reproduced, which utilizes the complementary nature of day and night image data, and slows down the large domain shift of day and night images and the accuracy of depth estimation caused by lighting changes. Impact, the most advanced depth estimation results of all-sky images have been achieved on the challenging Oxford RobotCar data set. + + +## Data + +For data download and preparation of Oxford RobotCar dataset, please refer to [Oxford RobotCar dataset data preparation](../../dataset/Oxford_RobotCar.md) + + +## Train + +### Oxford RobotCar dataset training + +#### Download and add pre-trained models + +1. Download the image pre-training model [resnet18.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/Resnet18_Imagenet.pdparams) as Backbone initialization parameters, or download through the wget command + + ```bash + wget -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.2/Resnet18_Imagenet.pdparams + ``` + +2. Open `PaddleVideo/configs/estimation/adds/adds.yaml`, and fill in the downloaded weight storage path below `pretrained:` + + ```yaml + MODEL: #MODEL field + framework: "DepthEstimator" #Mandatory, indicate the type of network, associate to the'paddlevideo/modeling/framework/'. + backbone: #Mandatory, indicate the type of backbone, associate to the'paddlevideo/modeling/backbones/'. + name: 'ADDS_DepthNet' + pretrained: fill in the path here + ``` + +#### Start training + +- The Oxford RobotCar dataset uses a single card for training, and the starting command for the training method is as follows: + + ```bash + python3.7 main.py --validate -c configs/estimation/adds/adds.yaml --seed 20 + ``` + + +## Test + +- The ADDS-DepthNet model is verified synchronously during training (only the day or night data is verified). You can find the keyword `best` in the training log to obtain the model test accuracy. The log example is as follows: + + ```bash + Already save the best model (rmse)8.5531 + ``` + +- Because the model can only test one day or night data set at a given path in the yaml file at a time, to get the complete test score at the beginning of this document, you need to run 4 test commands and record their indicators ( 40m during the day, 60m during the day, 40m at night, 60m at night) + +- Download URL of the trained model: [ADDS_car.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ADDS_car.pdparams) + +- The test commands are as follows: + + ```bash + # Night 40m + python3.7 main.py --test -c configs/estimation/adds/adds.yaml -w "output/ADDS/ADDS_best.pdparams" -o DATASET.test.file_path="data/oxford/splits/oxford_day/val_night_files.txt" -o MODEL.head.max_gt_depth=40 + + # Night 60m + python3.7 main.py --test -c configs/estimation/adds/adds.yaml -w "output/ADDS/ADDS_best.pdparams" -o DATASET.test.file_path="data/oxford/splits/oxford_day/val_night_files.txt" -o MODEL.head.max_gt_depth=60 + + # Daytime 40m + python3.7 main.py --test -c configs/estimation/adds/adds.yaml -w "output/ADDS/ADDS_best.pdparams" -o DATASET.test.file_path="data/oxford/splits/oxford_day/val_day_files.txt" -o MODEL.head.max_gt_depth=40 + + # Daytime 60m + python3.7 main.py --test -c configs/estimation/adds/adds.yaml -w "output/ADDS/ADDS_best.pdparams" -o DATASET.test.file_path="data/oxford/splits/oxford_day/val_day_files.txt" -o MODEL.head.max_gt_depth=60 + ``` + + The test indicators on the validation dataset of Oxford RobotCar dataset are as follows: + + | version | Max Depth | Abs Rel | Sq Rel | RMSE | RMSE log | | | | + | ----------- | --------- | ------- | ------ | ----- | ------- | ----------------- |------------------- | ------------------- | + | ours(night) | 40 | 0.209 | 1.741 | 6.031 | 0.243 | 0.708 | 0.923 | 0.975 | + | ours(night) | 60 | 0.207 | 2.052 | 7.888 | 0.258 | 0.686 | 0.909 | 0.970 | + | ours(day) | 40 | 0.114 | 0.574 | 3.411 | 0.157 | 0.860 | 0.977 | 0.993 | + | ours(day) | 60 | 0.119 | 0.793 | 4.842 | 0.173 | 0.838 | 0.967 | 0.991 | + +## Inference + +### Export inference model + +```bash +python3.7 tools/export_model.py -c configs/estimation/adds/adds.yaml -p data/ADDS_car.pdparams -o inference/ADDS +``` + +The above command will generate the model structure file `ADDS.pdmodel` and model weight files `ADDS.pdiparams` and `ADDS.pdiparams.info` files needed for prediction, all of which are stored in the `inference/ADDS/` directory + +For the meaning of each parameter in the above bash command, please refer to [Model Inference Method](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/en/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### Use predictive engine inference + +```bash +python3.7 tools/predict.py --input_file data/example.png \ + --config configs/estimation/adds/adds.yaml \ + --model_file inference/ADDS/ADDS.pdmodel \ + --params_file inference/ADDS/ADDS.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +At the end of the inference, the depth map estimated by the model will be saved in pseudo-color by default. + +The following is a sample picture and the corresponding predicted depth map: + +image + +depth + + +## Reference + +- [Self-supervised Monocular Depth Estimation for All Day Images using Domain Separation](https://arxiv.org/abs/2108.07628), Liu, Lina and Song, Xibin and Wang, Mengmeng and Liu, Yong and Zhang, Liangjun diff --git a/docs/en/model_zoo/localization/bmn.md b/docs/en/model_zoo/localization/bmn.md new file mode 100644 index 0000000000000000000000000000000000000000..eb64c670607bc384aac27a05dfbe00c76de7e726 --- /dev/null +++ b/docs/en/model_zoo/localization/bmn.md @@ -0,0 +1,104 @@ +[简体中文 ](../../../zh-CN/model_zoo/localization/bmn.md) | English + +# BMN + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +BMN model contains three modules: Base Module handles the input feature sequence, and out- puts feature sequence shared by the following two modules; Temporal Evaluation Module evaluates starting and ending probabilities of each location in video to generate boundary probability sequences; Proposal Evaluation Module con- tains the BM layer to transfer feature sequence to BM fea- ture map, and contains a series of 3D and 2D convolutional layers to generate BM confidence map. + +

+
+BMN Overview +

+ + +## Data + +We use ActivityNet dataset to train this model,data preparation please refer to [ActivityNet dataset](../../dataset/ActivityNet.md). + + +## Train + +You can start training by such command: + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_bmn main.py --validate -c configs/localization/bmn.yaml +``` + + +## Test + +You can start testing by such command: + +```bash +python main.py --test -c configs/localization/bmn.yaml -w output/BMN/BMN_epoch_00009.pdparams -o DATASET.test_batch_size=1 +``` + +- For now, we only support testing with **single card** and `batch_size=1`. + +- Please download [activity\_net\_1\_3\_new.json](https://paddlemodels.bj.bcebos.com/video_detection/activity_net_1_3_new.json) label file and specify the path to `METRIC.ground_truth_filename` in config file. + +- Args `-w` is used to specifiy the model path,you can download our model in [BMN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/BMN/BMN.pdparams) + + +Test accuracy in ActivityNet1.3: + +| AR@1 | AR@5 | AR@10 | AR@100 | AUC | +| :---: | :---: | :---: | :---: | :---: | +| 33.26 | 49.48 | 56.86 | 75.19 | 67.23% | + + +## Inference + +### export inference model + + To get model architecture file `BMN.pdmodel` and parameters file `BMN.pdiparams`, use: + +```bash +python3.7 tools/export_model.py -c configs/localization/bmn.yaml \ + -p data/BMN.pdparams \ + -o inference/BMN +``` + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +```bash +python3.7 tools/predict.py --input_file data/example_feat.list \ + --config configs/localization/bmn.yaml \ + --model_file inference/BMN/BMN.pdmodel \ + --params_file inference/BMN/BMN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +``` +BMN Inference results of data/example_feat.npy : +{'score': 0.7968077063560486, 'segment': [0.0, 122.9877]} +{'score': 0.49097609519958496, 'segment': [12.423000000000002, 124.23]} +{'score': 0.21395835280418396, 'segment': [39.7536, 122.9877]} +{'score': 0.2106524258852005, 'segment': [0.0, 109.3224]} +{'score': 0.06876271963119507, 'segment': [23.6037, 114.2916]} +``` + +Inference results are saved in `data/bmn/BMN_INFERENCE_results`. + +## Reference + +- [BMN: Boundary-Matching Network for Temporal Action Proposal Generation](https://arxiv.org/abs/1907.09702), Tianwei Lin, Xiao Liu, Xin Li, Errui Ding, Shilei Wen. diff --git a/docs/en/model_zoo/multimodal/actbert.md b/docs/en/model_zoo/multimodal/actbert.md new file mode 100644 index 0000000000000000000000000000000000000000..f884a5e8f33667ddfdbc17591b96fc73caa822d0 --- /dev/null +++ b/docs/en/model_zoo/multimodal/actbert.md @@ -0,0 +1,98 @@ +[简体中文](../../../zh-CN/model_zoo/multimodal/actbert.md) | English + +# ActBERT + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Reference](#Reference) + +Before getting started, you need to install additional dependencies as follows: +```bash +python -m pip install paddlenlp +python -m pip install lmdb +``` + +## Introduction + +Actbert is proposed by Baidu in CVPR2020 for multimodal pretrain task. It leverage global action information to cat- alyze mutual interactions between linguistic texts and local regional objects. This method introduce a TaNgled Transformer block (TNT) to encode three sources of information, i.e., global actions, local regional objects, and linguistic descriptions. ActBERT significantly outperforms the state- of-the-art in five downstream video-and-language tasks, i.e., text-video clip retrieval, video captioning, video question answering, action segmentation, and action step localization. + +
+
+
+ + +## Data + +Please refer to Kinetics400 data download and preparation doc [HowTo100M-data](../../dataset/howto100m.md) + +Please refer to MSR-VTT data download and preparation doc [MSR-VTT-data](../../dataset/umsrvtt.md) + + +## Train + +### Train on HowTo100M + +#### download pretrain-model + +Please download [bert-base-uncased](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/bert-base-uncased.pdparams) as pretraind model: + +```bash +wget https://videotag.bj.bcebos.com/PaddleVideo-release2.2/bert-base-uncased.pdparams +``` + +and add path to `MODEL.framework.backbone.pretrained` in config file as: + +```yaml +MODEL: + framework: "ActBert" + backbone: + name: "BertForMultiModalPreTraining" + pretrained: your weight path +``` + +- We provide training option on small data, config file is for reference only. + +#### Start training + +- Train ActBERT on HowTo100M scripts: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_actbert main.py --validate -c configs/multimodal/actbert/actbert.yaml +``` + +- AMP is useful for speeding up training: + +```bash +export FLAGS_conv_workspace_size_limit=800 #MB +export FLAGS_cudnn_exhaustive_search=1 +export FLAGS_cudnn_batchnorm_spatial_persistent=1 + +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_actbert main.py --amp --validate -c configs/multimodal/actbert/actbert.yaml +``` + + +## Test + +- Evaluation performs on downstream task, i.e. text-video clip retrieval on MSR-VTT dataset, test accuracy can be obtained using scripts: + +```bash +python3.7 main.py --test -c configs/multimodal/actbert/actbert_msrvtt.yaml -w Actbert.pdparams +``` + + +Metrics on MSR-VTT: + +| R@1 | R@5 | R@10 | Median R | Mean R | checkpoints | +| :------: | :----------: | :----: | :----: | :----: | :----: | +| 8.6 | 31.2 | 45.5 | 13.0 | 28.5 | [ActBERT.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ActBERT.pdparams) | + + +## Reference + +- [ActBERT: Learning Global-Local Video-Text Representations +](https://arxiv.org/abs/2011.07231), Linchao Zhu, Yi Yang diff --git a/docs/en/model_zoo/partition/transnetv2.md b/docs/en/model_zoo/partition/transnetv2.md new file mode 100644 index 0000000000000000000000000000000000000000..e98e2c5702ebb2bbc90021bd9707d110e65c5dc0 --- /dev/null +++ b/docs/en/model_zoo/partition/transnetv2.md @@ -0,0 +1,80 @@ +[简体中文](../../../zh-CN/model_zoo/partition/transnetv2.md) | English + +# TransNetV2 + +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Details](#Details) +- [Reference](#Reference) + +Before getting started, you need to install additional dependencies as follows: +```bash +python -m pip install ffmpeg-python==0.2.0 +``` + +## Introduction + +TransNetV2 is a video segmentation model based on deep learning. It performs feature learning through the DDCNN V2 structure, and adds RGB color histograms and video frame similarity for more effective feature extraction, and finally obtains whether each frame is a shot boundary frame Probability, thereby completing the video segmentation. The algorithm has good effect and efficient calculation, which is very suitable for industrial landing. + +![](../../../images/transnetv2.png) + +This code currently only supports model inference, and model training and testing will be provided in the future. + +Please refer to the paper for details. [TransNet V2: An effective deep network architecture for fast shot transition detection](https://arxiv.org/abs/2008.04838) + +## Data + +coming soon + + +## Train + +coming soon + + +## Test + +coming soon + + +## Inference + + +Load the TransNetV2 weights trained on ClipShots and TRECVID IACC.3 dataset [TransNetV2_shots.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TransNetV2_shots.pdparams), or download through the command line + +```bash +wget https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TransNetV2_shots.pdparams +``` + +### export inference model + +```bash +python3.7 tools/export_model.py -c configs/partitioners/transnetv2/transnetv2.yaml -p data/TransNetV2_shots.pdparams -o inference/TransNetV2 +``` + +The above command will generate the model structure file`TransNetV2.pdmodel`and the model weight file`TransNetV2.pdiparams`required for prediction. + +For the meaning of each parameter, please refer to [Model Reasoning Method](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-Model Reasoning) + +### infer + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/partitioners/transnetv2/transnetv2.yaml \ + --model_file inference/TransNetV2/TransNetV2.pdmodel \ + --params_file inference/TransNetV2/TransNetV2.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +By defining the `output_path` parameters in `transnetv2.yaml`, the prediction probability of each frame can be output to `{output_path}/example_predictions.txt`, and the predicted lens boundary is output to `{output_path}/example_scenes.txt`. +By defining the `visualize` parameter in `transnetv2.yaml`, the predicted results can be visualized, and the visual results are saved to `{output_path}/example_vis.png`. + +## Reference + +- [TransNet V2: An effective deep network architecture for fast shot transition detection](https://arxiv.org/abs/2008.04838), Tomáš Souček, Jakub Lokoč diff --git a/docs/en/model_zoo/recognition/agcn.md b/docs/en/model_zoo/recognition/agcn.md new file mode 100644 index 0000000000000000000000000000000000000000..85677c729c07ba32908cd8cf5aac7664d5441b01 --- /dev/null +++ b/docs/en/model_zoo/recognition/agcn.md @@ -0,0 +1,129 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/agcn.md) | English + +# AGCN + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +We implemented Adaptive Graph Convolution Network to improve the accuracy of [ST-GCN](./stgcn.md). + +## Data + +Please refer to FSD-10 data download and preparation doc [FSD](../../dataset/fsd.md) + +Please refer to NTU-RGBD data download and preparation doc [NTU-RGBD](../../dataset/ntu-rgbd.md) + +## Train + +### Train on FSD + +- Train AGCN on FSD scripts: + +```bash +python3.7 main.py -c configs/recognition/agcn/agcn_fsd.yaml +``` + +- Turn off `valid` when training, as validation dataset is not available for the competition. + +### Train on NTU-RGBD + +- Train AGCN on NTU-RGBD scripts: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_agcn main.py --validate -c configs/recognition/agcn/agcn_ntucs.yaml +``` + +- config file `agcn_ntucs.yaml` corresponding to the config of AGCN on NTU-RGB+D dataset with cross-subject splits. + + +## Test + +### Test onf FSD + +- Test scripts: + +```bash +python3.7 main.py --test -c configs/recognition/agcn/agcn_fsd.yaml -w output/AGCN/AGCN_epoch_00100.pdparams +``` + +- Specify the config file with `-c`, specify the weight path with `-w`. + +- Evaluation results will be saved in `submission.csv` file, final score can be obtained in [competition website](https://aistudio.baidu.com/aistudio/competition/detail/115). + +Accuracy on FSD dataset: + +| Test_Data | Top-1 | checkpoints | +| :----: | :----: | :---- | +| Test_A | 62.29 | [AGCN_fsd.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AGCN_fsd.pdparams)| + + +### Test on NTU-RGB+D + +- Test scripts: + +```bash +python3.7 main.py --test -c configs/recognition/agcn/agcn_ntucs.yaml -w output/AGCN/AGCN_best.pdparams +``` + +- Specify the config file with `-c`, specify the weight path with `-w`. + +Accuracy on NTU-RGB+D dataset: + +| split | Top-1 | checkpoints | +| :----: | :----: | :---- | +| cross-subject | 83.27 | [AGCN_ntucs.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AGCN_ntucs.pdparams)| + + +## Inference + +### export inference model + + To get model architecture file `AGCN.pdmodel` and parameters file `AGCN.pdiparams`, use: + +```bash +python3.7 tools/export_model.py -c configs/recognition/agcn/agcn_fsd.yaml \ + -p data/AGCN_fsd.pdparams \ + -o inference/AGCN +``` + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +```bash +python3.7 tools/predict.py --input_file data/fsd10/example_skeleton.npy \ + --config configs/recognition/agcn/agcn_fsd.yaml \ + --model_file inference/AGCN/AGCN.pdmodel \ + --params_file inference/AGCN/AGCN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +``` +Current video file: data/fsd10/example_skeleton.npy + top-1 class: 27 + top-1 score: 0.8965644240379333 +``` + + +## Reference + +- [Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1801.07455), Sijie Yan, Yuanjun Xiong, Dahua Lin + +- [Two-Stream Adaptive Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1805.07694), Lei Shi, Yifan Zhang, Jian Cheng, Hanqing Lu + +- [Skeleton-Based Action Recognition with Multi-Stream Adaptive Graph Convolutional Networks](https://arxiv.org/abs/1912.06971), Lei Shi, Yifan Zhang, Jian Cheng, Hanqing Lu + +- Many thanks to [li7819559](https://github.com/li7819559) and [ZhaoJingjing713](https://github.com/ZhaoJingjing713) for contributing the code. diff --git a/docs/en/model_zoo/recognition/attention_lstm.md b/docs/en/model_zoo/recognition/attention_lstm.md new file mode 100644 index 0000000000000000000000000000000000000000..42fb5d90e0abf6a1194a5e6adcf188c49e3294af --- /dev/null +++ b/docs/en/model_zoo/recognition/attention_lstm.md @@ -0,0 +1,84 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/attention_lstm.md) | English + +# AttentionLSTM + +## content + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + +## Introduction + +Recurrent Neural Networks (RNN) are often used in the processing of sequence data, which can model the sequence information of multiple consecutive frames of video, and are commonly used methods in the field of video classification. +This model uses a two-way long and short-term memory network (LSTM) to encode all the frame features of the video in sequence. Unlike the traditional method that directly uses the output of the last moment of LSTM, this model adds an Attention layer, and the hidden state output at each moment has an adaptive weight, and then linearly weights the final feature vector. The reference paper implements a two-layer LSTM structure, while **this model implements a two-way LSTM with Attention**. + +The Attention layer can refer to the paper [AttentionCluster](https://arxiv.org/abs/1711.09550) + +## Data + +PaddleVide provides training and testing scripts on the Youtube-8M dataset. Youtube-8M data download and preparation please refer to [YouTube-8M data preparation](../../dataset/youtube8m.md) + +## Train + +### Youtube-8M data set training + +#### Start training + +- The Youtube-8M data set uses 8 cards for training. In the feature format, video and audio features will be used as input. The training start command of the data is as follows + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_attetion_lstm main.py --validate -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml + ``` + +## Test + +The command is as follows: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_attetion_lstm main.py --test -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml -w "output/AttentionLSTM/AttentionLSTM_best.pdparams" +``` + +When the test configuration uses the following parameters, the test indicators on the validation data set of Youtube-8M are as follows: + +| Hit@1 | PERR | GAP | checkpoints | +| :-----: | :---------: | :---: | ----- | +| 89.05 | 80.49 | 86.30 | [AttentionLSTM_yt8.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AttentionLSTM_yt8.pdparams) | + +## Inference + +### Export inference model +```bash +python3.7 tools/export_model.py -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml \ + -p data/AttentionLSTM_yt8.pdparams \ + -o inference/AttentionLSTM +``` + +The above command will generate the model structure file `AttentionLSTM.pdmodel` and the model weight file `AttentionLSTM.pdiparams` required for prediction. + +For the meaning of each parameter, please refer to [Model Reasoning Method](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0.0/docs/en/start.md#2-infer) + +### Use prediction engine inference + +```bash +python3.7 tools/predict.py --input_file data/example.pkl \ + --config configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml \ + --model_file inference/AttentionLSTM/AttentionLSTM.pdmodel \ + --params_file inference/AttentionLSTM/AttentionLSTM.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` +An example of the output is as follows: +```bash +Current video file: data/example.pkl + top-1 class: 11 + top-1 score: 0.9841002225875854 +``` +It can be seen that using the AttentionLSTM model trained on Youtube-8M to predict data/example.pkl, the output top1 category id is 11, and the confidence is 0.98. +## Reference paper + +- [Attention Clusters: Purely Attention Based Local Feature Integration for Video Classification](https://arxiv.org/abs/1711.09550), Xiang Long, Chuang Gan, Gerard de Melo, Jiajun Wu, Xiao Liu, Shilei Wen +- [YouTube-8M: A Large-Scale Video Classification Benchmark](https://arxiv.org/abs/1609.08675), Sami Abu-El-Haija, Nisarg Kothari, Joonseok Lee, Paul Natsev, George Toderici, Balakrishnan Varadarajan, Sudheendra Vijayanarasimhan diff --git a/docs/en/model_zoo/recognition/ctrgcn.md b/docs/en/model_zoo/recognition/ctrgcn.md new file mode 100644 index 0000000000000000000000000000000000000000..bdec0aacc7e2c7fbf389c2b8a402e5cab5c1a325 --- /dev/null +++ b/docs/en/model_zoo/recognition/ctrgcn.md @@ -0,0 +1,128 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/ctrgcn.md) | English + +# CTR-GCN + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +[CTRGCN](https://github.com/Uason-Chen/CTR-GCN.git) is a bone based behavior recognition model proposed by iccv 2021. By applying the changes to the graph convolution of human bone data with topological structure, and using spatio-temporal graph convolution to extract spatio-temporal features for behavior recognition, the accuracy of bone based behavior recognition task is greatly improved. + +
+
+
+ + +## Data + +Please refer to NTU-RGBD data download and preparation doc [NTU-RGBD](../../dataset/ntu-rgbd.md) + + +## Train + + +### Train on NTU-RGBD + +- Train CTR-GCN on NTU-RGBD scripts using single gpu: + +```bash +# joint modality +python main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml --seed 1 + +# bone modality +python main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_bone.yaml --seed 1 + +# motion modality +python main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_motion.yaml --seed 1 + +# bone motion modality +python main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_bone_motion.yaml --seed 1 +``` + +- Train CTR-GCN on NTU-RGBD scriptsusing multi gpus: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_ctrgcn main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml +``` + +- config file `ctrgcn_ntucs_joint.yaml` corresponding to the config of CTR-GCN on NTU-RGB+D dataset with cross-subject splits. + + +## Test + +### Test on NTU-RGB+D + +- Test scripts: + +```bash +# joint modality +python3.7 main.py --test -c configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml -w data/CTRGCN_ntucs_joint.pdparams + +# bone modality +python3.7 main.py --test -c configs/recognition/ctrgcn/ctrgcn_ntucs_bone.yaml -w data/CTRGCN_ntucs_bone.pdparams + +# motion modality +python3.7 main.py --test -c configs/recognition/ctrgcn/ctrgcn_ntucs_motion.yaml -w data/CTRGCN_ntucs_motion.pdparams + +# bone motion modality +python3.7 main.py --test -c configs/recognition/ctrgcn/ctrgcn_ntucs_bone_motion.yaml -w data/CTRGCN_ntucs_bone_motion.pdparams +``` + +- Specify the config file with `-c`, specify the weight path with `-w`. + + +Accuracy on NTU-RGB+D dataset: + +| split | modality | Top-1 | checkpoints | +| :----: | :----: | :----: | :----: | +| cross-subject | joint | 89.93 | [CTRGCN_ntucs_joint.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/CTRGCN_ntucs_joint.pdparams) | +| cross-subject | bone | 85.24 | [CTRGCN_ntucs_bone.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/CTRGCN_ntucs_bone.pdparams) | +| cross-subject | motion | 85.33 | [CTRGCN_ntucs_motion.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/CTRGCN_ntucs_motion.pdparams) | +| cross-subject | bone motion | 84.53 | [CTRGCN_ntucs_bone_motion.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/CTRGCN_ntucs_bone_motion.pdparams) | + + +## Inference + +### export inference model + +```bash +python3.7 tools/export_model.py -c configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml \ + -p data/CTRGCN_ntucs_joint.pdparams \ + -o inference/CTRGCN +``` + + To get model architecture file `CTRGCN.pdmodel` and parameters file `CTRGCN.pdiparams`, use: + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +```bash +python3.7 tools/predict.py --input_file data/example_NTU-RGB-D_sketeton.npy \ + --config configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml \ + --model_file inference/CTRGCN_joint/CTRGCN_joint.pdmodel \ + --params_file inference/CTRGCN_joint/CTRGCN_joint.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +``` +Current video file: data/example_NTU-RGB-D_sketeton.npy + top-1 class: 4 + top-1 score: 0.999988317489624 +``` + +## Reference + +- [Channel-wise Topology Refinement Graph Convolution for Skeleton-Based Action Recognition](https://arxiv.org/abs/2107.12213), Chen, Yuxin and Zhang, Ziqi and Yuan, Chunfeng and Li, Bing and Deng, Ying and Hu, Weiming diff --git a/docs/en/model_zoo/recognition/movinet.md b/docs/en/model_zoo/recognition/movinet.md new file mode 100644 index 0000000000000000000000000000000000000000..317501938d580cf1cfe435fc0133d80fb6f18481 --- /dev/null +++ b/docs/en/model_zoo/recognition/movinet.md @@ -0,0 +1,91 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/movinet.md) | English + +# MoViNet + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + +## Introduction + +Movinet is a mobile video network developed by Google research. It uses causal convolution operator with stream buffer and temporal ensembles to improve accuracy. It is a lightweight and efficient video model that can be used for online reasoning video stream. + + +## Data + +Please refer to Kinetics400 data download and preparation doc [k400-data](../../dataset/K400.md) + + +## Train + +- Train MoViNet on kinetics-400 scripts: + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_movinet main.py --validate -c configs/recognition/movinet/movinet_k400_frame.yaml +``` + +## Test + +- For uniform sampling, test accuracy can be found in training-logs by search key word `best`, such as: + +```txt +Already save the best model (top1 acc)0.6489 +``` + +- Test scripts: + +```bash +python3.7 main.py --test -c configs/recognition/movinet/movinet_k400_frame.yaml -w output/MoViNet/MoViNet_best.pdparams +``` + + +Accuracy on Kinetics400: + +| Config | Sampling method | num_seg | target_size | Top-1 | checkpoints | +| :------: | :--------: | :-------: | :-------: | :-----: | :-----: | +| A0 | Uniform | 50 | 172 | 66.62 | [MoViNetA0_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/MoViNetA0_k400.pdparams) | + +## Inference + +### export inference model + + To get model architecture file `MoViNetA0.pdmodel` and parameters file `MoViNetA0.pdiparams`, use: + +```bash +python3.7 tools/export_model.py -c configs/recognition/movinet/movinet_k400_frame.yaml \ + -p data/MoViNetA0_k400.pdparams \ + -o inference/MoViNetA0 +``` + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/movinet/movinet_k400_frame.yaml \ + --model_file inference/MoViNetA0/MoViNet.pdmodel \ + --params_file inference/MoViNetA0/MoViNet.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.7667049765586853 +``` + +## Reference + +- [MoViNets: Mobile Video Networks for Efficient Video Recognition](https://arxiv.org/abs/2103.11511) diff --git a/docs/en/model_zoo/recognition/pp-timesformer.md b/docs/en/model_zoo/recognition/pp-timesformer.md new file mode 100644 index 0000000000000000000000000000000000000000..9acbc6487e7c83b7f416469450b3373674009728 --- /dev/null +++ b/docs/en/model_zoo/recognition/pp-timesformer.md @@ -0,0 +1,156 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/pp-timesformer.md) | English + +# TimeSformer Video Classification Model + +## Content + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +We have improved the [TimeSformer model](./timesformer.md) and obtained a more accurate 2D practical video classification model **PP-TimeSformer**. Without increasing the amount of parameters and calculations, the accuracy on the UCF-101, Kinetics-400 and other data sets significantly exceeds the original version. The accuracy on the Kinetics-400 data set is shown in the table below. + +| Version | Top1 | +| :------ | :----: | +| Ours ([swa](#refer-anchor-1)+distill+16frame) | 79.44 | +| Ours ([swa](#refer-anchor-1)+distill) | 78.87 | +| Ours ([swa](#refer-anchor-1)) | **78.61** | +| [mmaction2](https://github.com/open-mmlab/mmaction2/tree/master/configs/recognition/timesformer#kinetics-400) | 77.92 | + + +## Data + +K400 data download and preparation please refer to [Kinetics-400 data preparation](../../dataset/k400.md) + +UCF101 data download and preparation please refer to [UCF-101 data preparation](../../dataset/ucf101.md) + + +## Train + +### Kinetics-400 data set training + +#### Download and add pre-trained models + +1. Download the image pre-training model [ViT_base_patch16_224_miil_21k.pdparams](https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams) as Backbone initialization parameters, or download through wget command + + ```bash + wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams + ``` + +2. Open `PaddleVideo/configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml`, and fill in the downloaded weight storage path below `pretrained:` + + ```yaml + MODEL: + framework: "RecognizerTransformer" + backbone: + name: "VisionTransformer_tweaks" + pretrained: fill in the path here + ``` + +#### Start training + +- The Kinetics400 data set uses 8 cards for training, and the start command of the training method is as follows: + + ```bash + # videos data format + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --validate -c configs/recognition/ pptimesformer/pptimesformer_k400_videos.yaml + ``` + +- Turn on amp mixed-precision training to speed up the training process. The training start command is as follows: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + # videos data format + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --amp --validate -c configs /recognition/pptimesformer/pptimesformer_k400_videos.yaml + ``` + +- In addition, you can customize and modify the parameter configuration to achieve the purpose of training/testing on different data sets. It is recommended that the naming method of the configuration file is `model_dataset name_file format_data format_sampling method.yaml` , Please refer to [config](../../tutorials/config.md) for parameter usage. + + +## Test + +- The PP-TimeSformer model is verified synchronously during training. You can find the keyword `best` in the training log to obtain the model test accuracy. The log example is as follows: + + ``` + Already save the best model (top1 acc)0.7258 + ``` + +- Because the sampling method of the PP-TimeSformer model test mode is a slightly slower but higher accuracy **UniformCrop**, which is different from the **RandomCrop** used in the verification mode during the training process, so the verification index recorded in the training log` topk Acc` does not represent the final test score, so after the training is completed, you can use the test mode to test the best model to obtain the final index. The command is as follows: + + ```bash + # 8-frames testing script + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --test -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml -w "output/ppTimeSformer/ppTimeSformer_best.pdparams" + + # 16-frames testing script + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --test \ + -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml \ + -o MODEL.backbone.num_seg=16 \ + -o MODEL.runtime_cfg.test.num_seg=16 \ + -o PIPELINE.test.decode.num_seg=16 \ + -o PIPELINE.test.sample.num_seg=16 \ + -w "data/ppTimeSformer_k400_16f_distill.pdparams" + ``` + + + When the test configuration uses the following parameters, the test indicators on the validation data set of Kinetics-400 are as follows: + + | backbone | Sampling method | num_seg | target_size | Top-1 | checkpoints | + | :----------------: | :-------------: | :-----: | :---------: | :---- | :----------------------------------------------------------: | + | Vision Transformer | UniformCrop | 8 | 224 | 78.61 | [ppTimeSformer_k400_8f.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTimeSformer_k400_8f.pdparams) | + | Vision Transformer | UniformCrop | 8 | 224 | 78.87 | [ppTimeSformer_k400_8f_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTimeSformer_k400_8f_distill.pdparams) | + | Vision Transformer | UniformCrop | 16 | 224 | 79.44 | [ppTimeSformer_k400_16f_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTimeSformer_k400_16f_distill.pdparams) | + + +- During the test, the PP-TimeSformer video sampling strategy is to use linspace sampling: in time sequence, from the first frame to the last frame of the video sequence to be sampled, `num_seg` sparse sampling points (including endpoints) are uniformly generated; spatially , Select 3 areas to sample at both ends of the long side and the middle position (left, middle, right or top, middle, and bottom). A total of 1 clip is sampled for 1 video. + +## Inference + +### Export inference model + +```bash +python3.7 tools/export_model.py -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml \ + -p data/ppTimeSformer_k400_8f.pdparams \ + -o inference/ppTimeSformer +``` + +The above command will generate the model structure file `ppTimeSformer.pdmodel` and the model weight file `ppTimeSformer.pdiparams` required for prediction. + +- For the meaning of each parameter, please refer to [Model Reasoning Method](../../start.md#2-Model Reasoning) + +### Use predictive engine inference + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml \ + --model_file inference/ppTimeSformer/ppTimeSformer.pdmodel \ + --params_file inference/ppTimeSformer/ppTimeSformer.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +The output example is as follows: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9997474551200867 +``` + +It can be seen that using the ppTimeSformer model trained on Kinetics-400 to predict `data/example.avi`, the output top1 category id is `5`, and the confidence is 0.99. By referring to the category id and name correspondence table `data/k400/Kinetics-400_label_list.txt`, it can be known that the predicted category name is `archery`. + +## Reference + +- [Is Space-TimeAttention All You Need for Video Understanding?](https://arxiv.org/pdf/2102.05095.pdf), Gedas Bertasius, Heng Wang, Lorenzo Torresani +- [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531), Geoffrey Hinton, Oriol Vinyals, Jeff Dean +
+ +- [Averaging Weights Leads to Wider Optima and Better Generalization](https://arxiv.org/abs/1803.05407v3), Pavel Izmailov, Dmitrii Podoprikhin, Timur Garipov +- [ImageNet-21K Pretraining for the Masses](https://arxiv.org/pdf/2104.10972v4.pdf), Tal Ridnik, Emanuel Ben-Baruch, Asaf Noy diff --git a/docs/en/model_zoo/recognition/pp-tsm.md b/docs/en/model_zoo/recognition/pp-tsm.md new file mode 100644 index 0000000000000000000000000000000000000000..b1ae1aa591b289e1b67518ea8301deff0dbf63a5 --- /dev/null +++ b/docs/en/model_zoo/recognition/pp-tsm.md @@ -0,0 +1,167 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/pp-tsm.md) | English + +# PP-TSM + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + +## Introduction + +We optimized TSM model and proposed **PP-TSM** in this repo. Without increasing the number of parameters, the accuracy of TSM was significantly improved in UCF101 and Kinetics-400 datasets. Please refer to [**Tricks on PP-TSM**](https://zhuanlan.zhihu.com/p/382134297) for more details. + +| Version | Sampling method | Top1 | +| :------ | :----------: | :----: | +| Ours (distill) | Dense | **76.16** | +| Ours | Dense | 75.69 | +| [mmaction2](https://github.com/open-mmlab/mmaction2/blob/master/configs/recognition/tsm/README.md) | Dense | 74.55 | +| [mit-han-lab](https://github.com/mit-han-lab/temporal-shift-module) | Dense | 74.1 | + + +| Version | Sampling method | Top1 | +| :------ | :----------: | :----: | +| Ours (distill) | Uniform | **75.11** | +| Ours | Uniform | 74.54 | +| [mmaction2](https://github.com/open-mmlab/mmaction2/blob/master/configs/recognition/tsm/README.md) | Uniform | 71.90 | +| [mit-han-lab](https://github.com/mit-han-lab/temporal-shift-module) | Uniform | 71.16 | + + +## Data + +Please refer to Kinetics400 data download and preparation doc [k400-data](../../dataset/K400.md) + +Please refer to UCF101 data download and preparation doc [ucf101-data](../../dataset/ucf101.md) + + +## Train + +### Train on kinetics-400 + +#### download pretrain-model + +Please download [ResNet50_vd_ssld_v2](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams) as pretraind model: + +```bash +wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams +``` + +and add path to `MODEL.framework.backbone.pretrained` in config file as: + +```yaml +MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTweaksTSM" + pretrained: your weight path +``` + +- If use ResNet101 as backbone, please download [ResNet101_vd_ssld_pretrained.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ResNet101_vd_ssld_pretrained.pdparams) as pretraind model. + +#### Start training + +- Train PP-TSM on kinetics-400 scripts: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +``` + +- Train PP-TSM on kinetics-400 video data using scripts: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_videos_uniform.yaml +``` + +- AMP is useful for speeding up training: + +```bash +export FLAGS_conv_workspace_size_limit=800 #MB +export FLAGS_cudnn_exhaustive_search=1 +export FLAGS_cudnn_batchnorm_spatial_persistent=1 + +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --amp --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +``` + +- Train PP-TSM on kinetics-400 with dense sampling: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_dense.yaml +``` + +- Train PP-TSM on kinetics-400 with ResNet101 as backbone using dense sampling: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_dense_r101.yaml +``` + + +## Test + +- For uniform sampling, test accuracy can be found in training-logs by search key word `best`, such as: + +```txt +Already save the best model (top1 acc)0.7454 +``` + +- For dense sampling, test accuracy can be obtained using scripts: + +```bash +python3 main.py --test -c configs/recognition/pptsm/pptsm_k400_frames_dense.yaml -w output/ppTSM/ppTSM_best.pdparams +``` + + +Accuracy on Kinetics400: + +| backbone | distill | Sampling method | num_seg | target_size | Top-1 | checkpoints | +| :------: | :----------: | :----: | :----: | :----: | :----: | :---- | +| ResNet50 | False | Uniform | 8 | 224 | 74.54 | [ppTSM_k400_uniform.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_uniform.pdparams) | +| ResNet50 | False | Dense | 8 | 224 | 75.69 | [ppTSM_k400_dense.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_dense.pdparams) | +| ResNet50 | True | Uniform | 8 | 224 | 75.11 | [ppTSM_k400_uniform_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_uniform_distill.pdparams) | +| ResNet50 | True | Dense | 8 | 224 | 76.16 | [ppTSM_k400_dense_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_dense_distill.pdparams) | +| ResNet101 | True | Uniform | 8 | 224 | 76.35 | [ppTSM_k400_uniform_distill_r101.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSM_k400_uniform_distill_r101.pdparams) | +| ResNet101 | False | Dense | 8 | 224 | 77.15 | [ppTSM_k400_dense_r101.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSM_k400_dense_r101.pdparams) | + +## Inference + +### export inference model + + To get model architecture file `ppTSM.pdmodel` and parameters file `ppTSM.pdiparams`, use: + +```bash +python3.7 tools/export_model.py -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml \ + -p data/ppTSM_k400_uniform.pdparams \ + -o inference/ppTSM +``` + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml \ + --model_file inference/ppTSM/ppTSM.pdmodel \ + --params_file inference/ppTSM/ppTSM.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9907386302947998 +``` + +we can get the class name using class id and map file `data/k400/Kinetics-400_label_list.txt`. The top1 prediction of `data/example.avi` is `archery`. + +## Reference + +- [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf), Ji Lin, Chuang Gan, Song Han +- [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531), Geoffrey Hinton, Oriol Vinyals, Jeff Dean diff --git a/docs/en/model_zoo/recognition/pp-tsn.md b/docs/en/model_zoo/recognition/pp-tsn.md new file mode 100644 index 0000000000000000000000000000000000000000..68d9215fd92fc843ef5200f90bad859284797683 --- /dev/null +++ b/docs/en/model_zoo/recognition/pp-tsn.md @@ -0,0 +1,146 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/pp-tsn.md) | English + +# PP-TSN + +## Content + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +We have improved the [TSN model](./tsn.md) and obtained a more accurate 2D practical video classification model **PP-TSN**. Without increasing the amount of parameters and calculations, the accuracy on the UCF-101, Kinetics-400 and other data sets significantly exceeds the original version. The accuracy on the Kinetics-400 data set is shown in the following table. + +| Version | Top1 | +| :------ | :----: | +| Ours (distill) | 75.06 | +| Ours | **73.68** | +| [mmaction2](https://github.com/open-mmlab/mmaction2/tree/master/configs/recognition/tsn#kinetics-400) | 71.80 | + + +## Data + +K400 data download and preparation please refer to [Kinetics-400 data preparation](../../dataset/k400.md) + +UCF101 data download and preparation please refer to [UCF-101 data preparation](../../dataset/ucf101.md) + + +## Train + +### Kinetics-400 data set training + +#### Download and add pre-trained models + +1. Download the image distillation pre-training model [ResNet50_vd_ssld_v2.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams) as the Backbone initialization parameter, or download it through wget + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams + ``` + +2. Open `PaddleVideo/configs/recognition/pptsn/pptsn_k400_frames.yaml`, and fill in the downloaded weight storage path below `pretrained:` + + ```yaml + MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTweaksTSN" + pretrained: fill in the path here + ``` + +#### Start training + +- The Kinetics400 data set uses 8 cards for training, and the start command of the training method is as follows: + + ```bash + # frames data format + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --validate -c configs/recognition/ pptsn/pptsn_k400_frames.yaml + + # videos data format + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --validate -c configs/recognition/ pptsn/pptsn_k400_videos.yaml + ``` + +- Turn on amp mixed-precision training to speed up the training process. The training start command is as follows: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + # frames data format + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --amp --validate -c configs /recognition/pptsn/pptsn_k400_frames.yaml + + # videos data format + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --amp --validate -c configs /recognition/pptsn/pptsn_k400_videos.yaml + ``` + +- In addition, you can customize and modify the parameter configuration to achieve the purpose of training/testing on different data sets. It is recommended that the naming method of the configuration file is `model_dataset name_file format_data format_sampling method.yaml` , Please refer to [config](../../tutorials/config.md) for parameter usage. + + +## Test + +- The PP-TSN model is verified during training. You can find the keyword `best` in the training log to obtain the model test accuracy. The log example is as follows: + + ``` + Already save the best model (top1 acc)0.7004 + ``` + +- Since the sampling method of the PP-TSN model test mode is **TenCrop**, which is slightly slower but more accurate, it is different from the **CenterCrop** used in the verification mode during the training process, so the verification index recorded in the training log is `topk Acc `Does not represent the final test score, so after the training is completed, you can use the test mode to test the best model to obtain the final index, the command is as follows: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --test -c configs/recognition/ pptsn/pptsn_k400_frames.yaml -w "output/ppTSN/ppTSN_best.pdparams" + ``` + + When the test configuration uses the following parameters, the test indicators on the validation data set of Kinetics-400 are as follows: + + | backbone | Sampling method | distill | num_seg | target_size | Top-1 | checkpoints | + | :------: | :-------------: | :-----: | :-----: | :---------: | :---- | :---------------------: | + | ResNet50 | TenCrop | False | 3 | 224 | 73.68 | [ppTSN_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSN_k400.pdparams) | + | ResNet50 | TenCrop | True | 8 | 224 | 75.06 | [ppTSN_k400_8.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSN_k400_8.pdparams) | + +- The PP-TSN video sampling strategy is TenCrop sampling: in time sequence, the input video is evenly divided into num_seg segments, and the middle position of each segment is sampled 1 frame; spatially, from the upper left corner, upper right corner, center point, lower left corner, and lower right corner Each of the 5 sub-regions sampled an area of 224x224, and the horizontal flip was added to obtain a total of 10 sampling results. A total of 1 clip is sampled for 1 video. + +- Distill is `True`, which means that the pre-trained model obtained by distillation is used. For the specific distillation scheme, please refer to [ppTSM Distillation Scheme](). + + +## Inference + +### Export inference model + +```bash +python3.7 tools/export_model.py -c configs/recognition/pptsn/pptsn_k400_frames.yaml -p data/ppTSN_k400.pdparams -o inference/ppTSN +``` + +The above command will generate the model structure file `ppTSN.pdmodel` and model weight files `ppTSN.pdiparams` and `ppTSN.pdiparams.info` files required for prediction, all of which are stored in the `inference/ppTSN/` directory + +For the meaning of each parameter in the above bash command, please refer to [Model Reasoning Method](https://github.com/HydrogenSulfate/PaddleVideo/blob/PPTSN-v1/docs/en/start.md#2-infer) + +### Use prediction engine inference + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/pptsn/pptsn_k400_frames.yaml \ + --model_file inference/ppTSN/ppTSN.pdmodel \ + --params_file inference/ppTSN/ppTSN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +The output example is as follows: + +```bash +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.998979389667511 +``` + +It can be seen that using the PP-TSN model trained on Kinetics-400 to predict `data/example.avi`, the output top1 category id is `5`, and the confidence is 0.99. By consulting the category id and name correspondence table `data/k400/Kinetics-400_label_list.txt`, it can be known that the predicted category name is `archery`. + +## Reference + +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/pdf/1608.00859.pdf), Limin Wang, Yuanjun Xiong, Zhe Wang +- [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531), Geoffrey Hinton, Oriol Vinyals, Jeff Dean diff --git a/docs/en/model_zoo/recognition/slowfast.md b/docs/en/model_zoo/recognition/slowfast.md new file mode 100644 index 0000000000000000000000000000000000000000..45259f0f7ac34ccd95dd115ab7b0d81dcf2510ed --- /dev/null +++ b/docs/en/model_zoo/recognition/slowfast.md @@ -0,0 +1,120 @@ +[简体中文 ](../../../zh-CN/model_zoo/recognition/slowfast.md) | English + +# SlowFast + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +SlowFast involves (i) a Slow pathway, operating at low frame rate, to capture spatial semantics, and (ii) a Fast path-way, operating at high frame rate, to capture motion at fine temporal resolution. The Fast pathway can be made very lightweight by reducing its channel capacity, yet can learn useful temporal information for video recognition. + +

+
+SlowFast Overview +

+ + +## Data + +We use Kinetics-400 to train this model,data preparation please refer to [Kinetics-400 dataset](../../dataset/k400.md). + + +## Train + +You can start training by: + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +python -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_slowfast main.py --validate -c configs/recognition/slowfast/slowfast.yaml +``` + +- Training would be efficent using our code. The training speed is 2x faster than the original implementation. Details can refer to [benchmark](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/en/benchmark.md). + +### Speed up training + +It's time consuming to train SlowFast model. So we implement [Multigrid training stragety](https://arxiv.org/abs/1912.00998) to speed up training. Training script: + +```bash +python -B -m paddle.distributed.launch --selected_gpus="0,1,2,3,4,5,6,7" --log_dir=log-slowfast main.py --validate --multigrid -c configs/recognition/slowfast/slowfast_multigrid.yaml +``` + +Performance evaluation: + +| training stragety | time cost of one epoch/min | total training time/min | speed-up | +| :------ | :-----: | :------: |:------: | +| Multigrid | 27.25 | 9758 (6.7 days) | 2.89x | +| Normal | 78.76 | 15438 (10.7days) | base | + +For more details, please refer to [accelerate doc](https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/tutorials/accelerate.md#%E8%AE%AD%E7%BB%83%E7%AD%96%E7%95%A5%E5%8A%A0%E9%80%9F). + + +## Test + +You can start testing by: + +```bash +python -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_slowfast_test main.py --test -c configs/recognition/slowfast/slowfast.yaml -w output/SlowFast/SlowFast_epoch_000196.pdparams +``` + +- Args `-w` is used to specifiy the model path,you can download our model in [SlowFast.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast.pdparams). + + +Test accuracy in Kinetics-400: + +| Configs | Acc1 | Acc5 | Weights | +| :---: | :---: | :---: | :---: | +| [slowfast.yaml](../../../../configs/recognition/slowfast/slowfast.yaml) | 74.35 | 91.33 | [slowfast_4x16.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast.pdparams) | +| [slowfast_multigrid.yaml](../../../../configs/recognition/slowfast/slowfast_multigrid.yaml) | 75.84 | 92.33 | [slowfast_8x8.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast_8*8.pdparams) | + +- Acc1 may be lower than that released in papaer, as ~5% data of kinetics-400 is missing. Experiments have verified that if training with the same data, we can get the same accuracy. + + +## Inference + +### export inference model + + To get model architecture file `SlowFast.pdmodel` and parameters file `SlowFast.pdiparams`, use: + +```bash +python3.7 tools/export_model.py -c configs/recognition/slowfast/slowfast.yaml \ + -p data/SlowFast.pdparams \ + -o inference/SlowFast +``` + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/slowfast/slowfast.yaml \ + --model_file inference/SlowFast/SlowFast.pdmodel \ + --params_file inference/SlowFast/SlowFast.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 1.0 +``` + +we can get the class name using class id and map file `data/k400/Kinetics-400_label_list.txt`. The top1 prediction of `data/example.avi` is `archery`. + + +## Reference + +- [SlowFast Networks for Video Recognition](https://arxiv.org/abs/1812.03982), Feichtenhofer C, Fan H, Malik J, et al. diff --git a/docs/en/model_zoo/recognition/stgcn.md b/docs/en/model_zoo/recognition/stgcn.md new file mode 100644 index 0000000000000000000000000000000000000000..14585e5e4d491394fc0ab11692e9631ccfec9a82 --- /dev/null +++ b/docs/en/model_zoo/recognition/stgcn.md @@ -0,0 +1,129 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/stgcn.md) | English + +# ST-GCN + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +ST-GCN is skeleton-based action recognition model proposed in AAAI 2018. + +
+
+
+ + +## Data + +Please refer to FSD data download and preparation doc [FSD](../../dataset/fsd.md) + +Please refer to NTU-RGBD data download and preparation doc [NTU-RGBD](../../dataset/ntu-rgbd.md) + + +## Train + +### Train on FSD + +- Train ST-GCN on FSD scripts: + +```bash +python3.7 main.py -c configs/recognition/stgcn/stgcn_fsd.yaml +``` + +- Turn off `valid` when training, as validation dataset is not available for the competition. + +### Train on NTU-RGBD + +- Train ST-GCN on NTU-RGBD scripts: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_stgcn main.py --validate -c configs/recognition/stgcn/stgcn_ntucs.yaml +``` + +- config file `stgcn_ntucs.yaml` corresponding to the config of ST-GCN on NTU-RGB+D dataset with cross-subject splits. + + +## Test + +### Test on FSD + +- Test scripts: + +```bash +python3.7 main.py --test -c configs/recognition/stgcn/stgcn_fsd.yaml -w output/STGCN/STGCN_epoch_00090.pdparams +``` + +- Specify the config file with `-c`, specify the weight path with `-w`. + +- Evaluation results will be saved in `submission.csv` file, final score can be obtained in [competition website](https://aistudio.baidu.com/aistudio/competition/detail/115). + +Accuracy on FSD-10 dataset: + +Test_Data| Top-1 | checkpoints | +| :----: | :----: | :---- | +| Test_A | 59.07 | [STGCN_fsd.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/STGCN_fsd.pdparams) | + + +### Test on NTU-RGB+D + +- Test scripts: + +```bash +python3.7 main.py --test -c configs/recognition/stgcn/stgcn_ntucs.yaml -w output/STGCN/STGCN_best.pdparams +``` + +- Specify the config file with `-c`, specify the weight path with `-w`. + + +Accuracy on NTU-RGB+D dataset: + +| split | Top-1 | checkpoints | +| :----: | :----: | :---- | +| cross-subject | 82.28 | [STGCN_ntucs.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/STGCN_ntucs.pdparams) | + + +## Inference + +### export inference model + + To get model architecture file `STGCN.pdmodel` and parameters file `STGCN.pdiparams`, use: + +```bash +python3.7 tools/export_model.py -c configs/recognition/stgcn/stgcn_fsd.yaml \ + -p data/STGCN_fsd.pdparams \ + -o inference/STGCN +``` + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +```bash +python3.7 tools/predict.py --input_file data/fsd10/example_skeleton.npy \ + --config configs/recognition/stgcn/stgcn_fsd.yaml \ + --model_file inference/STGCN/STGCN.pdmodel \ + --params_file inference/STGCN/STGCN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +``` +Current video file: data/fsd10/example_skeleton.npy + top-1 class: 27 + top-1 score: 0.9912770986557007 +``` + +## Reference + +- [Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1801.07455), Sijie Yan, Yuanjun Xiong, Dahua Lin diff --git a/docs/en/model_zoo/recognition/timesformer.md b/docs/en/model_zoo/recognition/timesformer.md new file mode 100644 index 0000000000000000000000000000000000000000..c004b9b0457b6e8006c7561bde80795d42dd4ed9 --- /dev/null +++ b/docs/en/model_zoo/recognition/timesformer.md @@ -0,0 +1,137 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/timesformer.md) | English + +# TimeSformer + +## Content + +- [Introduction](#Introduction) +- [Data](#DATA) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +TimeSformer is a video classification model based on vision transformer, which has the characteristics of no convolution, global receptive field, and strong time series modeling ability. At present, it has achieved SOTA accuracy on the Kinetics-400 data set, surpassing the classic CNN-based video classification models TSN, TSM and Slowfast, and has a shorter training time (the Kinetics-400 data set training time is 39 hourss). **This code implements the time-space separated attention cascade network in the paper**. + +
+image-20210628210446041image-20210628210446041 +
+ + +## Data + +K400 data download and preparation please refer to [Kinetics-400 data preparation](../../dataset/k400.md) + +UCF101 data download and preparation please refer to [UCF-101 data preparation](../../dataset/ucf101.md) + + +## Train + +### Kinetics-400 data set training + +#### Download and add pre-trained models + +1. Download the image pre-training model [ViT_base_patch16_224](https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams) as Backbone initialization parameters, or download through the wget command + + ```bash + wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams + ``` + +2. Open `PaddleVideo/configs/recognition/timesformer/timesformer_k400_videos.yaml`, and fill in the downloaded weight storage path below `pretrained:` + + ```yaml + MODEL: + framework: "RecognizerTransformer" + backbone: + name: "VisionTransformer" + pretrained: fill in the path here + ``` + +#### Start training + +- The Kinetics400 data set uses 8 cards for training, and the start command of the training method is as follows: + +```bash +# videos data format +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_timesformer main.py --validate -c configs/recognition/ timesformer/timesformer_k400_videos.yaml +``` + +- Turn on amp mixed-precision training to speed up the training process. The training start command is as follows: + +```bash +export FLAGS_conv_workspace_size_limit=800 # MB +export FLAGS_cudnn_exhaustive_search=1 +export FLAGS_cudnn_batchnorm_spatial_persistent=1 +# videos data format +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_timesformer main.py --amp --validate -c configs/recognition/ timesformer/timesformer_k400_videos.yaml +``` + +- In addition, you can customize and modify the parameter configuration to achieve the purpose of training/testing on different data sets. It is recommended that the naming method of the configuration file is `model_dataset name_file format_data format_sampling method.yaml` , Please refer to [config](../../tutorials/config.md) for parameter usage. + + +## Test + +- The TimeSformer model is verified synchronously during training. You can find the keyword `best` in the training log to obtain the model test accuracy. The log example is as follows: + + ``` + Already save the best model (top1 acc)0.7258 + ``` + +- Since the sampling method of the TimeSformer model test mode is **UniformCrop** with a slower speed but higher accuracy, which is different from the **RandomCrop** used in the verification mode during the training process, so the verification index recorded in the training log is `topk Acc `Does not represent the final test score, so after the training is completed, you can use the test mode to test the best model to obtain the final index, the command is as follows: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_timesformer main.py --test -c configs/recognition/ timesformer/timesformer_k400_videos.yaml -w "output/TimeSformer/TimeSformer_best.pdparams" + ``` + + + When the test configuration uses the following parameters, the test indicators on the validation data set of Kinetics-400 are as follows: + + + | backbone | Sampling method | num_seg | target_size | Top-1 | checkpoints | + | :----------------: | :-----: | :-----: | :---------: | :----: | :----------------------------------------------------------: | + | Vision Transformer | UniformCrop | 8 | 224 | 77.29 | [TimeSformer_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TimeSformer_k400.pdparams) | + + +- During the test, the TimeSformer video sampling strategy is to use Linspace sampling: in time sequence, num_seg sparse sampling points are uniformly generated from the video sequence to be sampled; in space, select the two ends of the long side and the middle position (left middle right or top middle bottom) 3 regions are sampled. A total of 1 clip is sampled for 1 video. + +## Inference + +### Export inference model + +```bash +python3.7 tools/export_model.py -c configs/recognition/timesformer/timesformer_k400_videos.yaml \ + -p data/TimeSformer_k400.pdparams \ + -o inference/TimeSformer +``` + +The above command will generate the model structure file `TimeSformer.pdmodel` and the model weight file `TimeSformer.pdiparams` required for prediction. + +- For the meaning of each parameter, please refer to [Model Reasoning Method](../../start.md#2-infer) + +### Use prediction engine inference + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/timesformer/timesformer_k400_videos.yaml \ + --model_file inference/TimeSformer/TimeSformer.pdmodel \ + --params_file inference/TimeSformer/TimeSformer.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +The output example is as follows: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9999722242355347 +``` + +It can be seen that using the TimeSformer model trained on Kinetics-400 to predict `data/example.avi`, the output top1 category id is `5`, and the confidence is 0.99. By consulting the category id and name correspondence table `data/k400/Kinetics-400_label_list.txt`, it can be seen that the predicted category name is `archery`. + +## Reference + +- [Is Space-Time Attention All You Need for Video Understanding?](https://arxiv.org/pdf/2102.05095.pdf), Gedas Bertasius, Heng Wang, Lorenzo Torresani diff --git a/docs/en/model_zoo/recognition/tsm.md b/docs/en/model_zoo/recognition/tsm.md new file mode 100644 index 0000000000000000000000000000000000000000..e44ea6be5d976f0437971add6550e8c0cac68de0 --- /dev/null +++ b/docs/en/model_zoo/recognition/tsm.md @@ -0,0 +1,221 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/tsm.md) | English + +# TSM + +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Details](#Details) +- [Reference](#Reference) + +## Introduction + +Temporal Shift Module (TSM) is a popular model that attracts more attention at present. +The method of moving through channels greatly improves the utilization ability of temporal information without increasing any +additional number of parameters and calculation amount. +Moreover, due to its lightweight and efficient characteristics, it is very suitable for industrial landing. + +
+
+
+ + +This code implemented **single RGB stream** of TSM networks. Backbone is ResNet-50. + +Please refer to the ICCV 2019 paper for details [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf) + +## Data + +Please refer to Kinetics-400 data download and preparation [k400 data preparation](../../dataset/k400.md) + +Please refer to UCF101 data download and preparation [ucf101 data preparation](../../dataset/ucf101.md) + + +## Train + +### Train on the Kinetics-400 dataset + +#### download pretrain-model + +1. Please download [ResNet50_pretrain.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams) as pretraind model: + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams + ``` + +2. Open `PaddleVideo/configs/recognition/tsm/tsm_k400_frames.yaml`, and fill in the downloaded weight path below `pretrained:` + + ```bash + MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTSM" + pretrained: your weight path + ``` + +#### Start training + +- By specifying different configuration files, different data formats/data sets can be used for training. Taking the training configuration of Kinetics-400 data set + 8 cards + frames format as an example, the startup command is as follows (more training commands can be viewed in `PaddleVideo/run.sh`). + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_k400_frames.yaml + ``` + +- Training Kinetics-400 dataset of videos format using scripts. + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_k400_videos.yaml + ``` + +- AMP is useful for speeding up training, scripts as follows: + +```bash +export FLAGS_conv_workspace_size_limit=800 #MB +export FLAGS_cudnn_exhaustive_search=1 +export FLAGS_cudnn_batchnorm_spatial_persistent=1 + +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_k400_frames.yaml +``` + +- AMP works better with `NHWC` data format, scripts as follows: + +```bash +export FLAGS_conv_workspace_size_limit=800 #MB +export FLAGS_cudnn_exhaustive_search=1 +export FLAGS_cudnn_batchnorm_spatial_persistent=1 + +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_k400_frames_nhwc.yaml +``` + +- For the config file usage,please refer to [config](../../tutorials/config.md). + +### Train on UCF-101 dataset + +#### download pretrain-model + +- Load the TSM model we trained on Kinetics-400 [TSM_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams), or download it through the command line + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams + ``` + +- Open `PaddleVideo/configs/recognition/tsm/tsm_ucf101_frames.yaml`, and fill in the downloaded weight path below `pretrained:` + + ```bash + MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTSM" + pretrained: your weight path + ``` + +#### Start training + +- By specifying different configuration files, different data formats/data sets can be used for training. Taking the training configuration of Kinetics-400 data set + 8 cards + frames format as an example, the startup command is as follows (more training commands can be viewed in `PaddleVideo/run.sh`). + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_ucf101_frames.yaml + ``` + +- Training UCF-101 dataset of videos format using scripts. + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_ucf101_videos.yaml + ``` + +- AMP is useful for speeding up training, scripts as follows: + + ```bash + export FLAGS_conv_workspace_size_limit=800 #MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_ucf101_frames.yaml + ``` + +- AMP works better with `NHWC` data format, scripts as follows: + + ```bash + export FLAGS_conv_workspace_size_limit=800 #MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_ucf101_frames_nhwc.yaml + ``` + +## Test + +Put the weight of the model to be tested into the `output/TSM/` directory, the test command is as follows + +```bash +python3 main.py --test -c configs/recognition/tsm/tsm.yaml -w output/TSM/TSM_best.pdparams +``` + +--- + +When the test configuration uses the following parameters, the evaluation accuracy on the validation data set of Kinetics-400 is as follows: + +| backbone | Sampling method | Training Strategy | num_seg | target_size | Top-1 | checkpoints | +| :--------: | :---------------: | :-------: | :-----------: | :-----: | :-----------: | :-----------: | +| ResNet50 | Uniform | NCHW | 8 | 224 | 71.06 | [TSM_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams) | + +When the test configuration uses the following parameters, the evaluation accuracy on the validation data set of UCF-101 is as follows: + +| backbone | Sampling method | Training Strategy | num_seg | target_size | Top-1 | checkpoints | +| :------: | :-------------: | :-----------------: | :-----: | :---------: | :---: | :---------: | +| ResNet50 | Uniform | NCHW | 8 | 224 | 94.42 | [TSM_ucf101_nchw.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_ucf101_nchw.pdparams) | +| ResNet50 | Uniform | NCHW+AMP | 8 | 224 | 94.40 | [TSM_ucf101_amp_nchw.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_ucf101_amp_nchw.pdparams) | +| ResNet50 | Uniform | NHWC+AMP | 8 | 224 | 94.55 | [TSM_ucf101_amp_nhwc.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_ucf101_amp_nhwc.pdparams) | + +## Inference + +### export inference model + +To get model architecture file `TSM.pdmodel` and parameters file `TSM.pdiparams`, use: + +```bash +python3.7 tools/export_model.py -c configs/recognition/tsm/tsm_k400_frames.yaml \ + -p data/TSM_k400.pdparams \ + -o inference/TSM +``` + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/tsm/tsm_k400_frames.yaml \ + --model_file inference/TSM/TSM.pdmodel \ + --params_file inference/TSM/TSM.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +## Implementation details + +### data processing + +- The model reads the `mp4` data in the Kinetics-400 data set, first divides each piece of video data into `num_seg` segments, and then uniformly extracts 1 frame of image from each segment to obtain sparsely sampled `num_seg` video frames. Then do the same random data enhancement to this `num_seg` frame image, including multi-scale random cropping, random left and right flips, data normalization, etc., and finally zoom to `target_size`. + +### Training strategy + +* Use Momentum optimization algorithm training, momentum=0.9 +* Using L2_Decay, the weight attenuation coefficient is 1e-4 +* Using global gradient clipping, the clipping factor is 20.0 +* The total number of epochs is 50, and the learning rate will be attenuated by 0.1 times when the epoch reaches 20 and 40 +* The learning rate of the weight and bias of the FC layer are respectively 5 times and 10 times the overall learning rate, and the bias does not set L2_Decay +* Dropout_ratio=0.5 + +### Parameter initialization + +- Initialize the weight of the FC layer with the normal distribution of Normal(mean=0, std=0.001), and initialize the bias of the FC layer with a constant of 0 + + +## Reference + +- [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf), Ji Lin, Chuang Gan, Song Han diff --git a/docs/en/model_zoo/recognition/tsn.md b/docs/en/model_zoo/recognition/tsn.md new file mode 100644 index 0000000000000000000000000000000000000000..cc157445939b29665e44caf8b64c7cc7da41c968 --- /dev/null +++ b/docs/en/model_zoo/recognition/tsn.md @@ -0,0 +1,123 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/tsn.md) | English + +# TSN + +## Content + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Details](#Details) +- [Reference](#Reference) + +## Introduction + +Temporal Segment Network (TSN) is a classic 2D-CNN-based solution in the field of video classification. This method mainly solves the problem of long-term behavior recognition of video, and replaces dense sampling by sparsely sampling video frames, which can not only capture the global information of the video, but also remove redundancy and reduce the amount of calculation. The core idea is to average the features of each frame as the overall feature of the video, and then enter the classifier for classification. The model implemented by this code is a TSN network based on a single-channel RGB image, and Backbone uses the ResNet-50 structure. + +
+
+
+ + +For details, please refer to the ECCV 2016 paper [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859) + +## Data + +PaddleVide provides training and testing scripts on the Kinetics-400 dataset. Kinetics-400 data download and preparation please refer to [Kinetics-400 data preparation](../../dataset/k400.md) + +## Train + +### Kinetics-400 data set training + +#### Download and add pre-trained models + +1. Load the ResNet50 weights trained on ImageNet1000 as Backbone initialization parameters [ResNet50_pretrain.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams), or download through the command line + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams + ``` + +2. Open `PaddleVideo/configs/recognition/tsn/tsn_k400_frames.yaml`, and fill in the downloaded weight path below `pretrained:` + + ```yaml + MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNet" + pretrained: fill in the path here + ``` + +#### Start training + +- Kinetics-400 data set uses 8 cards for training, the training start command for frames format data is as follows + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsn main.py --validate -c configs/recognition/ tsn/tsn_k400_frames.yaml + ``` + +## Test + +Since the sampling method of the TSN model test mode is **TenCrop** with a slower speed but higher accuracy, which is different from the **CenterCrop** used in the verification mode during the training process, the verification index `topk Acc` recorded in the training log It does not represent the final test score, so after the training is completed, you can use the test mode to test the best model to obtain the final index. The command is as follows: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsn main.py --test -c configs/recognition/ tsn/tsn_k400_frames.yaml -w "output/TSN/TSN_best.pdparams" +``` + +When the test configuration uses the following parameters, the test indicators on the validation data set of Kinetics-400 are as follows: + +| backbone | Sampling method | Training Strategy | num_seg | target_size | Top-1 | checkpoints | +| :------: | :-------------: | :---------------: | :-----: | :---------: | :---: | :----------------------------------------------------------: | +| ResNet50 | TenCrop | NCHW | 3 | 224 | 69.81 | [TSN_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TSN_k400.pdparams) | +| ResNet50 | TenCrop | NCHW | 8 | 224 | 71.70 | [TSN_k400_8.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TSN_k400_8.pdparams) | +## Inference + +### export inference model + +```bash +python3.7 tools/export_model.py -c configs/recognition/tsn/tsn_k400_frames.yaml \ + -p data/TSN_k400.pdparams \ + -o inference/TSN +``` + +The above command will generate the model structure file `TSN.pdmodel` and the model weight file `TSN.pdiparams` required for prediction. + +For the meaning of each parameter, please refer to [Model Reasoning Method](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-Model Reasoning) + +### infer + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/tsn/tsn_k400_frames.yaml \ + --model_file inference/TSN/TSN.pdmodel \ + --params_file inference/TSN/TSN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +## Details + +**data processing:** + +- The model reads the `mp4` data in the Kinetics-400 data set, first divides each piece of video data into `num_seg` segments, and then evenly extracts 1 frame of image from each segment to obtain sparsely sampled `num_seg` video frames , And then do the same random data enhancement to this `num_seg` frame image, including multi-scale random cropping, random left and right flips, data normalization, etc., and finally zoom to `target_size` + +**training strategy:** + +- Use Momentum optimization algorithm for training, momentum=0.9 + +- Using L2_Decay, the weight attenuation coefficient is 1e-4 + +- Use global gradient clipping, with a clipping factor of 40.0 + +- The total number of epochs is 100, and the learning rate will be attenuated by 0.1 times when the epoch reaches 40 and 80 + +- Dropout_ratio=0.4 + +**parameter initialization** + +- The convolutional layer of the TSN model uses Paddle's default [KaimingNormal](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api/paddle/nn/initializer/KaimingNormal_cn.html#kaimingnormal) and [Constant](https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/Constant_cn.html#constant) initialization method, with Normal(mean=0, std= 0.01) normal distribution to initialize the weight of the FC layer, and a constant 0 to initialize the bias of the FC layer + +## Reference + +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859), Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoou Tang, Luc Van Gool diff --git a/docs/en/model_zoo/recognition/tsn_dali.md b/docs/en/model_zoo/recognition/tsn_dali.md new file mode 100644 index 0000000000000000000000000000000000000000..affaf0ad5f7effae429e851f4740b953f09c4996 --- /dev/null +++ b/docs/en/model_zoo/recognition/tsn_dali.md @@ -0,0 +1,98 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/tsn_dali.md) | English + +# TSN DALI + +- [Introduction](#Introduction) +- [Requirement](#Requirement) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + +## Introduction + +We aims to speed up TSN model training using DALI in this code. As [nvidia DALI](https://github.com/NVIDIA/DALI) not support TSN sampling way, we reimplemented segment sampling in VideoReader. + +### Performance + +Test Environment: +``` +Card: Tesla v100 +Memory: 4 * 16G +Cuda: 9.0 +batch_size of single card: 32 +``` + +| Training way | batch cost/s | reader cost/s | ips:instance/sec | Speed up | +| :--------------- | :--------: | :------------: | :------------: | :------------: | +| DALI | 2.083 | 1.804 | 15.36597 | 1.41x | +| Dataloader: num_workers=4 | 2.943 | 2.649 | 10.87460| base | +| pytorch实现 | TODO | TODO | TODO | TODO | + + +## Requirement + +docker image: + +``` + huangjun12/paddlevideo:tsn_dali_cuda9_0 +``` + +To build container, you can use: + +```bash +nvidia-docker run --name tsn-DALI -v /home:/workspace --network=host -it --shm-size 64g -e NVIDIA_DRIVER_CAPABILITIES=compute,utility,video huangjun12/paddlevideo:tsn_dali_cuda9_0 /bin/bash +``` + +## Data + +- Kinetics400 dataset please refer to [K400 data](../../dataset/k400.md) + +- UCF101 dataset please refer to [UCF101 data](../../dataset/ucf101.md) + +## Train + +### download pretrain-model + +- Please download [ResNet50_pretrain.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams) as pretraind model: + +```bash +wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams +``` + +and add path to MODEL.framework.backbone.pretrained in config file as: + +```yaml +MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNet" + pretrained: your weight path +``` + +### Start training + +You can start training by: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsn main.py --train_dali -c configs/recognition/tsn/tsn_dali.yaml -o log_level="INFO" +``` + +- Args -c is used to specify config file,default is ```configs/recognition/tsn/tsn_dali.yaml```。 + +- For finetune please download our trained model [TSN.pdparams]()coming soon,and specify file path with --weights. + +- For the config file usage,please refer to [config](../../tutorials/config.md). + +## Test + +Please refer to [TSN Test](./tsn.md) + +## Inference + +Please refer to [TSN Inference](./tsn.md) + +## Reference + +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859), Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoou Tang, Luc Van Gool diff --git a/docs/en/model_zoo/recognition/videoswin.md b/docs/en/model_zoo/recognition/videoswin.md new file mode 100644 index 0000000000000000000000000000000000000000..5ba3ab645d178f4690953d145b8a44201cb30bba --- /dev/null +++ b/docs/en/model_zoo/recognition/videoswin.md @@ -0,0 +1,130 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/videoswin.md) | English + +# Video-Swin-Transformer Video Classification Model + +## content + +- [Introduction](#Introduction) +- [Data](#DATA) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + + +## Introduction + +Video-Swin-Transformer is a video classification model based on Swin Transformer. It utilizes Swin Transformer's multi-scale modeling and efficient local attention characteristics. It currently achieves SOTA accuracy on the Kinetics-400 data set, surpassing the same transformer structure. The TimeSformer model. + + +![VideoSwin](../../../images/videoswin.jpg) + +## DATA + +K400 data download and preparation please refer to [Kinetics-400 data preparation](../../dataset/k400.md) + + +## Train + +### Kinetics-400 data set training + +#### Download and add pre-trained models + +1. Download the image pre-training model [SwinTransformer_imagenet.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/SwinTransformer_imagenet.pdparams) as the Backbone initialization parameter, or download it through the wget command + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo-release2.2/SwinTransformer_imagenet.pdparams + ``` + +2. Open `configs/recognition/videoswin/videoswin_k400_videos.yaml`, and fill in the downloaded weight storage path below `pretrained:` + + ```yaml + MODEL: + framework: "RecognizerTransformer" + backbone: + name: "SwinTransformer3D" + pretrained: fill in the path here + ``` + +#### Start training + +- The Kinetics400 data set uses 8 cards for training, and the start command of the training method is as follows: + + ```bash + # videos data format + python3.7 -u -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin main.py --validate -c configs/ recognition/video_swin_transformer/videoswin_k400_videos.yaml + ``` + +- Turn on amp mixed-precision training to speed up the training process. The training start command is as follows: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + # videos data format + python3.7 -u -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin main.py --amp --validate- c configs/recognition/videoswin/videoswin_k400_videos.yaml + ``` + +- In addition, you can customize and modify the parameter configuration to achieve the purpose of training/testing on different data sets. It is recommended that the naming method of the configuration file is `model_dataset name_file format_data format_sampling method.yaml` , Please refer to [config](../../tutorials/config.md) for parameter usage. + + +## Test + +- The Video-Swin-Transformer model is verified during training. You can find the keyword `best` in the training log to obtain the model test accuracy. The log example is as follows: + + ``` + Already save the best model (top1 acc)0.7258 + ``` + +- Since the sampling method of the Video-Swin-Transformer model test mode is a bit slower but more accurate **UniformCrop**, which is different from the **CenterCrop** used in the verification mode during the training process, so the verification recorded in the training log The index `topk Acc` does not represent the final test score, so after the training is completed, you can use the test mode to test the best model to obtain the final index. The command is as follows: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin main.py --test -c configs/recognition/ video_swin_transformer/videoswin_k400_videos.yaml -w "output/VideoSwin/VideoSwin_best.pdparams" + ``` + + + When the test configuration uses the following parameters, the test indicators on the validation data set of Kinetics-400 are as follows: + + | backbone | Sampling method | num_seg | target_size | Top-1 | checkpoints | + | :----------------: | :-------------: | :-----: | :---------: | :---- | :----------------------------------------------------------: | + | Swin Transformer | UniformCrop | 32 | 224 | 82.40 | [SwinTransformer_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/VideoSwin_k400.pdparams) | + + +## Inference + +### Export inference model + +```bash +python3.7 tools/export_model.py -c configs/recognition/videoswin/videoswin_k400_videos.yaml \ + -p data/VideoSwin_k400.pdparams \ + -o inference/VideoSwin +``` + +The above command will generate the model structure file `VideoSwin.pdmodel` and the model weight file `VideoSwin.pdiparams` required for prediction. + +- For the meaning of each parameter, please refer to [Model Reasoning Method](../../start.md#2-Model Reasoning) + +### Use predictive engine inference + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/videoswin/videoswin_k400_videos.yaml \ + --model_file inference/VideoSwin/VideoSwin.pdmodel \ + --params_file inference/VideoSwin/VideoSwin.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +The output example is as follows: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9999829530715942 +``` + +It can be seen that using the Video-Swin-Transformer model trained on Kinetics-400 to predict `data/example.avi`, the output top1 category id is `5`, and the confidence is 0.99. By referring to the category id and name correspondence table `data/k400/Kinetics-400_label_list.txt`, it can be known that the predicted category name is `archery`. + +## Reference + +- [Video Swin Transformer](https://arxiv.org/pdf/2106.13230.pdf), Ze Liu, Jia Ning, Yue Cao, Yixuan Wei diff --git a/docs/en/model_zoo/segmentation/asrf.md b/docs/en/model_zoo/segmentation/asrf.md new file mode 100644 index 0000000000000000000000000000000000000000..18f7d016a1dcabc18c3de50a833be5693cbfd2fd --- /dev/null +++ b/docs/en/model_zoo/segmentation/asrf.md @@ -0,0 +1,139 @@ +[简体中文](../../../zh-CN/model_zoo/segmentation/asrf.md) | English + +# ASRF : Video Action Segmentation Model + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + +## Introduction + +ASRF model is an improvement on the video motion segmentation model ms-tcn, which was published on WACV in 2021. We reproduce the officially implemented pytorch code and obtain approximate results in paddlevideo. + +

+
+MS-TCN Overview +

+ +## Data + +ASRF can choose 50salads, breakfast, gtea as trianing set. Please refer to Video Action Segmentation dataset download and preparation doc [Video Action Segmentation dataset](../../dataset/SegmentationDataset.md) + +Unlike MS-TCN, ASRF model requires additional data construction. The script process is as follows +```bash +python data/50salads/prepare_asrf_data.py --dataset_dir data/ +``` + +## Train + +After prepare dataset, we can run sprits. + +```bash +# gtea dataset +export CUDA_VISIBLE_DEVICES=3 +python3.7 main.py --validate -c configs/segmentation/asrf/asrf_gtea.yaml +``` + +- Start the training by using the above command line or script program. There is no need to use the pre training model. The video action segmentation model is usually a full convolution network. Due to the different lengths of videos, the `DATASET.batch_size` of the video action segmentation model is usually set to `1`, that is, batch training is not required. At present, only **single sample** training is supported. + +## Test + +Test MS-TCN on dataset scripts: + +```bash +python main.py --test -c configs/segmentation/asrf/asrf_gtea.yaml --weights=./output/ASRF/ASRF_split_1.pdparams +``` + +- The specific implementation of the index is to calculate ACC, edit and F1 scores by referring to the test script[evel.py](https://github.com/yabufarha/ms-tcn/blob/master/eval.py) provided by the author of ms-tcn. + +The reproduction of pytorch comes from the official [code base](https://github.com/yiskw713/asrf) + +- The evaluation method of data set adopts the folding verification method in ms-tcn paper, and the division method of folding is the same as that in ms-tcn paper. + +Accuracy on Breakfast dataset(4 folding verification): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 67.6% | 72.4% | 74.3% | 68.9% | 56.1% | +| pytorch | 65.8% | 71.0% | 72.3% | 66.5% | 54.9% | +| paddle | 66.1% | 71.9% | 73.3% | 67.9% | 55.7% | + +Accuracy on 50salads dataset(5 folding verification): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 84.5% | 79.3% | 82.9% | 83.5% | 77.3% | +| pytorch | 81.4% | 75.6% | 82.7% | 81.2% | 77.2% | +| paddle | 81.6% | 75.8% | 83.0% | 81.5% | 74.8% | + +Accuracy on gtea dataset(4 folding verification): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 77.3% | 83.7% | 89.4% | 87.8% | 79.8% | +| pytorch | 76.3% | 79.6% | 87.3% | 85.8% | 74.9% | +| paddle | 77.1% | 83.3% | 88.9% | 87.5% | 79.1% | + +Model weight for gtea +Test_Data| F1@0.5 | checkpoints | +| :----: | :----: | :---- | +| gtea_split1 | 72.4409 | [ASRF_gtea_split_1.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ASRF_gtea_split_1.pdparams) | +| gtea_split2 | 76.6666 | [ASRF_gtea_split_2.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ASRF_gtea_split_2.pdparams) | +| gtea_split3 | 84.5528 | [ASRF_gtea_split_3.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ASRF_gtea_split_3.pdparams) | +| gtea_split4 | 82.6771 | [ASRF_gtea_split_4.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ASRF_gtea_split_4.pdparams) | +## Infer + +### export inference model + +```bash +python3.7 tools/export_model.py -c configs/segmentation/asrf/asrf_gtea.yaml \ + -p data/ASRF_gtea_split_1.pdparams \ + -o inference/ASRF +``` + +To get model architecture file `ASRF.pdmodel` and parameters file `ASRF.pdiparams`, use: + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +Input file are the file list for infering, for example: +``` +S1_Cheese_C1.npy +S1_CofHoney_C1.npy +S1_Coffee_C1.npy +S1_Hotdog_C1.npy +... +``` + +```bash +python3.7 tools/predict.py --input_file data/gtea/splits/test.split1.bundle \ + --config configs/segmentation/asrf/asrf_gtea.yaml \ + --model_file inference/ASRF/ASRF.pdmodel \ + --params_file inference/ASRF/ASRF.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +```bash +result write in : ./inference/infer_results/S1_Cheese_C1.txt +result write in : ./inference/infer_results/S1_CofHoney_C1.txt +result write in : ./inference/infer_results/S1_Coffee_C1.txt +result write in : ./inference/infer_results/S1_Hotdog_C1.txt +result write in : ./inference/infer_results/S1_Pealate_C1.txt +result write in : ./inference/infer_results/S1_Peanut_C1.txt +result write in : ./inference/infer_results/S1_Tea_C1.txt +``` + + +## Reference + +- [Alleviating Over-segmentation Errors by Detecting Action Boundaries](https://arxiv.org/pdf/2007.06866v1.pdf), Yuchi Ishikawa, Seito Kasai, Yoshimitsu Aoki, Hirokatsu Kataoka diff --git a/docs/en/model_zoo/segmentation/cfbi.md b/docs/en/model_zoo/segmentation/cfbi.md new file mode 100644 index 0000000000000000000000000000000000000000..1b5eefe229290b6e6718067687f7bdc41ae862d7 --- /dev/null +++ b/docs/en/model_zoo/segmentation/cfbi.md @@ -0,0 +1,46 @@ +[简体中文](../../../zh-CN/model_zoo/recognition/cfbi.md) | English + +# CFBI + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Test](#Test) +- [Reference](#Reference) + +## Introduction + +CFBI is a Video Object Segmentation model proposed by Baidu in ECCV 2020. This method consider background should be equally treated and thus propose Collaborative video object segmentation by Foreground-Background Integration (CFBI) approach. Our CFBI implicitly imposes the feature embedding from the target foreground object and its corresponding background to be contrastive, promoting the segmentation results accordingly. Given the image and target segmentation of the reference frame (the first frame) and the previous frame, the model will predict the segmentation of the current frame. + +
+
+
+ + +## Data + +Please refer to DAVIS data download and preparation doc [DAVIS-data](../../dataset/davis.md) + + +## Test + +- Test scripts: + +```bash +python3.7 main.py --test -c configs/segmentation/cfbip_davis.yaml -w CFBIp_davis.pdparams +``` + +- Predicted results will be saved in `result_root`. To get evaluation metrics, please use [davis2017-evaluation tools](https://github.com/davisvideochallenge/davis2017-evaluation). + +Metrics on DAVIS: + +| J&F-Mean | J-Mean | J-Recall | J-Decay | F-Mean | F-Recall | F-Decay | checkpoints | +| :------: | :-----: | :----: | :----: | :----: | :----: | :----: | :----: | +| 0.823 | 0.793 | 0.885 | 0.083 | 0.852 | 0.932 | 0.100 | [CFBIp_r101_davis.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/CFBIp_r101_davis.pdparams) | + + +## Reference + +- [Collaborative Video Object Segmentation by Foreground-Background Integration](https://arxiv.org/abs/2003.08333), Zongxin Yang, Yunchao Wei, Yi Yang diff --git a/docs/en/model_zoo/segmentation/mstcn.md b/docs/en/model_zoo/segmentation/mstcn.md new file mode 100644 index 0000000000000000000000000000000000000000..7c619f2b79bb0eebac734bd9d8e4a6b5a1cfa02b --- /dev/null +++ b/docs/en/model_zoo/segmentation/mstcn.md @@ -0,0 +1,130 @@ +[简体中文](../../../zh-CN/model_zoo/segmentation/mstcn.md) | English + +# MS-TCN : Video Action Segmentation Model + +--- +## Contents + +- [Introduction](#Introduction) +- [Data](#Data) +- [Train](#Train) +- [Test](#Test) +- [Inference](#Inference) +- [Reference](#Reference) + +## Introduction + +Ms-tcn model is a classic model of video motion segmentation model, which was published on CVPR in 2019. We optimized the officially implemented pytorch code and obtained higher precision results in paddlevideo. + +

+
+MS-TCN Overview +

+ +## Data + +MS-TCN can choose 50salads, breakfast, gtea as trianing set. Please refer to Video Action Segmentation dataset download and preparation doc [Video Action Segmentation dataset](../../dataset/SegmentationDataset.md) + +## Train + +After prepare dataset, we can run sprits. + +```bash +# gtea dataset +export CUDA_VISIBLE_DEVICES=3 +python3.7 main.py --validate -c configs/segmentation/ms_tcn/ms_tcn_gtea.yaml --seed 1538574472 +``` + +- Start the training by using the above command line or script program. There is no need to use the pre training model. The video action segmentation model is usually a full convolution network. Due to the different lengths of videos, the `DATASET.batch_size` of the video action segmentation model is usually set to `1`, that is, batch training is not required. At present, only **single sample** training is supported. + +## Test + +Test MS-TCN on dataset scripts: + +```bash +python main.py --test -c configs/segmentation/ms_tcn/ms_tcn_gtea.yaml --weights=./output/MSTCN/MSTCN_split_1.pdparams +``` + +- The specific implementation of the index is to calculate ACC, edit and F1 scores by referring to the test script[evel.py](https://github.com/yabufarha/ms-tcn/blob/master/eval.py) provided by the author of ms-tcn. + +- The evaluation method of data set adopts the folding verification method in ms-tcn paper, and the division method of folding is the same as that in ms-tcn paper. + +Accuracy on Breakfast dataset(4 folding verification): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 66.3% | 61.7% | 48.1% | 48.1% | 37.9% | +| paddle | 65.2% | 61.5% | 53.7% | 49.2% | 38.8% | + +Accuracy on 50salads dataset(5 folding verification): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 80.7% | 67.9% | 76.3% | 74.0% | 64.5% | +| paddle | 81.1% | 71.5% | 77.9% | 75.5% | 66.5% | + +Accuracy on gtea dataset(4 folding verification): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 79.2% | 81.4% | 87.5% | 85.4% | 74.6% | +| paddle | 76.9% | 81.8% | 86.4% | 84.7% | 74.8% | + +Model weight for gtea + +Test_Data| F1@0.5 | checkpoints | +| :----: | :----: | :---- | +| gtea_split1 | 70.2509 | [MSTCN_gtea_split_1.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MSTCN_gtea_split_1.pdparams) | +| gtea_split2 | 70.7224 | [MSTCN_gtea_split_2.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MSTCN_gtea_split_2.pdparams) | +| gtea_split3 | 80.0 | [MSTCN_gtea_split_3.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MSTCN_gtea_split_3.pdparams) | +| gtea_split4 | 78.1609 | [MSTCN_gtea_split_4.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MSTCN_gtea_split_4.pdparams) | + +## Infer + +### export inference model + +```bash +python3.7 tools/export_model.py -c configs/segmentation/ms_tcn/ms_tcn_gtea.yaml \ + -p data/MSTCN_gtea_split_1.pdparams \ + -o inference/MSTCN +``` + +To get model architecture file `MSTCN.pdmodel` and parameters file `MSTCN.pdiparams`, use: + +- Args usage please refer to [Model Inference](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86). + +### infer + +Input file are the file list for infering, for example: +``` +S1_Cheese_C1.npy +S1_CofHoney_C1.npy +S1_Coffee_C1.npy +S1_Hotdog_C1.npy +... +``` + +```bash +python3.7 tools/predict.py --input_file data/gtea/splits/test.split1.bundle \ + --config configs/segmentation/ms_tcn/ms_tcn_gtea.yaml \ + --model_file inference/MSTCN/MSTCN.pdmodel \ + --params_file inference/MSTCN/MSTCN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +example of logs: + +```bash +result write in : ./inference/infer_results/S1_Cheese_C1.txt +result write in : ./inference/infer_results/S1_CofHoney_C1.txt +result write in : ./inference/infer_results/S1_Coffee_C1.txt +result write in : ./inference/infer_results/S1_Hotdog_C1.txt +result write in : ./inference/infer_results/S1_Pealate_C1.txt +result write in : ./inference/infer_results/S1_Peanut_C1.txt +result write in : ./inference/infer_results/S1_Tea_C1.txt +``` + +## Reference + +- [MS-TCN: Multi-Stage Temporal Convolutional Network for Action Segmentation](https://arxiv.org/pdf/1903.01945.pdf), Y. Abu Farha and J. Gall. diff --git a/docs/en/tools.md b/docs/en/tools.md new file mode 100644 index 0000000000000000000000000000000000000000..56138e5f298100c81b016a27acc3e8b7aadec3db --- /dev/null +++ b/docs/en/tools.md @@ -0,0 +1,22 @@ +[简体中文](../zh-CN/tools.md) | English + +# Tools + +This page includes the usage of some useful tools in PaddleVideo. + +## Params + +To get the params of a model. + +```shell +python3.7 tools/summary.py -c configs/recognization/tsm/tsm.yaml +``` + +## FLOPS +to print FLOPs. + +```shell +python3.7 tools/summary.py -c configs/recognization/tsm/tsm.yaml --FLOPs +``` + +## Test the export model coming soon diff --git a/docs/en/tutorials/Action Recognition Datasets b/docs/en/tutorials/Action Recognition Datasets new file mode 100644 index 0000000000000000000000000000000000000000..9bd259157bb69fb27539744650596f4588ea03b7 --- /dev/null +++ b/docs/en/tutorials/Action Recognition Datasets @@ -0,0 +1,12 @@ +Usefull Action Recognition Datasets. + + AVA, https://arxiv.org/abs/1705.08421 + Kinetics, https://arxiv.org/abs/1705.06950 + YouTube-8M, https://arxiv.org/abs/1609.08675 + ActivityNet, http://www.cv-foundation.org/openaccess/content_cvpr_2015/html/Heilbron_ActivityNet_A_Large-Scale_2015_CVPR_paper.html + Moments in Time, https://arxiv.org/pdf/1801.03150.pdf + Charades, https://arxiv.org/abs/1604.01753 + EPIC-Kitchens, https://arxiv.org/abs/1804.02748 + THUMOS, https://arxiv.org/abs/1604.06182 + UCF-101, http://crcv.ucf.edu/papers/UCF101_CRCV-TR-12-01.pdf + HMDB51, http://serre-lab.clps.brown.edu/wp-content/uploads/2012/08/Kuehne_etal_iccv11.pdf diff --git a/docs/en/tutorials/Action Recognition Papers b/docs/en/tutorials/Action Recognition Papers new file mode 100644 index 0000000000000000000000000000000000000000..7282bef96ecf42e964da4b5e82356a02b4b7992a --- /dev/null +++ b/docs/en/tutorials/Action Recognition Papers @@ -0,0 +1,31 @@ +Useful Papers on Action Recognition and Video Classification. + +TSN: Temporal Segment Networks: Towards Good Practices for Deep Action Recognition, ECCV 2016 +TSM: Temporal Shift Module for Efficient Video Understanding, ICCV 2019 +SlowFast Networks for Video Recognition, ICCV 2019 +Non-local Neural Networks, CVPR 2018 +A Multigrid Method for Efficiently Training Video Models, CVPR2020 +X3D: Progressive Network Expansion for Efficient Video Recognition, CVPR2020 +ECO: Efficient Convolutional Network for Online Video Understanding, ECCV 2018 +3D Resnet: Would Mega-scale Datasets Further Enhance Spatiotemporal 3D CNNs, CVPR 2018 +TPN: Temporal Pyramid Network for Action Recognition, CVPR 2020 +EvaNet: Evolving Space-Time Neural Architectures for Videos, ICCV 2019 +RepFlow: Representation Flow for Action Recognition, CVPR 2019 +MARS: Motion-Augmented RGB Stream for Action Recognition, CVPR 2019 +StNet: Local and Global Spatial-Temporal Modeling for Human Action Recognition, AAAI 2019 +Attention Cluster: Purely Attention Based Local Feature Integration for Video Classification +NeXtVLAD: An Efficient Neural Network to Aggregate Frame-level Features for Large-scale Video Classification +C-TCN: Action localization Model by Baidu, the Champion model of ActivityNet 2018 +Neural Graph Matching Networks for Fewshot 3D Action Recognition - M. Guo et al., ECCV2018. +Temporal 3D ConvNets using Temporal Transition Layer - A. Diba et al., CVPRW2018. +Temporal 3D ConvNets: New Architecture and Transfer Learning for Video Classification - A. Diba et al., arXiv2017. +Attentional Pooling for Action Recognition - R. Girdhar and D. Ramanan, NIPS2017. +Fully Context-Aware Video Prediction - Byeon et al, arXiv2017. +Hidden Two-Stream Convolutional Networks for Action Recognition - Y. Zhu et al, arXiv2017. +Dynamic Image Networks for Action Recognition - H. Bilen et al, CVPR2016. +Long-term Recurrent Convolutional Networks for Visual Recognition and Description - J. Donahue et al, CVPR2015. +Describing Videos by Exploiting Temporal Structure - L. Yao et al, ICCV2015. +Real-time Action Recognition with Enhanced Motion Vector CNNs - B. Zhang et al, CVPR2016. +Action Recognition with Trajectory-Pooled Deep-Convolutional Descriptors - L. Wang et al, CVPR2015. + + diff --git a/docs/en/tutorials/Spatio-Temporal Action Detection Papers b/docs/en/tutorials/Spatio-Temporal Action Detection Papers new file mode 100644 index 0000000000000000000000000000000000000000..f466849f6aa96454b7ad9c7bdf42102c817ecc4a --- /dev/null +++ b/docs/en/tutorials/Spatio-Temporal Action Detection Papers @@ -0,0 +1,30 @@ +Usefull Spatio-Temporal Action Detection Papers. + + + + A Better Baseline for AVA - R. Girdhar et al., ActivityNet Workshop, CVPR2018. + Real-Time End-to-End Action Detection with Two-Stream Networks - A. El-Nouby and G. Taylor, arXiv2018. + Human Action Localization with Sparse Spatial Supervision - P. Weinzaepfel et al., arXiv2017. + Unsupervised Action Discovery and Localization in Videos - K. Soomro and M. Shah, ICCV2017. + Spatial-Aware Object Embeddings for Zero-Shot Localization and Classification of Actions - P. Mettes and C. G. M. Snoek, ICCV2017. + Action Tubelet Detector for Spatio-Temporal Action Localization - V. Kalogeiton et al, ICCV2017. + Tube Convolutional Neural Network (T-CNN) for Action Detection in Videos - R. Hou et al, ICCV2017. + Chained Multi-stream Networks Exploiting Pose, Motion, and Appearance for Action Classification and Detection - M. Zolfaghari et al, ICCV2017. + TORNADO: A Spatio-Temporal Convolutional Regression Network for Video Action Proposal - H. Zhu et al., ICCV2017. + Online Real time Multiple Spatiotemporal Action Localisation and Prediction - G. Singh et al, ICCV2017. + AMTnet: Action-Micro-Tube regression by end-to-end trainable deep architecture - S. Saha et al, ICCV2017. + Am I Done? Predicting Action Progress in Videos - F. Becattini et al, BMVC2017. + Generic Tubelet Proposals for Action Localization - J. He et al, arXiv2017. + Incremental Tube Construction for Human Action Detection - H. S. Behl et al, arXiv2017. + Multi-region two-stream R-CNN for action detection - X. Peng and C. Schmid. ECCV2016. + Spot On: Action Localization from Pointly-Supervised Proposals - P. Mettes et al, ECCV2016. + Deep Learning for Detecting Multiple Space-Time Action Tubes in Videos - S. Saha et al, BMVC2016. + Learning to track for spatio-temporal action localization - P. Weinzaepfel et al. ICCV2015. + Action detection by implicit intentional motion clustering - W. Chen and J. Corso, ICCV2015. + Finding Action Tubes - G. Gkioxari and J. Malik CVPR2015. + APT: Action localization proposals from dense trajectories - J. Gemert et al, BMVC2015. + Spatio-Temporal Object Detection Proposals - D. Oneata et al, ECCV2014. + Action localization with tubelets from motion - M. Jain et al, CVPR2014. + Spatiotemporal deformable part models for action detection - Y. Tian et al, CVPR2013. + Action localization in videos through context walk - K. Soomro et al, ICCV2015. + Fast Action Proposals for Human Action Detection and Search - G. Yu and J. Yuan, CVPR2015. diff --git a/docs/en/tutorials/TSM.md b/docs/en/tutorials/TSM.md new file mode 100644 index 0000000000000000000000000000000000000000..a0db9aa9bcecee669f739776174b9798127994ab --- /dev/null +++ b/docs/en/tutorials/TSM.md @@ -0,0 +1,73 @@ +# 1. Background&Motivation +At present, the video data on the Internet is increasing rapidly, and the time users spend watching short videos and small videos is also increasing rapidly. How to analyze, process and classify the massive video resources quickly and accurately is an urgent problem to be solved. The video understanding technology can analyze the video content in multiple dimensions, understand the video semantics, and automatically classify and label the video, which greatly saves the efficiency of manual audit and costs. At the same time, accurate user recommendation is realized to improve the experience effect. +In this paper, we will introduce the classic model **TSM (Temporal Shift Module)** in the field of video understanding, which is proposed by **MIT** and **IBM Watson AI Lab** `Ji Lin, Chuang Gan and Songhan, etc`, to achieve the balance between effeiciency and performance and improve video understanding ability. + +The most relevant video understanding model to TSM is the **Temporal Segment Network (TSN)** published by Limin Wang +a series of works represented such as I3D, S3D and P3D, which carry out end-to-end joint spatial-temporal modeling through 3D convolution. Although this series of works can capture spatial-temporal features, compared with TSN, the transition from 2D convolution to 3D convolution inevitably introduces extra computation. TSM cleverly uses the idea of temporal dimension feature map shift, theoretically achieving the purpose of feature fusion and joint modeling among different frames with zero extra computing overhead compared with TSN. + +**Paper Address:** [Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383v2.pdf) + +Let's have a look at the following example: if the video is played from left to right and then from right to left respectively, the subjects will give different but correct interpretation of the video, indicating that the understanding of the video is strongly dependent on the temporal information of the video. Yes !, It is the motivation why TSM is proposed. +

+
+

+ +It looks interesting, next,let's dive into the core modules of TSM. + +# 2. Dark technologies used in TSM + +On the basis of traditional image analysis, video analysis needs researchers to supplement the modeling structure of temporal information. At present, 2D CNN and 3D CNN are the two most commonly used methods in video understanding: using 2D CNN model requires less computation but will lose part of the time information; While using 3D CNN has a good effect but a large amount of computation. Faced with such a situation, Ji Lin, Chuang Gan and Song Han et al. from MIT and IBM Watson AI Lab proposed the Temp Shift Module (TSM) Module. By embedding the time displacement module into 2D CNN, they can easily achieve the same video understanding ability as 3D CNN without adding any additional calculation and parameters. +

+
+

+ +The rows and columns of the matrix in the figure above represent the temporal and channel dimensions of the feature graph, respectively. In TSM module, some channels are moved forward one step int the temporal dimension, and some channels are moved backward one step in the temporal dimension, and the gaps after the displacement are zeroed. In this way, context interaction on the temporal dimension is introduced into the feature graph. The channel movement operation can make the current frame contain the channel information of the two adjacent frames. In this way, the 2D convolution operation can directly extract the spatial-temporal information of the video just like the 3D convolution. +It improves the modeling ability of the model in time dimension. based on this basis, the researchers further subdivided the module into TSM module suitable for online video and TSM module suitable for offline video. +

+
+

+ +Bi-Direction TSM module can obtain past and future spatial and temporal information, which is suitable for offline video with high throughput. However, UNI-Direction TSM module is only suitable for low delay online video recognition compared with the present and past spatio-temporal information. +In addition, the author also considered the insertion position of TSM modules and compared two TSM insertion methods: **Residual TSM** and **in-place TSM**. The author found that **Residual TSM** could achieve better performance than **in-place TSM**, At the same time, author explained that **in-place TSM** may affect the extraction of spatial information. +

+
+

+ +TSM module looks **So Easy!!**, the next question is how to implement ? + +# 3. The core codes of TSM + +Now that the principle is clear, let's look at how the code works. First let's have a look the torch version tsm. Unfortunately, the Torch framework does not provide an API for TSM, so we will have to do it by ourselves. The code is shown below: +

+
+

+ +This means that you only need to add four lines of code to TSN's codebase then you can **double the accuracy in Something-Something datasets!!** what a simple and efficient model! + +But..., + +**paddlepaddle** framework take the needs of the majority of users into account and have achieve TSM OP,then users can use it easily. +

+
+

+ +So you no longer have to achieve it by yourself, **it cab be called directly!!! , it can be called directly!!! , it can be called directly!!!** The important thing must say three times. + +Do you think that it is the end of the this topic? **Too young Too simple !!!** + +We have also optimized it to increase speed by 5 times while reducing memory consumption. See the acceleration documentation [accelerate.md](./accelerate.md) for more information. + +Let's have a look at how TSM is implemented using **paddlepaddle**: + +`import paddle.nn.functional as F` + + +`shifts = F.temporal_shift(inputs, self.num_seg, 1.0 / self.num_seg)` + +**Only two lines codes !!!**, isn't it easy ? + +# Reference +[1] [Lin Ji , Gan Chuang , Han Song . TSM: Temporal Shift Module for Efficient Video Understanding. arXiv:1811.08383,2018](https://arxiv.org/pdf/1811.08383v2.pdf). + + +[2] [Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoo Tang,and Luc Van Gool. Temporal segment networks for action recognition in videos? In Proceedings of the European Conference on Computer Vision,pages 20–36. Springer, 2016](https://arxiv.org/abs/1608.00859). diff --git a/docs/en/tutorials/Temporal Action Detection Papers b/docs/en/tutorials/Temporal Action Detection Papers new file mode 100644 index 0000000000000000000000000000000000000000..dc475d3de6a69a614df061e6321061fcf900f506 --- /dev/null +++ b/docs/en/tutorials/Temporal Action Detection Papers @@ -0,0 +1,24 @@ +Usefull Temporal Action Detection Papers. + + Rethinking the Faster R-CNN Architecture for Temporal Action Localization - Yu-Wei Chao et al., CVPR2018 + Weakly Supervised Action Localization by Sparse Temporal Pooling Network - Phuc Nguyen et al., CVPR 2018 + Temporal Deformable Residual Networks for Action Segmentation in Videos - P. Lei and S. Todrovic., CVPR2018. + End-to-End, Single-Stream Temporal Action Detection in Untrimmed Videos - Shayamal Buch et al., BMVC 2017 + Cascaded Boundary Regression for Temporal Action Detection - Jiyang Gao et al., BMVC 2017 + Temporal Tessellation: A Unified Approach for Video Analysis - Kaufman et al., ICCV2017. + Temporal Action Detection with Structured Segment Networks - Y. Zhao et al., ICCV2017. + Temporal Context Network for Activity Localization in Videos - X. Dai et al., ICCV2017. + Detecting the Moment of Completion: Temporal Models for Localising Action Completion - F. Heidarivincheh et al., arXiv2017. + CDC: Convolutional-De-Convolutional Networks for Precise Temporal Action Localization in Untrimmed Videos - Z. Shou et al, CVPR2017. + SST: Single-Stream Temporal Action Proposals - S. Buch et al, CVPR2017. + R-C3D: Region Convolutional 3D Network for Temporal Activity Detection - H. Xu et al, arXiv2017. [code] [project web] [PyTorch] + DAPs: Deep Action Proposals for Action Understanding - V. Escorcia et al, ECCV2016. + Online Action Detection using Joint Classification-Regression Recurrent Neural Networks - Y. Li et al, ECCV2016. + Temporal Action Localization in Untrimmed Videos via Multi-stage CNNs - Z. Shou et al, CVPR2016. + Fast Temporal Activity Proposals for Efficient Detection of Human Actions in Untrimmed Videos - F. Heilbron et al, CVPR2016. + Actionness Estimation Using Hybrid Fully Convolutional Networks - L. Wang et al, CVPR2016. + Learning Activity Progression in LSTMs for Activity Detection and Early Detection - S. Ma et al, CVPR2016. + End-to-end Learning of Action Detection from Frame Glimpses in Videos - S. Yeung et al, CVPR2016. + Fast Action Proposals for Human Action Detection and Search - G. Yu and J. Yuan, CVPR2015. + Bag-of-fragments: Selecting and encoding video fragments for event detection and recounting - P. Mettes et al, ICMR2015. + Action localization in videos through context walk - K. Soomro et al, ICCV2015. diff --git a/docs/en/tutorials/accelerate.md b/docs/en/tutorials/accelerate.md new file mode 100644 index 0000000000000000000000000000000000000000..da2032d192de4770b0064e226e75f79814e489ec --- /dev/null +++ b/docs/en/tutorials/accelerate.md @@ -0,0 +1 @@ +[简体中文](../../zh-CN/tutorials/accelerate.md) | English diff --git a/docs/en/tutorials/config.md b/docs/en/tutorials/config.md new file mode 100644 index 0000000000000000000000000000000000000000..20b3e48803c08e2d1e7c9ace0016a7d7942c5a81 --- /dev/null +++ b/docs/en/tutorials/config.md @@ -0,0 +1,131 @@ +# Configs design + +--- +This page shows how PaddleVideo use the basic IOC/DI technology to decouple and control the whole framework. It is flexible to increase modularity of this system and make it extensible. At last, we will explain the details of config yaml and script args. + + +## Design + +First, when we create a new class, it is common to new a instance like: + +```python +class TSM(): + pass + +model = TSM(init_attributes) +``` + +when more classes are created, the coupling relationship between the calling and called method will increase sharply, obviously, we can create a factory class to solve it, like that: + +```python +if model_name == "TSM": + model = TSM() +elif model_name == "TSN": + model = TSN() +elif ... +``` +and + +```python +optimizer_cfg = dict(name:"MOMENTUM", params: XXX) +if optimizer_cfg.name = "MOMENTUM": + optimizer = MOMENTUM(optimizer_cfg.pop(name)) +elif: + ... +``` + +more and more conditions have to be created though. like widly used in the Java or other platforms, we apply ```inversion of control``` and ```Dependency Inversion``` to decuople. + +Second, to implenment DI, we build two components: + +- Register, to regist a class +- Builder, to new an instance + +1. Register + +We implenment a getter and a setter function to map string to an instance. +[source code](../../paddlevideo/utils/registry.py) + +```python +#excerpt from source code. +class Registry(): + def __init__(self, name): + self._name = name + self._obj_map = {} + + #mapping name -> object + def register(self, obj, name): + self._obj_map[name] = obj + + #get object + def get(self, name): + ret = self._obj_map.get(name) + return ret +``` + +It provides name -> object mapping. For example, To register an object: +```python + + BACKBONES = Registry('backbone') + class ResNet: + pass + BACKBONES.register(ResNet) +``` + +Or, use a decorator +```python + BACKBONES = Registry('backbone') #new a Register + @BACKBONES.register() #regist resnet as a backbone. + class ResNet: + pass +``` + +2. Builder + +To obtain a registed module. +```python + # Usage: To build a module. + + backbone_name = "ResNet" + b = BACKBONES.get(backbone_name)() +``` + +so that we can new(register) an instance in **where it declared**, not **where it called**, a basic DI sub-system has been created now. + +We apply this design on many places, such as: PIPELINE, BACKBONE, HEAD, LOSS, METRIC and so on. + +Finally, We build all of the framework components from config yaml which matches the source code one by one, **It means the attributes in a configuration field is same as the init atrributes of the mathced class**, and to indicate a specified class, we always use ```name``` to mark it. like: + +```yaml +head: + name: "TSMHead" # class name + num_classes: 400 # TSMHead class init attributes + ... +``` + +--- + +## config yaml details + +We separate the config to several parts, in high level: + +- **MODEL:** Architecture configuration, such as HEAD module, BACKBONE module. +- **DATASET:** DATASET and dataloader configuration. +- **PIPELINE:** pipeline of processing configuration. +- **OPTIMIZER:** Optimizer configuration. + +and some unique global configurations, like +- model_name +- log_interval +- epochs +- resume_epoch +- log_level +... + +Training script args + +- **--validate**: switch validate mode on or not +- **--test**: switch test mode on or not +- **--weights**: weights path +- **-c**: config yaml path +- **-o**: override args, one can use it like: -o DATASET.batch_size=16 diff --git a/docs/en/tutorials/customized_usage.md b/docs/en/tutorials/customized_usage.md new file mode 100644 index 0000000000000000000000000000000000000000..ca348179c9a11cd2142ddfb2ab734eda85b237b1 --- /dev/null +++ b/docs/en/tutorials/customized_usage.md @@ -0,0 +1,44 @@ +[简体中文](../../zh-CN/tutorials/customized_usage.md) | English + +# Customized Usage + +## Customized Dataset + +1. finetune + +Please refer to [finetune](../start.md#model_finetune) if only change a "regular" dataset. + +2. customized pipeline + + - add new augments + - add new batch augments + **Note**: Be care of checking the difference of different modes. + +## Customized Network + +1. module function + +Please refer to [modular desigh](modular_design.md) for more information. + +2. customized framework + + - change framework + - change initialized function + - customized loss + +## Customized Solvers + +1. step decay and epoch decay + +2. customized solvers + +## Customized metrics + + - add new data processing + - add new record + - add new metrics + +## Debug tools + +1. Debug level +2. FAQ diff --git a/docs/en/tutorials/demos b/docs/en/tutorials/demos new file mode 100644 index 0000000000000000000000000000000000000000..2228d3048b64378830f8e7a15f72870d1d09dcb2 --- /dev/null +++ b/docs/en/tutorials/demos @@ -0,0 +1,8 @@ +some useful demo todo. + +1、single-class action recognition, tsn/tsm/slowfast +2、multi-class action recognition,lstm +3、action localization,bmn +4、spatio temporal action detection,todo +5、3000-class tagging application(videotag):tsn+lstm +6、Highlights detection application:bmn+tsn+lstm diff --git a/docs/en/tutorials/deployment.md b/docs/en/tutorials/deployment.md new file mode 100644 index 0000000000000000000000000000000000000000..c88329f4572d6d61e10aad3f4741dd9bc3dff90f --- /dev/null +++ b/docs/en/tutorials/deployment.md @@ -0,0 +1,48 @@ +[简体中文](../../zh-CN/tutorials/deployment.md) | English + +# Inference + +## How to convert dygraph model to static model? +To infer and deploy a model, we need export an inference model, or called to_static: `convert dygraph model to static model`, at first. + +```python +python3.7 tools/export_model.py -c config_file -o output_path -p params_file +``` + +Note: In `export_model.py`, It will build a model again, and then loading the prarams. But some init params in the infer phase is different from the train phase. +we add `num_seg` for TSM in advanced, please add more params or modify them if it is necessary. +please refer to [official documents](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/04_dygraph_to_static/index_cn.html) for more information. + +## How to test the export model? + +PaddleVideo supports a test script to test the exported model. + +```python +python3.7 tools/test_export_model.py -p params_file -i inference_folder -c config_file +``` + +We just print the output shape, please feel free to extend it. Avtually, only test a video file by PaddleInference can make sure the exported model is right. + +## How to use PaddleInference? +PaddleVideo supports ```tools/predict.py``` to infer + +```python +python3.7 tools/predict.py -v example.avi --model_file "./inference/example.pdmodel" --params_file "./inference/example.pdiparams" --enable_benchmark=False --model="example" --num_seg=8 + ``` + +## How to test inference speed? +PaddleVideo support a script to test inference speed + +```python +python3.7 tools/predict.py --enable_benchmark=True --model_file=模型文件 --params_file=参数文件 +``` +## How to use C++ infer? + coming soon + +# Deployment + +## How to use PaddleHub Serving deploy? + coming soon + +## How to use PaddleLite deploy? + coming soon diff --git a/docs/en/tutorials/modular_design.md b/docs/en/tutorials/modular_design.md new file mode 100644 index 0000000000000000000000000000000000000000..a426ef5c736645719a3aa6b7bf2ecec4e5d6c046 --- /dev/null +++ b/docs/en/tutorials/modular_design.md @@ -0,0 +1 @@ +[简体中文](../../zh-CN/tutorials/modular_design.md) | English diff --git a/docs/en/tutorials/pp-tsm.md b/docs/en/tutorials/pp-tsm.md new file mode 100644 index 0000000000000000000000000000000000000000..d3ed2dbb7a6eb60f528f31ec68f2e85f02a0c8a9 --- /dev/null +++ b/docs/en/tutorials/pp-tsm.md @@ -0,0 +1,32 @@ +# High performance recognition 2D architecture PP-TSM + +PP-TSM:An Effective and Efficient video-recognition model + +PP-TSM is an optimized model based on TSM in PaddleVideo, +whose performance (top-1 on UCF101 and Kinetics400) and inference spped +are better than TSM paper(https://arxiv.org/abs/1811.08383 ) and +other open source TSM,PaddlePaddle2.0(available on pip now) or +Daily Version( https://www.paddlepaddle.org.cn/documentation/docs/zh/install/Tables.html#whl-dev ) +is required to run PP-TSM. + +When only use ImageNet for pretrain and only use 8X1 sample, +PP-TSM’s top1 reached to 89.5% and 73.5% on UCF101 and Kinetics400, +and inference speed of FP32 on single V100 is 147 VPS on Kinectics400 dataset. +inference speed of FP16 with TensorRT on single V100 isTODO. + +As far as we know, under the same conditions, +top1=73.5% on Kinetics400 is the best performance for 2D video model until now. + + +PP-TSM improved performance and speed of TSM with following methods: +1、Model Tweaks: ResNet50vd ,+2.5% +2、ImageNet pretrain weights based on Knowledge Distillation , +1.3% +3、beter batch size ,+0.2% +4、beter L2 ,+0.3% +5、label_smoothing ,+0.2% +6、beter lr decay ,+0.15% +7、Data augmentation ,+0.3% +8、beter epoch num ,+0.15% +9、bn strategy ,+0.4% +10、integrated PaddleInference +11、more strategies todo: Knowledge Distillation、optimizer and so on. diff --git a/docs/en/tutorials/summarize.md b/docs/en/tutorials/summarize.md new file mode 100644 index 0000000000000000000000000000000000000000..6bd49bf7dcc79adc6135935004f8f922bd1a0a66 --- /dev/null +++ b/docs/en/tutorials/summarize.md @@ -0,0 +1,208 @@ +[简体中文](../../zh-CN/tutorials/summarize.md) | English + +# Introduction for video classification(action recognition) + +## Wide range of application scenarios +Video classification has a wide range of applications in many fields, such as online video platforms such as short videos, offline such as security, transportation, quality inspection and other fields。 + + +## Multiple subtasks +Similar to image tasks, video tasks can also be divided into two categories: **classification (recognition) and detection**, and these two types of tasks can be specifically subdivided by combining different scenes: + ++ Task1:Trimmed Action Recognition. Users input a trimmed video,which contains only single action,then a video tag will be output by model as depicted in fig below: +

+
+ Action Classification +

+ + In terms of the data modality used, classification tasks can be further subdivided into classification based on single modality data, classification based on multi-modality data, classification based on RGB images and classification based on human skeleton, etc, as shown in the figure below: + +

+
+ multi-modality +

+In terms of the perspective of video, it can also be divided into first-person action recognition, +third-person action recognition, single perspective action recognition and multi-perspective fusion action recognition. +Users who are interested in these fields can refer to relevant literatures. + ++ Task2:Untrimmed Video Classification. +Unlike trimmed videos, untrimmed videos often contain multiple actions and have a long time span. +There are a lot of movements that we may need not paying attention to. Through the global analysis of the input long video, and then make a soft classify to mutiple categories. + ++ Task3:Temporal Action Proposal. It is similar to the ROI extraction in the image detection task. +The task is to find the video clips that may contain action in a long video with a lot of actions. + ++ Task4:Temporal Action Localization. Compared with the temporal action proposal task as mentioned above, +temporal action localization task is more consistent with detection task in the field of imgae, +it requires not only to find the video segments with possible actions from the video but also to classify them, +as shown in the figure below +

+
+ Action Detection +

+ ++ Task5:Dense-Captioning Events. The reason why it is called dense captioning events is mainly +because that this task requires video action description on the basis of temporal action localization +(detection). That is to say, the task needs to locate the actions in a **untrimmed** video,in **temporal +dimension** and describe the behavior of the **whole video** after obtaining many video segments which contain actions. + +## Introduction of datasets + +### Classification datasets + +The training and validation of the model cannot be done without comprehensive, +large and well annotated datasets. With the deepening of research on video action recognition, +more and more datasets are applied to the research in this field. +Typical datasets are as follows: + ++ KTH[1](#1) + +KTH dataset is an early small action recognition dataset, +including 599 videos of 6 types of actions (walking, jumping, running, punching, waving and clapping). +The background is relatively still, except for the zoom in and out of the camera, +the camera movement is relatively slight. Since this data set is relatively small, +it is easy to overfit when training heavy 3D networks, +so most current researches are not based on this it. + ++ UCF10[2](#2) + +UCF101 is a medium-size dataset in which most videos are from YouTube. +It contains 13,320 videos with 101 types of actions. +Each type of action is performed by 25 people, each of whom performs 4-7 sets of actions. +The UCF101 and HMDB51 datasets used to be the benchmarks to evaluate the effectiveness of action +recognition model for a long time before the Kinetics dataset was released. + ++ HMDB51[3](#3) + +Brown University's proposed dataset named HMDB51 was released in 2011. +Most of the videos come from movies, +but some come from public databases and online video libraries such as YouTube. +The datasets contains 6849 samples divided into 51 classes, +each of which contains at least 101 samples. + ++ Kinetics[4](#4) + +Kinetics is the most important large-scale action recognition dataset, which was proposed by Google's DeepMind team in 2017. The video data also comes from YouTube, with 400 categories (now expanded to 700 categories) and more than 300,000 videos (now expanded to 600,000 videos), each lasting about 10 seconds. +The action categories are mainly divided into three categories: "human", "human and animal", "human and human interaction". Kinetics can train 3D-RESNET up to 152 layers without over-fitting, +which solves the problem that the previous training dataset is too small to train deep 3D network. +Kinetics has replaced UCF101 and HMDB51 as the benchmark in the field of action recognition. +At present, most studies use this dataset for evaluation and pre-training. + ++ Something-Something[5](#5) + +SomethingV1 contains 108,499 annotated videos (V2 has expanded to 220,847), each of which last two to six seconds. These videos contain 174 kinds of actions. Different from the previous dataset, +the identification of this data set requires stronger time information, +so this dataset has a very important reference value in testing the temporal modeling ability of the model. + +In addition to the above datasets, there are Charades[6](#6) dataset for complex Action recognition, Breakfast Action[7](#7), and Sports 1M[8](#8). + + +### Detection datasets + ++ THUMOS 2014 + +This dataset is from THUMOS Challenge 2014, Its training set is UCF101, validation set and test set include 1010 and 1574 undivided video clips respectively. In the action detection task, only 20 kinds of unsegmented videos of actions were labeled with sequential action fragments, +including 200 validation sets (3007 action fragments) and 213 test sets (3358 action fragments). + ++ MEXaction2 + +The Mexaction2 dataset contains two types of action: horse riding and bullfighting. +The dataset consists of three parts: YouTube videos, horseback riding videos in UCF101, and INA videos. +YouTube clips and horseback riding videos in UCF101 are short segmented video clips that are used as training sets. +The INA video is a long unsegmented video with a total length of 77 hours, +and it is divided into three parts: training, validation and test. +There are 1336 action segments in the training set, 310 in the validation set and 329 in the test set. +Moreover, the Mexaction2 dataset is characterized by very long unsegmented video lengths, +and marked action segments only account for a very low proportion of the total video length. + ++ ActivityNet + +At present the largest database, also contains two tasks of classification and detection. +This dataset only provides a YouTube link to the video, not a direct download of the video, +so you also need to use the YouTube download tool in Python to automatically download the videos. +The dataset contains 200 action categories, 20,000 (training + verification + test set) videos, +and a total of about 700 hours of video. + +## Introduction of classic models +As shown in the figure, +the action recognition framework mainly includes three steps: +feature extraction, motion representation and classification. +How to extract spatiotemporal features of video is the core problem of action recognition and video classification. +

+
+Framework of action recognition +

+According to different methods, action recognition (video classification) methods can be generally summarized into two stages: +manual feature-based method and deep learning-based method. +Typical motion descriptors in the manual feature-based method stage include DTP and IDT, +which are also the most excellent motion descriptors accepted by most researchers before deep-learning is applied in this field. +Interinterested readers may refer to the relevant references at the end of this paper. +Since 2014, deep learning methods have been gradually applied to the field of video classification. +At present, deep learning-based methods have become a hotspot of research in both academic and the practice, and the effect is far beyond the motion features of manual design. +Since 2014, many classic network structures have been put forward by the researchers regarding the problem of how to represent motion characteristics, +as shown in the figure below: +

+
+Classic Models +

+ +At present,Paddlevideo has contained several classic models such as:TSN[9](#9),TSM[10](#10),slowfast[11](#11),et al.In the future, +we will analyze the classic models and papers in these fields. Please look forward to it + + +## Introduction of competetion ++ [ActivityNet](http://activity-net.org/challenges/2020/challenge.html) + +ActivityNet is a large-scale action recognition competition. Since 2016, +it has been held simultaneously with CVPR every year. Up to this year, +it has been held for 4 consecutive sessions. It focuses on identifying everyday, high-level, goal-oriented activities from +user-generated videos taken from the Internet video portal YouTube. +At present, ActivityNet competition has become the most influential competition in the field of action recognition. + +## Reference + +
+[1] Schuldt C, Laptev I, Caputo B.Recognizing Human Actions: A Local SVM Approach Proceedings of International Conference on Pattern Recognition. Piscataway, NJ: IEEE, 2004:23-26 +
+
+
+[2] Soomro K, Zamir A R, Shah M. UCF101: A Dataset of 101 Human Actions Classes From Videos in The Wild. arXiv:1212.0402,2012. +
+
+
+[3] Kuehne H, Jhuang H, Garrote E, et al. HMDB: a large video database for human motion recognition Proceedings of IEEE International Conference on Computer Vision. Piscataway, NJ: IEEE, 2011:2556-2563. +
+
+
+[4] Carreira J , Zisserman A . Quo Vadis, Action Recognition? A New Model and the Kinetics Dataset Proceedings of IEEE Conference on Computer Vision and Pattern Recognition. Piscataway, NJ: IEEE, 2017:6299-6308. +
+
+
+[5] Goyal R, Kahou S E, Michalski V. The “something something” video database for learning and evaluating visual common sense. arXiv:1706.04261,2017. +
+
+
+[6] Sigurdsson G A , Varol Gül, Wang Xiaolong, et al. Hollywood in Homes: Crowdsourcing Data Collection for Activity Understanding. arXiv: 604.01753,2016 +
+
+
+[7] Kuehne H, Arslan A, Serre T. The Language of Actions Recovering the Syntax and Semantics of Goal-Directed Human Activities Proceedings of IEEE Conference on Computer Vision and Pattern Recognition. Piscataway, NJ: IEEE, 2014. +
+
+
+[8] Karpathy A , Toderici G , Shetty S , et al. Large-Scale Video Classification with Convolutional Neural Networks Proceedings of IEEE Conference on Computer Vision and Pattern Recognition. Piscataway, NJ: IEEE, 2014:1725-1732. +
+
+
+[9] Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoo Tang,and Luc Van Gool. Temporal segment networks for action recognition in videos? In Proceedings of the European Conference on Computer Vision,pages 20–36. Springer, 2016. +
+
+
+[10] Lin Ji , Gan Chuang , Han Song . TSM: Temporal Shift Module for Efficient Video Understanding. arXiv:1811.08383,2018. +
+
+
+[11] Feichtenhofer C , Fan Haoqi , Malik J , et al. SlowFast Networks for Video Recognition. arXiv:1812.03982,2018. +
+ + diff --git a/docs/en/usage.md b/docs/en/usage.md new file mode 100644 index 0000000000000000000000000000000000000000..612fd576799cc33f5ca2c73912965f03d423b16e --- /dev/null +++ b/docs/en/usage.md @@ -0,0 +1,177 @@ +[简体中文](../zh-CN/usage.md) | English + +# Usage +--- + +Please refer to [installation documents](./install.md) to prepare the enviroment, and follow the steps mentioned in the [data preparation documents](./dataset/) to construct dataset, we will take you through the basic functions supported by PaddleVideo, all of it takes the ucf101 dataset with frame format as example. + +PaddleVideo only support linux operation system and GPU running time environment now. + +Default detination folder of PaddleVideo files. running the [example config](../../configs/example.yaml) as example. + +``` +PaddleVideo + ├── paddlevideo + ├── ... #other source codes + ├── output #ouput destination + | ├── example + | | ├── example_best.pdparams #path_to_weights + | | └── ... + | └── ... + ├── log #log file destination. + | ├── worker.0 + | ├── worker.1 + | └── ... + └── inference #inference files destination. + ├── .pdiparams file + ├── .pdimodel file + └── .pdiparmas.info file +``` + + +## 1. Train and Test + +Start running multi-cards training scripts or test scripts by `paddle.distributed.launch`, or run the `run.sh` directly. + +```bash +sh run.sh +``` + +We put all the start commands in advanced in the ```run.sh```, please uncomment the selected one to run. + + + +### 1.1 Train + +Switch `--validate` on to validating while training. + +```bash + +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + --validate \ + -c ./configs/example.yaml +``` + +Indicating `-c` to set configuration, and one can flexible add `-o` in the script to update it. + +```bash +python -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/example.yaml \ + --validate \ + -o DATASET.batch_size=16 +``` +Indicating `-o DATASET.batch_size=16` can update batch size to 16, please refer to [configuration](tutorials/config.md#config-yaml-details) for more information. + +After starting training, log files will generated, and its format is shown as below, it will output to both the screen and files. Default destination of log is under the `.log/` folder, and stored in the files named like `worker.0`, `worker.1` ... + +[train phase] current time, current epoch/ total epoch, batch id, metrics, elapse time, ips, etc.: + + [12/28 17:31:26] epoch:[ 1/80 ] train step:0 loss: 0.04656 lr: 0.000100 top1: 1.00000 top5: 1.00000 elapse: 0.326 reader: 0.001s ips: 98.22489 instance/sec. + +[eval phase] current time, current epoch/ total epoch, batch id, metrics, elapse time, ips, etc.: + + + [12/28 17:31:32] epoch:[ 80/80 ] val step:0 loss: 0.20538 top1: 0.88281 top5: 0.99219 elapse: 1.589 reader: 0.000s ips: 20.14003 instance/sec. + + +[epoch end] current time, metrics, elapse time, ips, etc. + + [12/28 17:31:38] END epoch:80 val loss_avg: 0.52208 top1_avg: 0.84398 top5_avg: 0.97393 elapse_avg: 0.234 reader_avg: 0.000 elapse_sum: 7.021s ips: 136.73686 instance/sec. + +[the best Acc] + + [12/28 17:28:42] Already save the best model (top1 acc)0.8494 + + +### 1.2 Resume + +Indicate `-o resume_epoch` to resume, It will training from ```resume_epoch``` epoch, PaddleVideo will auto load optimizers parameters and checkpoints from `./output` folder, as it is the default output destination. + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/example.yaml \ + --validate \ + -o resume_epoch=5 + +``` + + +### 1.3 Finetune + +Indicate `--weights` to load pretrained parameters, PaddleVideo will auto treat it as a finetune mission. + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/example.yaml \ + --validate \ + --weights=./outputs/example/path_to_weights +``` + +Note: PaddleVideo will NOT load shape unmatched parameters. + + +### 1.4 Test + +Switch `--test` on to start test mode, and indicate `--weights` to load pretrained model. + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/example.yaml \ + --test \ + --weights=./output/example/path_to_weights +``` + + + + +## 2. Infer + +First, export model. +Indicate `-c` to set configuration, `-p` to load pretrained model, `-o` to set inference files destination. + +```bash +python tools/export_model.py \ + -c ./configs/example.yaml \ + -p ./output/example/path_to_weights \ + -o ./inference +``` + + +It will generate `model_name.pdmodel` , `model_name.pdiparams` and `model_name.pdiparames.info`. +Second, start PaddleInference engine to infer a video. + +```bash +python tools/predict.py \ + --input_file "data/example.avi" \ + --model_file "./inference/example.pdmodel" \ + --params_file "./inference/example.pdiparams" \ + --use_gpu=True \ + --use_tensorrt=False +``` + +Attributes: ++ `input_file`: input file path or input directory, which contains input files(s). ++ `model_file`: pdmodel file path. ++ `params_file`: pdiparams file path. ++ `use_tensorrt`: use tensorrt to acclerate or not, default: False. ++ `use_gpu`: use gpu to infer or not, default: True. + +benchmark results are shown in th [benchmark](./benchmark.md). diff --git a/docs/en/whl_en.md b/docs/en/whl_en.md new file mode 100644 index 0000000000000000000000000000000000000000..dd989cc79dfc7de597b2966d579213290e43edee --- /dev/null +++ b/docs/en/whl_en.md @@ -0,0 +1,185 @@ +[简体中文](../zh-CN/whl_zh.md) | English +# paddlevideo package + +## Get started quickly + +### install package + +install by pypi +```bash +python3.7 -m pip install paddlevideo==0.0.1 +``` +**note:** you may have difficulty in installing opencv-python,you can try: + +``` +python3.7 -m pip install opencv-python==4.2.0.32 -i https://pypi.doubanio.com/simple +``` + +build own whl package and install +```bash +python3.7 setup.py bdist_wheel +python3.7 -m pip install dist/paddlevideo-0.0.1-py3-none-any.whl +``` + +### 1. Quick Start + +* Assign `video_file='data/example.avi'`, Use inference model that Paddle provides `model_name='ppTSM'` + + +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False,use_tensorrt=False) +video_file='data/example.avi.' +result = clas.predict(video_file) +print(result) +``` + +``` + >>> result + [{'videoname': 'data/example.avi', 'class_ids': [5], 'scores': [0.9621570706367493], 'label_names': ['archery']}] +``` + +* Using command line interactive programming +```bash +ppvideo --model_name='ppTSM' --video_file='data/example.avi' +``` + +``` + >>> result + **********data/example.avi********** + [{'videoname': 'data/example.avi', 'class_ids': [5], 'scores': [0.9621570706367493], 'label_names': ['archery']}] +``` + + +### 2. Definition of Parameters +* model_name(str): model's name. If not assigning `model_file`and`params_file`, you can assign this param. If using inference model based on Kinectics-400 provided by Paddle, set as default='ppTSM'. +* video_file(str): video's path. Support assigning single local video, internet video and folder containing series of videos. Also Support numpy.ndarray. +* use_gpu(bool): Whether to use GPU or not, defalut=False. +* num_seg(int): Number of segments while using the sample strategies proposed in TSN. +* seg_len(int): Number of frames for each segment. +* short_size(int): resize the minima between height and width into resize_short(int), default=256. +* target_size(int): resize image into resize(int), default=224. +* normalize(bool): whether normalize image or not, default=True. +* model_file(str): path of inference.pdmodel. If not assign this param,you need assign `model_name` for downloading. +* params_file(str): path of inference.pdiparams. If not assign this param,you need assign `model_name` for downloading. +* batch_size(int): batch number, default=1. +* use_fp16(bool): Whether to use float16 in memory or not, default=False. +* use_tensorrt(bool): whether to open tensorrt or not. Using it can greatly promote predict preformance, default=False. +* gpu_mem(int): GPU memory usages,default=8000. +* top_k(int): Assign top_k, default=1. +* enable_mkldnn(bool): whether enable MKLDNN or not, default=False. + + +### 3. Different Usages of Codes + +**We provide two ways to use: 1. Python interative programming 2. Bash command line programming** + +* check `help` information +```bash +ppvideo -h +``` + +* Use user-specified model, you need to assign model's path `model_file` and parameters's path`params_file` + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_file='user-specified model path', + params_file='parmas path', use_gpu=False, use_tensorrt=False) +video_file = '' +result = clas.predict(video_file) +print(result) +``` + +###### bash +```bash +ppvideo --model_file='user-specified model path' --params_file='parmas path' --video_file='video path' +``` + + +* Use inference model which PaddlePaddle provides to predict, you need to choose one of model when initializing ppvideo to assign `model_name`. You may not assign `model_file` , and the model you chosen will be download in `BASE_INFERENCE_MODEL_DIR` ,which will be saved in folder named by `model_name`,avoiding overlay different inference model. + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False, use_tensorrt=False) +video_file = '' +result = clas.predict(video_file) +print(result) +``` + +###### bash +```bash +ppvideo --model_name='ppTSM' --video_file='video path' +``` + +* You can assign input as format`np.ndarray` which has been preprocessed `--video_file=np.ndarray`. + + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False, use_tensorrt=False) +video_file = np.ndarray +result = clas.predict(video_file) +``` + +###### bash +```bash +ppvideo --model_name='ppTSM' --video_file=np.ndarray +``` + +* You can assign `video_file` as a folder path containing series of videos, also can assign `top_k`. + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False, use_tensorrt=False,top_k=5) +video_file = '' # it can be video_file folder path which contains all of videos you want to predict. +result = clas.predict(video_file) +print(result) +``` + +###### bash +```bash +paddleclas --model_name='ResNet50' --video_file='video path' --top_k=5 +``` + +* You can assign `--label_name_path` as your own label_dict_file, format should be as(class_idclass_name<\n>). + +``` +0 abseiling +1 air_drumming +2 answering_questions +3 applauding +4 applying_cream +5 archery +...... +``` + +* If you use inference model that Paddle provides, you do not need assign `label_name_path`. Program will take `data/k400/Kinetics-400_label_list.txt` as defaults. If you hope using your own training model, you can provide `label_name_path` outputing 'label_name' and scores, otherwise no 'label_name' in output information. + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_file= './inference.pdmodel',params_file = './inference.pdiparams',label_name_path='./data/k400/Kinetics-400_label_list.txt',use_gpu=False) +video_file = '' # it can be video_file folder path which contains all of videos you want to predict. +result = clas.predict(video_file) +print(result) +``` +###### bash +```bash +ppvideo --model_file= './inference.pdmodel' --params_file = './inference.pdiparams' --video_file='video path' --label_name_path='./data/k400/Kinetics-400_label_list.txt' +``` +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False) +video_file = '' # it can be video_file folder path which contains all of videos you want to predict. +result = clas.predict(video_file) +print(result) +``` +###### bash +```bash +ppvideo --model_name='ppTSM' --video_file='video path' +``` diff --git a/docs/images/BMN.png b/docs/images/BMN.png new file mode 100644 index 0000000000000000000000000000000000000000..ea0519812260ac1ba180d8e5de5b78f8f63c3edd Binary files /dev/null and b/docs/images/BMN.png differ diff --git a/docs/images/FootballAction.gif b/docs/images/FootballAction.gif new file mode 100644 index 0000000000000000000000000000000000000000..2447251768429225baf8ab0b377dcedbb1d10bf2 Binary files /dev/null and b/docs/images/FootballAction.gif differ diff --git a/docs/images/SlowFast.png b/docs/images/SlowFast.png new file mode 100644 index 0000000000000000000000000000000000000000..9b7db836fe38a94f55c92ba93601e0f07dc9b2ac Binary files /dev/null and b/docs/images/SlowFast.png differ diff --git a/docs/images/VideoTag.gif b/docs/images/VideoTag.gif new file mode 100644 index 0000000000000000000000000000000000000000..e60ddfbe4150db86afedf4e6bbd4170e6d8effb2 Binary files /dev/null and b/docs/images/VideoTag.gif differ diff --git a/docs/images/acc_vps.jpeg b/docs/images/acc_vps.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3b42cdd2dca6b3d9f7e9ee1ae5fd1d4378c4c194 Binary files /dev/null and b/docs/images/acc_vps.jpeg differ diff --git a/docs/images/actbert.png b/docs/images/actbert.png new file mode 100644 index 0000000000000000000000000000000000000000..40b21e2c57d6b4a11757108c3bee3259a652c3da Binary files /dev/null and b/docs/images/actbert.png differ diff --git a/docs/images/action_classification.png b/docs/images/action_classification.png new file mode 100644 index 0000000000000000000000000000000000000000..13e7f698c1c0cdfdf1776f49487f8458fd179aac Binary files /dev/null and b/docs/images/action_classification.png differ diff --git a/docs/images/action_detection.png b/docs/images/action_detection.png new file mode 100644 index 0000000000000000000000000000000000000000..9ddbd6234efaa06462a981c1923219a002b937e4 Binary files /dev/null and b/docs/images/action_detection.png differ diff --git a/docs/images/action_framework.png b/docs/images/action_framework.png new file mode 100644 index 0000000000000000000000000000000000000000..7cc33271ae010d808a94a1eb220d851d48d74dc1 Binary files /dev/null and b/docs/images/action_framework.png differ diff --git a/docs/images/application.png b/docs/images/application.png new file mode 100644 index 0000000000000000000000000000000000000000..7772408987d79573768b1ab66538fa8f2f5c586b Binary files /dev/null and b/docs/images/application.png differ diff --git a/docs/images/asrf.png b/docs/images/asrf.png new file mode 100644 index 0000000000000000000000000000000000000000..3f49edde2b53af9b8010f4e8ccac2ec6a18724d5 Binary files /dev/null and b/docs/images/asrf.png differ diff --git a/docs/images/cfbi.png b/docs/images/cfbi.png new file mode 100644 index 0000000000000000000000000000000000000000..cc34629ffe75e089eebb5e7aaf48ba3fa2045446 Binary files /dev/null and b/docs/images/cfbi.png differ diff --git a/docs/images/classic_model.png b/docs/images/classic_model.png new file mode 100644 index 0000000000000000000000000000000000000000..21e849e1215037e92459a6aa6e678c3195b5fdc0 Binary files /dev/null and b/docs/images/classic_model.png differ diff --git a/docs/images/contribute/001_fork.png b/docs/images/contribute/001_fork.png new file mode 100644 index 0000000000000000000000000000000000000000..50a920dc0e5b4f8b5eb93219471bde699105d2ff Binary files /dev/null and b/docs/images/contribute/001_fork.png differ diff --git a/docs/images/contribute/002_clone.png b/docs/images/contribute/002_clone.png new file mode 100644 index 0000000000000000000000000000000000000000..484e24f4319601ca0a15d2f8ca8e238c44d78e38 Binary files /dev/null and b/docs/images/contribute/002_clone.png differ diff --git a/docs/images/contribute/003_precommit.png b/docs/images/contribute/003_precommit.png new file mode 100644 index 0000000000000000000000000000000000000000..067fb75ddb222ab0b9c71a46619c3fe7b239bc26 Binary files /dev/null and b/docs/images/contribute/003_precommit.png differ diff --git a/docs/images/contribute/004_pr.png b/docs/images/contribute/004_pr.png new file mode 100644 index 0000000000000000000000000000000000000000..489141610c32f45066deb0dd2f8364ee578d68c5 Binary files /dev/null and b/docs/images/contribute/004_pr.png differ diff --git a/docs/images/ctrgcn.jpg b/docs/images/ctrgcn.jpg new file mode 100644 index 0000000000000000000000000000000000000000..899da363939508c36ed8f6b7f28e4a4692935776 Binary files /dev/null and b/docs/images/ctrgcn.jpg differ diff --git a/docs/images/home.gif b/docs/images/home.gif new file mode 100644 index 0000000000000000000000000000000000000000..1335bb00d7fe19f6ad3df0923210867c8935107c Binary files /dev/null and b/docs/images/home.gif differ diff --git a/docs/images/i3d_compare.jpg b/docs/images/i3d_compare.jpg new file mode 100644 index 0000000000000000000000000000000000000000..548e9daabf561c4290d671f495590de8b5482038 Binary files /dev/null and b/docs/images/i3d_compare.jpg differ diff --git a/docs/images/i3d_expand.jpg b/docs/images/i3d_expand.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c183e1cb256f9f57d69b7fe69fc61d9fc72e552a Binary files /dev/null and b/docs/images/i3d_expand.jpg differ diff --git a/docs/images/i3d_expriment1.jpg b/docs/images/i3d_expriment1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..daee822d8a21b76aa08b5b06874d7d295bc3c87c Binary files /dev/null and b/docs/images/i3d_expriment1.jpg differ diff --git a/docs/images/i3d_expriment2.jpg b/docs/images/i3d_expriment2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..499188e000463d33385c0d6254fb50f1f2376aaa Binary files /dev/null and b/docs/images/i3d_expriment2.jpg differ diff --git a/docs/images/joinus.PNG b/docs/images/joinus.PNG new file mode 100644 index 0000000000000000000000000000000000000000..00da92edadca77f4f256f299dc2d0ceb17bcbe7d Binary files /dev/null and b/docs/images/joinus.PNG differ diff --git a/docs/images/mstcn.PNG b/docs/images/mstcn.PNG new file mode 100644 index 0000000000000000000000000000000000000000..354b5308817dcaf29e1a04972d42c7577eabc54b Binary files /dev/null and b/docs/images/mstcn.PNG differ diff --git a/docs/images/multimodality.png b/docs/images/multimodality.png new file mode 100644 index 0000000000000000000000000000000000000000..22c4f3b2e94fb0a1290bc0c9e89ece051e0aed6f Binary files /dev/null and b/docs/images/multimodality.png differ diff --git a/docs/images/oxford_image.png b/docs/images/oxford_image.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1f090947e9784c0e7685cfeb9a54ea66c38fb9 Binary files /dev/null and b/docs/images/oxford_image.png differ diff --git a/docs/images/oxford_image_depth.png b/docs/images/oxford_image_depth.png new file mode 100644 index 0000000000000000000000000000000000000000..ad74126b50120cb47a2b13f905d400f793b948c1 Binary files /dev/null and b/docs/images/oxford_image_depth.png differ diff --git a/docs/images/residual_tsm.png b/docs/images/residual_tsm.png new file mode 100644 index 0000000000000000000000000000000000000000..c7e1dcb458b0dcca943d9012dc1f701e077fe064 Binary files /dev/null and b/docs/images/residual_tsm.png differ diff --git a/docs/images/skeleton_example.png b/docs/images/skeleton_example.png new file mode 100644 index 0000000000000000000000000000000000000000..701603b25e2cb60128f1bef2ea92e0bf13b9216c Binary files /dev/null and b/docs/images/skeleton_example.png differ diff --git a/docs/images/slowfast_network.jpg b/docs/images/slowfast_network.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ce3b9e9579cc047d252935a12775c30f4a24523 Binary files /dev/null and b/docs/images/slowfast_network.jpg differ diff --git a/docs/images/slowfast_structure.jpg b/docs/images/slowfast_structure.jpg new file mode 100644 index 0000000000000000000000000000000000000000..17b955efeb96f3ba84d84c11ffebbde86d0a1731 Binary files /dev/null and b/docs/images/slowfast_structure.jpg differ diff --git a/docs/images/st-gcn.png b/docs/images/st-gcn.png new file mode 100644 index 0000000000000000000000000000000000000000..a52c4277d74fb9dbf270b19824f641a31fe41e47 Binary files /dev/null and b/docs/images/st-gcn.png differ diff --git a/docs/images/temporal.png b/docs/images/temporal.png new file mode 100644 index 0000000000000000000000000000000000000000..20cde2e9598590ee49d5fd5aef5c4a7d96bbff13 Binary files /dev/null and b/docs/images/temporal.png differ diff --git a/docs/images/timesformer_attention_arch.png b/docs/images/timesformer_attention_arch.png new file mode 100644 index 0000000000000000000000000000000000000000..4d331f12cb343a37cbcefd11ac1e9a7220660791 Binary files /dev/null and b/docs/images/timesformer_attention_arch.png differ diff --git a/docs/images/timesformer_attention_visualize.png b/docs/images/timesformer_attention_visualize.png new file mode 100644 index 0000000000000000000000000000000000000000..d7546ede2ddf98d6ba1ae5293e960f6b06abbfe1 Binary files /dev/null and b/docs/images/timesformer_attention_visualize.png differ diff --git a/docs/images/torch_tsm.png b/docs/images/torch_tsm.png new file mode 100644 index 0000000000000000000000000000000000000000..d4fde0cca78f6a708e071e2aa5cc11cdc861ccd9 Binary files /dev/null and b/docs/images/torch_tsm.png differ diff --git a/docs/images/transnetv2.png b/docs/images/transnetv2.png new file mode 100644 index 0000000000000000000000000000000000000000..8b48e8c6bfa9112c4c17121d88d6a7158e5aefab Binary files /dev/null and b/docs/images/transnetv2.png differ diff --git a/docs/images/tsm_architecture.png b/docs/images/tsm_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..286792350b8d1498780f6c3dcd987031ec87321a Binary files /dev/null and b/docs/images/tsm_architecture.png differ diff --git a/docs/images/tsm_intr.png b/docs/images/tsm_intr.png new file mode 100644 index 0000000000000000000000000000000000000000..c8e32e7328b0970d28fe209a81029a4cf2b35e11 Binary files /dev/null and b/docs/images/tsm_intr.png differ diff --git a/docs/images/tsm_op.png b/docs/images/tsm_op.png new file mode 100644 index 0000000000000000000000000000000000000000..dc853257558b7ed5c80b24c6d56a39c3e12e81ff Binary files /dev/null and b/docs/images/tsm_op.png differ diff --git a/docs/images/tsn_architecture.png b/docs/images/tsn_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..d605f089d2a5d4b55193c283ed566c712bf12180 Binary files /dev/null and b/docs/images/tsn_architecture.png differ diff --git a/docs/images/tsn_input.jpg b/docs/images/tsn_input.jpg new file mode 100644 index 0000000000000000000000000000000000000000..391179c55ba8c457cee7de7b38a881e8095dcadc Binary files /dev/null and b/docs/images/tsn_input.jpg differ diff --git a/docs/images/tsn_structure.jpg b/docs/images/tsn_structure.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7d1ddb9e03b00eb137c3e479a7bbd43a7ecff27 Binary files /dev/null and b/docs/images/tsn_structure.jpg differ diff --git a/docs/images/user_group.png b/docs/images/user_group.png new file mode 100644 index 0000000000000000000000000000000000000000..e3dbfb33b31b8dd3e9fb2bbad211911621f9b20d Binary files /dev/null and b/docs/images/user_group.png differ diff --git a/docs/images/videodata.png b/docs/images/videodata.png new file mode 100644 index 0000000000000000000000000000000000000000..f1400fec4f46a1041e1d8b7380cd7d42ce2cec7e Binary files /dev/null and b/docs/images/videodata.png differ diff --git a/docs/images/videoswin.jpg b/docs/images/videoswin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1d16dbb054bc9055ff8d30ddb55078eac83cfd9c Binary files /dev/null and b/docs/images/videoswin.jpg differ diff --git a/docs/zh-CN/benchmark.md b/docs/zh-CN/benchmark.md new file mode 100644 index 0000000000000000000000000000000000000000..6624206a92a885c0f9888f8454c914718c63d0b6 --- /dev/null +++ b/docs/zh-CN/benchmark.md @@ -0,0 +1,68 @@ +简体中文 | [English](../en/benchmark.md) + +# Benchmark + +此文档主要对比PaddleVideo模型库与主流模型库的训练速度。 + + +## 环境配置 +### 硬件环境 + +- 8 NVIDIA Tesla V100 (16G) GPUs +- Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz + +### 软件环境 + +- Python 3.7 +- PaddlePaddle2.0 +- CUDA 10.1 +- CUDNN 7.6.3 +- NCCL 2.1.15 +- GCC 8.2.0 + + +### 实验与评价指标 + +实验中我们测量了平均训练时间,包括数据处理和模型训练两个部分,训练速度均采用每秒钟训练的样本数量(ips)来计量, +数值越大说明训练速度越快,并且考虑到机器预热的问题,前50次迭代的时间没有被计算在内。 + +在相同的数据和模型配置下对比了PaddleVideo和其他的视频理解工具箱,为了保证比较的公平性,对比实验都是在相同的硬件条件下进行,实验所用数据请参考[数据准备](./dataset/k400.md) +观察下表可以发现PaddleVideo相比其他的视频理解框架在训练速度方面取得了巨大的提升,尤其是[Slowfast](../../configs/recognition/slowfast/slowfast.yaml)模型取得了将近一倍的训练速度的提升。 +对于每一种模型配置,我们采用了相同的数据预处理方法并且保证输入是相同的。 + +## 实验结果 +### 分类模型 + +| Model | batch size x gpus | PaddleVideo(ips) | Reference(ips) | MMAction2 (ips) | PySlowFast (ips)| +| :------: | :-------------------:|:---------------:|:---------------: | :---------------: |:---------------: | +| [TSM](../../configs/recognition/tsm/tsm.yaml) | 16x8 | 58.1 | 46.04(temporal-shift-module) | To do | X | +| [PPTSM](../../configs/recognition/tsm/pptsm.yaml) | 16x8 | 57.6 | X | X | X | +| [TSN](../../configs/recognition/tsn/tsn.yaml) | 16x8 | To do | To do (tsn-pytorch) | To do | X | +| [Slowfast](../../configs/recognition/slowfast/slowfast.yaml)| 16x8 | 99.5 | X | To do | 43.2 | +| [Attention_LSTM](../../configs/recognition/attention_lstm/attention_lstm.yaml) | 128x8 | 112.6 | X | X | X | + + +### 定位模型 + +| Model | PaddleVideo(ips) |MMAction2 (ips) |BMN(boundary matching network) (ips)| +| :--- | :---------------: | :-------------------------------------: | :-------------------------------------: | +| [BMN](../../configs/localization/bmn.yaml) | 43.84 | x | x | + +### 分割模型 + +本仓库提供经典和热门时序动作分割模型的性能和精度对比 + +| Model | Metrics | Value | Flops(M) |Params(M) | test time(ms) bs=1 | test time(ms) bs=2 | inference time(ms) bs=1 | inference time(ms) bs=2 | +| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| MS-TCN | F1@0.5 | 38.8% | 791.360 | 0.8 | 170 | - | 10.68 | - | +| ASRF | F1@0.5 | 55.7% | 1,283.328 | 1.3 | 190 | - | 16.34 | - | + +* 模型名称:填写模型的具体名字,比如PP-TSM +* Metrics:填写模型测试时所用的指标,使用的数据集为**breakfast** +* Value:填写Metrics指标对应的数值,一般保留小数点后两位 +* Flops(M):模型一次前向运算所需的浮点运算量,可以调用PaddleVideo/tools/summary.py脚本计算(不同模型可能需要稍作修改),保留小数点后一位,使用数据**输入形状为(1, 2048, 1000)的张量**测得 +* Params(M):模型参数量,和Flops一起会被脚本计算出来,保留小数点后一位 +* test time(ms) bs=1:python脚本开batchsize=1测试时,一个样本所需的耗时,保留小数点后两位。测试使用的数据集为**breakfast**。 +* test time(ms) bs=2:python脚本开batchsize=2测试时,一个样本所需的耗时,保留小数点后两位。时序动作分割模型一般是全卷积网络,所以训练、测试和推理的batch_size都是1。测试使用的数据集为**breakfast**。 +* inference time(ms) bs=1:推理模型用GPU(默认V100)开batchsize=1测试时,一个样本所需的耗时,保留小数点后两位。推理使用的数据集为**breakfast**。 +* inference time(ms) bs=2:推理模型用GPU(默认V100)开batchsize=1测试时,一个样本所需的耗时,保留小数点后两位。时序动作分割模型一般是全卷积网络,所以训练、测试和推理的batch_size都是1。推理使用的数据集为**breakfast**。 diff --git a/docs/zh-CN/contribute/README.md b/docs/zh-CN/contribute/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c22ca32e4bfb7d434d62fd5f821da80493792cb1 --- /dev/null +++ b/docs/zh-CN/contribute/README.md @@ -0,0 +1,5 @@ +# 代码贡献指南 + +- [1. 如何添加新算法](./add_new_algorithm.md) +- [2. 配置系统设计解析](./config.md) +- [3. 如何提pr](./how_to_contribute.md) diff --git a/docs/zh-CN/contribute/add_new_algorithm.md b/docs/zh-CN/contribute/add_new_algorithm.md new file mode 100644 index 0000000000000000000000000000000000000000..25218b342089c48452c1362fc02a74677299938f --- /dev/null +++ b/docs/zh-CN/contribute/add_new_algorithm.md @@ -0,0 +1,414 @@ +# 添加新算法 + +PaddleVideo将一个算法分解为以下几个部分,并对各部分进行模块化处理,方便快速组合出新的算法。 + +* [1. 数据加载和处理](#1) +* [2. 网络](#2) +* [3. 优化器](#3) +* [4. 训练策略](#4) +* [5. 指标评估](#5) + +示例代码如下: +```python +import numpy as np +import paddle +from paddle.io import Dataset, DataLoader +import paddle.nn as nn + +# 1. 数据加载和处理 +## 1.2 数据预处理Pipeline +class ExamplePipeline(object): + """ Example Pipeline""" + def __init__(self, mean=0, std=1.0): + self.mean = mean + self.std = std + + def __call__(self, results): + data = results['data'] + norm_data = (data - self.mean) / self.std + results['data'] = norm_data + return results + +## 1.1 数据集类 +class ExampleDataset(Dataset): + """ExampleDataset""" + def __init__(self): + super(ExampleDataset, self).__init__() + self.x = np.random.rand(100, 20, 20) + self.y = np.random.randint(10, size = (100, 1)) + + def __getitem__(self, idx): + x_item = self.x[idx] + results = {} + results['data'] = x_item + pipeline = ExamplePipeline() + results = pipeline(results) + x_item = results['data'].astype('float32') + y_item = self.y[idx].astype('int64') + return x_item, y_item + + def __len__(self): + return self.x.shape[0] + +train_dataset = ExampleDataset() +## 1.3 封装为Dataloader对象 +train_loader = DataLoader(train_dataset, batch_size=10, shuffle=True) + +# 2. 网络 +class ExampleModel(nn.Layer): + """Example Model""" + def __init__(self): + super(ExampleModel, self).__init__() + ## 2.1 网络Backbobe + self.layer1 = paddle.nn.Flatten(1, -1) + self.layer2 = paddle.nn.Linear(400, 512) + self.layer3 = paddle.nn.ReLU() + self.layer4 = paddle.nn.Dropout(0.2) + ## 2.2 网络Head + self.layer5 = paddle.nn.Linear(512, 10) + + def forward(self, x): + """ model forward""" + y = self.layer1(x) + y = self.layer2(y) + y = self.layer3(y) + y = self.layer4(y) + y = self.layer5(y) + return y + +model = ExampleModel() +model.train() + +# 3. 优化器 +optim = paddle.optimizer.Adam(parameters=model.parameters()) + +epochs = 5 +for epoch in range(epochs): + for batch_id, data in enumerate(train_loader()): + x_data = data[0] + y_data = data[1] + predicts = model(x_data) + + ## 2.3 网络Loss + loss = paddle.nn.functional.cross_entropy(predicts, y_data) + + acc = paddle.metric.accuracy(predicts, y_data) + + loss.backward() + print("epoch: {}, batch_id: {}, loss is: {}, acc is: {}".format(epoch, batch_id, loss.numpy(), acc.numpy())) + + optim.step() + optim.clear_grad() +``` +上述代码的运行输出日志如下: +```txt +epoch: 0, batch_id: 0, loss is: [2.5613842], acc is: [0.] +epoch: 0, batch_id: 1, loss is: [2.5776138], acc is: [0.1] +epoch: 0, batch_id: 2, loss is: [2.551022], acc is: [0.1] +epoch: 0, batch_id: 3, loss is: [2.782001], acc is: [0.] +epoch: 0, batch_id: 4, loss is: [2.787499], acc is: [0.1] +``` +将以上代码集成进PaddleVideo的示例pr参考 [#257](https://github.com/PaddlePaddle/PaddleVideo/pull/257) + +下面将分别对每个部分进行介绍,并介绍如何在该部分里添加新算法所需模块。 + + + +## 1. 数据加载和处理 + +数据加载和处理部分由`Dataset类`、`预处理Pipeline`和`Dataloader对象`组成。`Dataset类`是数据集类,其中的`__getitem__`方法定义了每一个视频样本数据的处理方式。`预处理Pipeline`定义了数据预处理步骤,包括视频的读取,解码以及数据增强等操作。`预处理定义的Pipeline`通常在`Dataset类`的`__getitem__`方法中被调用,以完成对视频预处理操作。这一部分在[paddlevideo/loader](../../../paddlevideo/loader)下。 各个文件及文件夹作用说明如下: + +```txt +paddlevideo/loader/ +├── dataset +│ ├── base.py # Dataset基类 +│ ├── frame.py # 处理Frame格式输入的Dataset类 +│ └── video.py # 处理Video格式输入的Dataset类 +├── pipelines +│ ├── decode.py # 解码Pipeline,对视频进行解码 +│ ├── sample.py # 抽帧Pipeline,对视频抽帧的方式 +│ ├── augmentations.py # 数据增强Pipeline,包括缩放、裁剪、反转、正则化等 +... +``` + +PaddleVideo内置了针对不同数据集的Dataset相关模块,对于没有内置的模块可通过如下步骤添加: + +1. 在 [paddlevideo/loader/dataset](../../../paddlevideo/loader/dataset) 文件夹下新建文件,如my_dataset.py。 +2. 在 my_dataset.py 文件内添加相关代码,示例代码如下: + +```python +@DATASETS.register() # 通过装饰器,自动进行注册 +class MyDataset: + def __init__(self, *args, **kwargs): + # your init code + pass + + def load_file(self): + info = [] + # load file list + return info + + def prepare_train(self, idx): + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) # train pipeline + return results['image'], results['labels'] #return your data item + + def prepare_test(self, idx): + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) # test pipeline + return results['image'], results['labels'] #return your data item +``` + +3. 在 [paddlevideo/loader/dataset/\_\_init\_\_.py](../../../paddlevideo/loader/dataset/__init__.py) 文件内导入添加的模块。 + +最后在config文件中指定Dataset类名即可使用。如: + +```yaml +# Define your Dataset name and args +DATASET: + batch_size: 16 # single-card bacth size + num_workers: 4 # the number of subprocess on each GPU. + train: + format: "FrameDataset" # Dataset class + data_prefix: "data/k400/rawframes" # train data root path + file_path: "data/k400/train_frames.list" # train data list file path + suffix: 'img_{:05}.jpg' + valid: + format: "FrameDataset" # Dataset class + data_prefix: "data/k400/rawframes" # valid data root path + file_path: "data/k400/train_frames.list" # valid data list file path + suffix: 'img_{:05}.jpg' + test: + format: "FrameDataset" # Dataset class + data_prefix: "data/k400/rawframes" # test data root path + file_path: "data/k400/train_frames.list" # test data list file path + suffix: 'img_{:05}.jpg' +``` + +- 关于模块注册机制的详细说明,可以参考[配置系统设计](./config.md) + +PaddleVideo内置了大量视频编解码及图像变换相关模块,对于没有内置的模块可通过如下步骤添加: + +1. 在 [paddlevideo/loader/pipelines](../../../paddlevideo/loader/pipelines) 文件夹下新建文件,如my_pipeline.py。 +2. 在 my_pipeline.py 文件内添加相关代码,示例代码如下: + +```python +@PIPELINES.register() # 通过装饰器,自动进行注册 +class MyPipeline: + def __init__(self, *args, **kwargs): + # your init code + pass + + def __call__(self, results): + img = results['image'] + label = results['label'] + # your process code + + results['image'] = img + results['label'] = label + return results +``` + +3. 在 [paddlevideo/loader/pipelines/\_\_init\_\_.py](../../../paddlevideo/loader/pipelines/__init__.py) 文件内导入添加的模块。 + +数据处理的所有处理步骤由不同的模块顺序执行而成,在config文件中按照列表的形式组合并执行。如: + +```yaml +# Define your pipeline name and args +PIPELINE: + train: + decode: + name: "FrameDecoder" # Pipeline Class name + sample: + name: "Sampler" # Pipeline Class name + num_seg: 8 # init args + seg_len: 1 # init args + valid_mode: False # init args + transform: + - Scale: # Pipeline Class name + short_size: 256 # init args +``` + + + +## 2. 网络 + +网络部分完成了网络的组网操作,PaddleVideo将网络划分为四三部分,这一部分在[paddlevideo/modeling](../../../paddlevideo/modeling)下。 进入网络的数据将按照顺序(backbones->heads->loss)依次通过这三个部分。backbone用于特征提取,loss通过heads的[loss方法](https://github.com/PaddlePaddle/PaddleVideo/blob/5f7e22f406d11912eef511bafae28c594ccaa07e/paddlevideo/modeling/heads/base.py#L67)被调用。除了损失值,训练过程中如果想观察其它的精度指标(如top1, top5),也可以在head中定义相应的计算方法,参考[get_acc方法](https://github.com/PaddlePaddle/PaddleVideo/blob/5f7e22f406d11912eef511bafae28c594ccaa07e/paddlevideo/modeling/heads/base.py#L122),loss模块最终返回一个[loss字典](https://github.com/PaddlePaddle/PaddleVideo/blob/5f7e22f406d11912eef511bafae28c594ccaa07e/paddlevideo/modeling/heads/base.py#L81),存储loss值以及其它需要的精度指标。 + +```bash +├── framework # 组合backbones->heads->loss,定义从输入数据到输出loss的过程 +├── backbones # 网络的特征提取模块 +├── heads # 网络的输出模块 +└── losses # 网络的损失函数模块 +``` + +PaddleVideo内置了TSN、TSM、SlowFast、ST-GCN、BMN等算法相关的常用模块,对于没有内置的模块可通过如下步骤添加,四个部分添加步骤一致,以backbones为例: + +1. 在 [paddlevideo/modeling/backbones](../../../paddlevideo/modeling/backbones) 文件夹下新建文件,如my_backbone.py。 +2. 在 my_backbone.py 文件内添加相关代码,示例代码如下: + +```python +@BACKBONES.register() # 通过装饰器,自动进行注册 +class MyBackbone(nn.Layer): + def __init__(self, *args, **kwargs): + super(MyBackbone, self).__init__() + # your init code + self.conv = nn.xxxx + + def forward(self, inputs): + # your network forward + y = self.conv(inputs) + return y +``` + +3. 在 [paddlevideo/modeling/backbones/\_\_init\_\_.py](../../../paddlevideo/modeling/backbones/__init__.py)文件内导入添加的模块。 + +在完成网络的四部分模块添加之后,只需要配置文件中进行配置即可使用,如: + +```yaml +MODEL: + framework: "Recognizer2D" # Framework class name + backbone: + name: "ResNetTweaksTSM" # Backbone class name + depth: 50 # init args + head: + name: "ppTSMHead" # Heads class name + num_classes: 400 # init args + loss: + name: "MyLoss" # Losses class name + scale: 0.1 # init args +``` + + + +## 3. 优化器 + +优化器用于训练网络。优化器内部还包含了网络正则化和学习率衰减模块。 这一部分在[paddlevideo/solver/](../../../paddlevideo/solver/)下。 PaddleVideo内置了飞桨框架所有的[优化器模块](https://www.paddlepaddle.org.cn/documentation/docs/zh/2.1/api/paddle/optimizer/Overview_cn.html#api)和[学习率衰减模块](https://www.paddlepaddle.org.cn/documentation/docs/zh/2.1/api/paddle/optimizer/Overview_cn.html#about-lr)。只需要在配置文件中指定相应模块名称及参数即可方便的调用,示例: + +```yaml +OPTIMIZER: + name: 'Momentum' # Optimizer class name + momentum: 0.9 # init args + learning_rate: + name: 'PiecewiseDecay' # Learning rate scheduler class name + boundaries: [10, 20] # init args + values: [0.001, 0.0001, 0.00001] # init args +``` + +对于没有内置的模块可通过如下步骤添加,以`learning rate`为例: + +1. 在 [paddlevideo/solver/custom_lr.py](../../../paddlevideo/solver/custom_lr.py) 文件内创建自己的学习率调整策略,示例代码如下: + +```python +class MyLR(LRScheduler): + def __init__(self, *args, **kwargs): + self.learning_rate = learning_rate + + def step(self, epoch): + # learning rate step scheduler + self.last_lr = xxx + +``` + +在学习率模块添加之后,只需要配置文件中进行配置即可使用,如: + +```yaml +OPTIMIZER: + name: 'Momentum' + momentum: 0.9 + learning_rate: + iter_step: True + name: 'CustomWarmupCosineDecay' # LR class name + max_epoch: 80 # init args + warmup_epochs: 10 # init args +``` + + + +## 4. 训练策略 + +PaddleVideo内置了很多模型训练相关trick,包括标签平滑、数据增强Mix-up、PreciseBN等,只需要在配置文件中指定相应模块名称及参数即可方便的调用,示例: + +```yaml + +MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTweaksTSM" + head: + name: "ppTSMHead" + ls_eps: 0.1 # ls_eps字段添加label smooth,并指定平滑系数 + +MIX: + name: "Mixup" # 添加数据增强 Mix-up策略 + alpha: 0.2 # 指定mix系数 + +PRECISEBN: # 添加preciseBN策略 + preciseBN_interval: 5 # 指定prciseBN间隔 + num_iters_preciseBN: 200 # 指定preciseBN运行的batchs数量 + +``` + +训练相关的代码通过[paddlevideo/tasks/train.py](../../../paddlevideo/tasks/train.py)被组织起来,最终被[PaddleVideo/main.py](../../../../PaddleVideo/main.py)调用启动训练,单卡训练和多卡训练的启动方式略有不同。单卡训练启动方式如下: + +```bash +export CUDA_VISIBLE_DEVICES=0 #指定使用的GPU显卡id +python3.7 main.py --validate -c configs_path/your_config.yaml +``` +- `--validate` 参数指定训练时运行validation +- `-c` 参数指定配置文件路径 +- `-o`: 指定重写参数,例如: `-o DATASET.batch_size=16` 用于重写train时batch size大小 + +多卡训练通过paddle.distributed.launch启动,方式如下: +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +``` +- `--gpus`参数指定使用的GPU显卡id +- `--log_dir`参数指定日志保存目录 +多卡训练详细说明可以参考[单机多卡训练](https://www.paddlepaddle.org.cn/documentation/docs/zh/2.1/guides/02_paddle2.0_develop/06_device_cn.html#danjiduokaxunlian) + + + + +## 5. 指标评估 + +训练完成后,需要进行指标评估,paddlevideo将指标评估模块与训练模块解耦,通过在[PaddleVideo/main.py](../../../../PaddleVideo/main.py)运行时指定`--test`参数调用test模块进行指标评估,评估方法的实现主体在[paddlevideo/metrics/](../../../paddlevideo/metrics)下。 PaddleVideo内置了Uniform、Dense等相关的指标评估模块,对于没有内置的模块可通过如下步骤添加: + +1. 在 [paddlevideo/metrics/](../../../paddlevideo/metrics/) 文件夹下新建文件,如my_metric.py。 +2. 在 my_metric.py 文件内添加相关代码,示例代码如下: + +```python +@METRIC.register # 通过装饰器,自动进行注册 +class MyMetric(BaseMetric): + def __init__(self, *args, **kwargs): + self.top1 = [] + + def update(self, batch_id, data, outputs): + # update metrics during each iter + self.top1.append(xx) + + def accumulate(self): + # accumulate metrics when finished all iters. + xxx + print(np.mean(np.array(self.top1))) + +``` + +3. 在 [paddlevideo/metrics/\_\_init\_\_.py](../../../paddlevideo/metrics/__init__.py)文件内导入添加的模块。 + +在指标评估模块添加之后,只需要配置文件中进行配置即可使用,如: + +```yaml +METRIC: + name: 'CenterCropMetric' # Metric class name +``` + +模型测试运行方法如下: +```bash +python3.7 main.py --test -c config_path/your_config.yaml -w weight_path/your_weight.pdparams +``` +- `--test`参数指定运行测试模式 +- `-c`参数指定配置文件 +- `-w`参数指定训练好的权重保存路径 + diff --git a/docs/zh-CN/contribute/config.md b/docs/zh-CN/contribute/config.md new file mode 100644 index 0000000000000000000000000000000000000000..a38964c456402ce380b24b9fbddce355f8957153 --- /dev/null +++ b/docs/zh-CN/contribute/config.md @@ -0,0 +1,242 @@ +简体中文 | [English](../../en/tutorials/config.md) + +# 配置系统设计 + +--- + +本文档将介绍PaddleVideo利用依赖注入技术实现控制反转,来对整个系统进行解耦,通过可自定义调整的配置文件来控制整个系统从而实现模块化。最后,介绍了配置文件和PaddleVideo运行时参数的含义。 + + +## 设计原则 + +首先,模型库中会有很多对一个类实例化的操作,例如: + +```python +class TSM(): + pass + +model = TSM(init_attributes) +``` +当越来越多的实例被创建,这种调用方法和被调用方法间的联系陡然上升,增加了整个系统的耦合性,对启用新功能建设,或是对已用功能扩展产生不便。 +当然我们可以建立一个工厂模式来解决这个问题,根据配置文件的指定输入,来统一的做条件判断: + +```python +if model_name == "TSM": + model = TSM() +elif model_name == "TSN": + model = TSN() +elif ... +``` +或是像如下代码片段 + +```python +optimizer_cfg = dict(name:"MOMENTUM", params: XXX) +if optimizer_cfg.name = "MOMENTUM": + optimizer = MOMENTUM(optimizer_cfg.pop(name)) +elif: + ... +``` + +可是,越来越多的条件判断被创建出来,还是没有统一彻底的解决这个问题。 +而在其他系统中被广泛利用的 控制反转/依赖注入 技术,PaddleVideo将其利用起来进行系统解耦,并应用到诸如 LOSS METRICS BACKBONE HEAD等场景中。 +PaddleVideo实现了两个组件用于完成控制反转/依赖注入: + +- Register, 注册器,用于注册一个模块组件 +- Builder, 用于建立(实例化)一个已注册的组件 + +1. Register 注册器 + +PaddleVideo实现了类似setter和getter方法 + +[source code](../../paddlevideo/utils/registry.py) + +```python +#excerpt from source code. +class Registry(): + def __init__(self, name): + self._name = name + self._obj_map = {} + + #mapping name -> object + def register(self, obj, name): + self._obj_map[name] = obj + + #get object + def get(self, name): + ret = self._obj_map.get(name) + return ret +``` + +用于建立字符串和对象的map,如下的代码将ResNet类注册到BACKBONE map中 + +```python + + BACKBONES = Registry('backbone') + class ResNet: + pass + BACKBONES.register(ResNet) +``` + +或是通过python3语法糖来装饰一个类 + +```python + BACKBONES = Registry('backbone') #new a Register + @BACKBONES.register() #regist resnet as a backbone. + class ResNet: + pass +``` + +2. Builder + +应用python的反射机制,调用get方法 得到一个已经注册的模块: +```python + # Usage: To build a module. + + backbone_name = "ResNet" + b = BACKBONES.get(backbone_name)() +``` + +至此,PaddleVideo注册了一个实例,不是在他的调用地方,而是在他的声明处,一个简单的IoC系统建立起来了。 +最后,PaddleVideo 通过这种方式建立了所有组件,并和配置文件参数一一对应。这里,一一对应的含义是:配置文件中的字段,`name` 代表着类的名字,其余字段对应着这个类的初始化参数。当然,除了`name` 我们也应用了别的名字来标记类名,例如:`framework` + +```yaml +head: + name: "TSMHead" # class name + num_classes: 400 # TSMHead class init attributes + ... +``` + +--- + +## 配置参数 + +配置文件中,有多组字段,如下 + +- **MODEL:** 代笔模型结构 +- **DATASET:** 数据集和dataloader配置 +- **PIPELINE:** 数据处理流程配置字段 +- **OPTIMIZER:** 优化器字段 + +和一些共有的参数, 如: + +- model_name +- log_interval +- epochs +- resume_epoch +- log_level +... + +## 模块概览 + + + + + + + + + + + + + + + + + + + + +
+ Architectures + + Frameworks + + Components + + Data Augmentation +
+
  • Recognition
  • +
      +
    • TSN
    • +
    • TSM
    • +
    • SlowFast
    • +
    • PP-TSM
    • +
    • VideoTag
    • +
    • AttentionLSTM
    • +
    +
+
  • Localization
  • +
      +
    • BMN
    • +
    +
+
+
  • Recognizer1D
  • +
  • Recognizer2D
  • +
  • Recognizer3D
  • +
  • Localizer
  • +
    +
      Backbone +
    • resnet
    • +
    • resnet_tsm
    • +
    • resnet_tweaks_tsm
    • +
    • bmn
    • +
    +
      Head +
    • pptsm_head
    • +
    • tsm_head
    • +
    • tsn_head
    • +
    • bmn_head
    • + + +
    +
    +
    • Solver
    • +
      • Optimizer
      • +
          +
        • Momentum
        • +
        • RMSProp
        • +
        +
      +
      • LearningRate
      • +
          +
        • PiecewiseDecay
        • +
        +
      +
    +
    • Loss
    • +
        +
      • CrossEntropy
      • +
      • BMNLoss
      • +
      +
    +
    • Metrics
    • +
        +
      • CenterCrop
      • +
      • MultiCrop
      • +
      +
    +
    +
    • Video
    • +
        +
      • Mixup
      • +
      • Cutmix
      • +
      +
    +
    • Image
    • +
        +
      • Scale
      • +
      • Random FLip
      • +
      • Jitter Scale
      • +
      • Crop
      • +
      • MultiCrop
      • +
      • Center Crop
      • +
      • MultiScaleCrop
      • +
      • Random Crop
      • +
      • PackOutput
      • +
      +
    +
    + +--- diff --git a/docs/zh-CN/contribute/how_to_contribute.md b/docs/zh-CN/contribute/how_to_contribute.md new file mode 100644 index 0000000000000000000000000000000000000000..752d38a66498867f03045c60aa412bd8c9e22a79 --- /dev/null +++ b/docs/zh-CN/contribute/how_to_contribute.md @@ -0,0 +1,262 @@ +# PaddleVideo 社区贡献指南 +--- + +## 目录 + +- [如何贡献代码](#1) + - [1.1 PaddleVideo 分支说明](#1.1) + - [1.2 PaddleVideo 代码提交流程与规范](#1.2) + - [1.2.1 fork 和 clone 代码](#1.2.1) + - [1.2.2 和远程仓库建立连接](#1.2.2) + - [1.2.3 创建本地分支](#1.2.3) + - [1.2.4 使用 pre-commit 勾子](#1.2.4) + - [1.2.5 修改与提交代码](#1.2.5) + - [1.2.6 保持本地仓库最新](#1.2.6) + - [1.2.7 push到远程仓库](#1.2.7) + - [1.2.8 提交Pull Request](#1.2.8) + - [1.2.9 签署 CLA 协议和通过单元测试](#1.2.9) + - [1.2.10 删除分支](#1.2.10) + - [1.2.11 提交代码的一些约定](#1.2.11) +- [总结](#2) +- [参考文献](#3) + + +## 一、如何贡献代码 + + +### 1.1 PaddleVideo 分支说明 + +PaddleVideo 未来将维护 2 种分支,分别为: + +* release/x.x.x 系列分支:为稳定的发行版本分支,会适时打 tag 发布版本,适配 Paddle 的 release 版本。当前最新的分支为 release/2.2.0 分支。随着版本迭代, release/x.x.x 系列分支会越来越多,默认维护最新版本的 release 分支,其他的分支不再维护。 +* develop 分支:为开发分支,也是默认分支,适配 Paddle 的 develop 版本,主要用于开发新功能。如果有同学需要进行二次开发,请选择 develop 分支。为了保证 develop 分支能在需要的时候拉出 release/x.x.x 分支, develop 分支的代码只能使用 Paddle 最新 release 分支中有效的 api 。也就是说,如果 Paddle develop 分支中开发了新的 api,但尚未出现在 release 分支代码中,那么请不要在 PaddleVideo 中使用。除此之外,对于不涉及 api 的性能优化、参数调整、策略更新等,都可以正常进行开发。 + +PaddleVideo 的历史分支,未来将不再维护。考虑到一些同学可能仍在使用,这些分支还会继续保留: + +* application 分支:这个分支主要存放应用案例相关代码,目前包括VideoTag和FootballAction,后续会将此分支代码迁移至develop分支,并随 release/x.x.x 发版。 + + +PaddleVideo 欢迎大家向 repo 中积极贡献代码,下面给出一些贡献代码的基本流程。 + + +### 1.2 PaddleVideo 代码提交流程与规范 + + +#### 1.2.1 fork 和 clone 代码 + +* 跳转到 [PaddleVideo GitHub首页](https://github.com/PaddlePaddle/PaddleVideo) ,然后单击 Fork 按钮,生成自己目录下的仓库,比如 `https://github.com/USERNAME/PaddleVideo` 。 + + +
    + +
    + + +* 将远程仓库 clone 到本地 + +```shell +# 拉取develop分支的代码 +git clone https://github.com/USERNAME/PaddleVideo.git +cd PaddleVideo +``` + +clone 的地址可以从下面获取 + +
    + +
    + + +#### 1.2.2 和远程仓库建立连接 + +首先通过 `git remote -v` 查看当前远程仓库的信息。 + +``` +origin https://github.com/USERNAME/PaddleVideo.git (fetch) +origin https://github.com/USERNAME/PaddleVideo.git (push) +``` + +上面的信息只包含了 clone 的远程仓库的信息,也就是自己用户名下的 PaddleVideo ,接下来我们创建一个原始 PaddleVideo 仓库的远程主机,命名为 upstream 。 + +```shell +git remote add upstream https://github.com/PaddlePaddle/PaddleVideo.git +``` + +使用 `git remote -v` 查看当前远程仓库的信息,输出如下,发现包括了 origin 和 upstream 2 个远程仓库。 + +``` +origin https://github.com/USERNAME/PaddleVideo.git (fetch) +origin https://github.com/USERNAME/PaddleVideo.git (push) +upstream https://github.com/PaddlePaddle/PaddleVideo.git (fetch) +upstream https://github.com/PaddlePaddle/PaddleVideo.git (push) +``` + +这主要是为了后续在提交 pull request (PR) 时,始终保持本地仓库最新。 + + +#### 1.2.3 创建本地分支 + +可以基于当前分支创建新的本地分支,命令如下。 + +```shell +git checkout -b new_branch +``` + +也可以基于远程或者上游的分支创建新的分支,命令如下。 + +```shell +# 基于用户远程仓库(origin)的develop创建new_branch分支 +git checkout -b new_branch origin/develop +# 基于上游远程仓库(upstream)的develop创建new_branch分支 +# 如果需要从upstream创建新的分支,需要首先使用git fetch upstream获取上游代码 +git checkout -b new_branch upstream/develop +``` + +最终会显示切换到新的分支,输出信息如下 + +``` +Branch new_branch set up to track remote branch develop from upstream. +Switched to a new branch 'new_branch' +``` + + +#### 1.2.4 使用 pre-commit 勾子 + +Paddle 开发人员使用 pre-commit 工具来管理 Git 预提交钩子。 它可以帮助我们格式化源代码(C++,Python),在提交(commit)前自动检查一些基本事宜(如每个文件只有一个 EOL,Git 中不要添加大文件等)。 + +pre-commit 测试是 Travis-CI 中单元测试的一部分,不满足钩子的 PR 不能被提交到 PaddleVideo ,首先安装并在当前目录运行它: + +```shell +pip install pre-commit +pre-commit install +``` + +* **注意** + +1. Paddle 使用 clang-format 来调整 C/C++ 源代码格式,请确保 `clang-format` 版本在 3.8 以上。 +2. 通过 `pip install pre-commit` 和 `conda install -c conda-forge pre-commit` 安装的 `yapf` 稍有不同的,PaddleVideo 开发人员使用的是 `pip install pre-commit` 。 + + +#### 1.2.5 修改与提交代码 + +可以通过 `git status` 查看改动的文件。 +对 PaddleVideo 的 `README.md` 做了一些修改,希望提交上去。则可以通过以下步骤 + +```shell +git add README.md +pre-commit +``` + +重复上述步骤,直到 pre-comit 格式检查不报错。如下所示。 + +
    + +
    + + +使用下面的命令完成提交。 + +```shell +git commit -m "your commit info" +``` + + +#### 1.2.6 保持本地仓库最新 + +获取 upstream 的最新代码并更新当前分支。这里的 upstream 来自于 1.2 节的`和远程仓库建立连接`部分。 + +```shell +git fetch upstream +# 如果是希望提交到其他分支,则需要从upstream的其他分支pull代码,这里是develop +git pull upstream develop +``` + + +#### 1.2.7 push到远程仓库 + +```shell +git push origin new_branch +``` + + +#### 1.2.8 提交Pull Request + +点击 new pull request,选择本地分支和目标分支,如下图所示。在 PR 的描述说明中,填写该 PR 所完成的功能。接下来等待 review ,如果有需要修改的地方,参照上述步骤更新 origin 中的对应分支即可。 + +
    + +
    + + +#### 1.2.9 签署 CLA 协议和通过单元测试 + +* 签署 CLA +在首次向 PaddlePaddle 提交 Pull Request 时,您需要您签署一次 CLA (Contributor License Agreement) 协议,以保证您的代码可以被合入,具体签署方式如下: + +1. 请您查看 PR 中的 Check 部分,找到 license/cla ,并点击右侧 detail ,进入 CLA 网站 +2. 点击 CLA 网站中的 `Sign in with GitHub to agree` , 点击完成后将会跳转回您的 Pull Request 页面 + + +#### 1.2.10 删除分支 + +* 删除远程分支 + +在 PR 被 merge 进主仓库后,我们可以在 PR 的页面删除远程仓库的分支。 + +也可以使用 `git push origin :分支名` 删除远程分支,如: + + +```shell +git push origin :new_branch +``` + +* 删除本地分支 + +```shell +# 切换到develop分支,否则无法删除当前分支 +git checkout develop + +# 删除new_branch分支 +git branch -D new_branch +``` + + +#### 1.2.11 提交代码的一些约定 + +为了使官方维护人员在评审代码时更好地专注于代码本身,请您每次提交代码时,遵守以下约定: + +1)请保证 Travis-CI 中单元测试能顺利通过。如果没过,说明提交的代码存在问题,官方维护人员一般不做评审。 + +2)提交 Pull Request前: + +请注意 commit 的数量。 + +原因:如果仅仅修改一个文件但提交了十几个 commit ,每个 commit 只做了少量的修改,这会给评审人带来很大困扰。评审人需要逐一查看每个 commit 才能知道做了哪些修改,且不排除 commit 之间的修改存在相互覆盖的情况。 + +建议:每次提交时,保持尽量少的 commit ,可以通过 `git commit --amend` 补充上次的 commit 。对已经 Push 到远程仓库的多个 commit ,可以参考 [squash commits after push](https://stackoverflow.com/questions/5667884/how-to-squash-commits-in-git-after-they-have-been-pushed) 。 + +请注意每个 commit 的名称:应能反映当前 commit 的内容,不能太随意。 + +3)如果解决了某个 Issue 的问题,请在该 Pull Request 的第一个评论框中加上: `fix #issue_number` ,这样当该 Pull Request 被合并后,会自动关闭对应的 Issue 。关键词包括: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved ,请选择合适的词汇。详细可参考 [Closing issues via commit messages](https://help.github.com/articles/closing-issues-via-commit-messages) 。 + +此外,在回复评审人意见时,请您遵守以下约定: + +1)官方维护人员的每一个 review 意见都希望得到回复,这样会更好地提升开源社区的贡献。 + +- 对评审意见同意且按其修改完的,给个简单的 Done 即可; +- 对评审意见不同意的,请给出您自己的反驳理由。 + +2)如果评审意见比较多, + +- 请给出总体的修改情况。 +- 请采用 `start a review` 进行回复,而非直接回复的方式。原因是每个回复都会发送一封邮件,会造成邮件灾难。 + + +## 二、总结 + +* 开源社区依赖于众多开发者与用户的贡献和反馈,在这里感谢与期待大家向 PaddleVideo 提出宝贵的意见与 Pull Request ,希望我们可以一起打造一个领先实用全面的视频理解代码仓库! + + +## 三、参考文献 +1. [PaddlePaddle本地开发指南](https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/10_contribution/local_dev_guide_cn.html) +2. [向开源框架提交pr的过程](https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh_CN/advanced_tutorials/how_to_contribute.md) diff --git a/docs/zh-CN/dataset/AVA.md b/docs/zh-CN/dataset/AVA.md new file mode 100644 index 0000000000000000000000000000000000000000..a30f5d8c3bbb30dc9865306edfa93d108535d919 --- /dev/null +++ b/docs/zh-CN/dataset/AVA.md @@ -0,0 +1,105 @@ +[English](../../en/dataset/AVA.md) | 简体中文 +# AVA数据准备 +此文档主要介绍AVA数据集的相关准备流程。主要介绍 AVA数据集的视频文件下载,标注文件准备,视频文件切分 +视频文件提取帧数据,以及拉取提名文件等。在开始之前,请把当前工作目录设定在 `$PaddleVideo/data/ava/shell` + +--- + +## 1. 视频数据下载 +想要获取更多有关AVA数据集的信息,您可以访问其官方网站[AVA](https://research.google.com/ava/index.html). +至于数据集下载,您可以参看考[AVA Download](https://github.com/cvdfoundation/ava-dataset) ,该Repo详细介绍了AVA视频数据的下载方法. +我们也提供了视频文件的下载脚本: + +```shell +bash download_videos.sh +``` + +为了方便用户,我们将视频文件以zip包的形式上传到百度网盘,您可以直接进行下载 [Link]() coming soon. + + +**注意: 您自己下载的视频文件应当被放置在`data/ava/videos`文件夹下** + +--- +## 2.准备标注文件 + +接下来,您可以使用下面的脚本来准备标注文件 + +```shell +bash download_annotations.sh +``` + +该脚本会默认下载`ava_v2.1.zip`,如果您想下载`v2.2`,您可以使用: + +```shell +VERSION=2.2 bash download_annotations.sh +``` + +**注意:事实上,我们也同样在百度网盘中提供了该标注文件,所以您无需自己下载** + +--- +## 3. 切分视频文件 + +以帧率30fps,切分视频文件从第15分钟到第30分钟 + +```shell +bash cut_videos.sh +``` +--- + +## 4. 提取RGB帧 + +您可以通过以下的脚本使用`ffmpeg`来提取RGB帧. + +```shell +bash extract_rgb_frames.sh +``` + +--- + +## 5.拉取提名文件 + +这个脚本来自于Facbook研究院[Long-Term Feature Banks](https://github.com/facebookresearch/video-long-term-feature-banks). +您可以使用如下的脚本来获取预计算的提名文件列表。 + +```shell +bash fetch_ava_proposals.sh +``` + +--- +## 6.目录结构 + +经过整个AVA数据处理流程后,您可以获得AVA的帧文件,视频文件和标注文件 + +整个项目(AVA)的目录结构如下所示: + +``` +PaddleVideo +├── configs +├── paddlevideo +├── docs +├── tools +├── data +│ ├── ava +│ │ ├── annotations +│ │ | ├── ava_dense_proposals_train.FAIR.recall_93.9.pkl +│ │ | ├── ava_dense_proposals_val.FAIR.recall_93.9.pkl +│ │ | ├── ava_dense_proposals_test.FAIR.recall_93.9.pkl +│ │ | ├── ava_train_v2.1.csv +│ │ | ├── ava_val_v2.1.csv +│ │ | ├── ava_train_excluded_timestamps_v2.1.csv +│ │ | ├── ava_val_excluded_timestamps_v2.1.csv +│ │ | ├── ava_action_list_v2.1_for_activitynet_2018.pbtxt +│ │ ├── videos +│ │ │ ├── 053oq2xB3oU.mkv +│ │ │ ├── 0f39OWEqJ24.mp4 +│ │ │ ├── ... +│ │ ├── videos_15min +│ │ │ ├── 053oq2xB3oU.mkv +│ │ │ ├── 0f39OWEqJ24.mp4 +│ │ │ ├── ... +│ │ ├── rawframes +│ │ │ ├── 053oq2xB3oU +| │ │ │ ├── img_00001.jpg +| │ │ │ ├── img_00002.jpg +| │ │ │ ├── ... +``` \ No newline at end of file diff --git a/docs/zh-CN/dataset/ActivityNet.md b/docs/zh-CN/dataset/ActivityNet.md new file mode 100644 index 0000000000000000000000000000000000000000..68a7fd90bd439dfe975b06803b7edf7215669ba0 --- /dev/null +++ b/docs/zh-CN/dataset/ActivityNet.md @@ -0,0 +1,80 @@ +[English](../../en/dataset/ActivityNet.md) | 简体中文 + +# ActivityNet数据准备 + +- [数据集介绍](#数据集介绍) +- [数据下载与处理](#数据下载与处理) + +## 数据集介绍 + +ActivityNet是一个用于大规模视频理解任务的数据集,可用于动作定位、动作识别等任务。 + + +## 数据下载与处理 +1. BMN模型使用的是处理过后的ActivityNet 1.3数据集,有如下两种使用方法: + - 使用我们处理好的ActivityNet 1.3数据集(压缩包约5.5G),每一个视频有对应的动作标签、持续区间、持续帧数、持续秒数等信息 + 使用以下命令下载: + ```bash + wget https://paddlemodels.bj.bcebos.com/video_detection/bmn_feat.tar.gz # 下载处理好的视频特征数据 + wget https://paddlemodels.bj.bcebos.com/video_detection/activitynet_1.3_annotations.json # 下载处理好的标签数据 + ``` + 或者点击以下超链接下载: + + [视频特征数据](https://paddlemodels.bj.bcebos.com/video_detection/bmn_feat.tar.gz) + [视频特征数据](https://paddlemodels.bj.bcebos.com/video_detection/activitynet_1.3_annotations.json) + + 然后解压下下好的视频特征压缩包 + ```bash + tar -xf bmn_feat.tar.gz + ``` + + - 自行提取特征 + + 首先参考[下载说明](https://github.com/activitynet/ActivityNet/tree/master/Crawler)下载原始数据集。在训练此模型时,需要先使用TSN对源文件抽取特征。可以[自行抽取](https://github.com/yjxiong/temporal-segment-networks)视频帧及光流信息,预训练好的TSN模型可从[此处](https://github.com/yjxiong/anet2016-cuhk)下载。 + + + `activitynet_1.3_annotations.json`标签文件内的信息如下所示: + ```json + { + "v_QOlSCBRmfWY": { + "duration_second": 82.73, + "subset": "training", + "duration_frame": 2067, + "annotations": [{ + "segment": [6.195294851794072, 77.73085420904837], + "label": "Ballet" + }], + "feature_frame": 2064 + }, + "v_ehGHCYKzyZ8": { + "duration_second": 61.718999999999994, + "subset": "training", + "duration_frame": 1822, + "annotations": [{ + "segment": [43.95990729267573, 45.401932082395355], + "label": "Doing crunches" + }], + "feature_frame": 1808 + }, + ..., + ... + } + ``` + 最终应该能得到`19228`个视频特征npy文件,对应`activitynet_1.3_annotations.json`文件中的`19228`个标签信息。 + +2. 新建`data/bmn_data`文件夹,再将下载完毕后将视频特征数据解压出来放入该文件夹下,最终应该组织成以下形式: + ``` + PaddleVideo + ├── data + │ ├── bmn_data + │ │ ├── fix_feat_100 + │ │ │ ├── v___c8enCfzqw.npy + │ │ │ ├── v___dXUJsj3yo.npy + │ │ │ ├── ... + │ │ │ + │ │ └── activitynet_1.3_annotations.json + ``` + +3. 最后修改配置文件configs/localization/bmn.yaml中的`feat_path`字段指定特征文件夹路径,通过`file_path`字段指定标签文件路径。 + + diff --git a/docs/zh-CN/dataset/Oxford_RobotCar.md b/docs/zh-CN/dataset/Oxford_RobotCar.md new file mode 100644 index 0000000000000000000000000000000000000000..ee12ff8856e552b5c52d4c79b8bec51aa3b8cb16 --- /dev/null +++ b/docs/zh-CN/dataset/Oxford_RobotCar.md @@ -0,0 +1,152 @@ +[English](../../en/dataset/Oxford_RobotCar.md) | 简体中文 + +# Oxford-RobotCar-for-ADDS数据准备 + +- [数据集简介](#数据集简介) +- [数据集下载](#数据集下载) +- [数据预处理](#数据预处理) +- [1. 图像去畸变](#1-图像去畸变) +- [2. 动态帧筛选](#2-动态帧筛选) +- [3. 图像重命名](#3-图像重命名) +- [4. 白天-伪夜晚图像对准备](#4-白天-伪夜晚图像对准备) + + +## 数据集简介 + +[Oxford RobotCar Dataset](https://robotcar-dataset.robots.ox.ac.uk/) 是一个大规模自动驾驶数据集, 包含了大量不同自动驾驶场景下的数据. + +这里用到的是从原始的Oxford RobotCar数据集中筛选出一部分用于白天-夜晚深度估计的数据, 即Oxford-RobotCar-for-ADDS. + +如果您要使用Oxford-RobotCar-for-ADDS, 请引用以下论文: +```latex +@article{maddern20171, + title={1 year, 1000 km: The oxford robotcar dataset}, + author={Maddern, Will and Pascoe, Geoffrey and Linegar, Chris and Newman, Paul}, + journal={The International Journal of Robotics Research}, + volume={36}, + number={1}, + pages={3--15}, + year={2017}, + publisher={SAGE Publications Sage UK: London, England} +} +``` +```latex +@inproceedings{liu2021self, + title={Self-supervised Monocular Depth Estimation for All Day Images using Domain Separation}, + author={Liu, Lina and Song, Xibin and Wang, Mengmeng and Liu, Yong and Zhang, Liangjun}, + booktitle={Proceedings of the IEEE/CVF International Conference on Computer Vision}, + pages={12737--12746}, + year={2021} +} +``` + +## 数据集下载 + +1. 下载序列[2014-12-09](https://robotcar-dataset.robots.ox.ac.uk/datasets/2014-12-09-13-21-02/) 中Bumblebee XB3的左目图像作为白天场景的训练集, 下载好的图像解压在同一文件夹下. +2. 下载序列[2014-12-16](https://robotcar-dataset.robots.ox.ac.uk/datasets/2014-12-16-18-44-24/) 中Bumblebee XB3的左目图像作为夜晚场景的训练集, 下载好的图像解压在同一文件夹下. +3. 验证集的图像和深度真值从原始数据集中筛选, 下载地址如下: + ```shell + https://videotag.bj.bcebos.com/Data/ADDS/1209_all_files.txt + https://videotag.bj.bcebos.com/Data/ADDS/1216_all_files.txt + https://videotag.bj.bcebos.com/Data/ADDS/day_train_all.7z.001 + https://videotag.bj.bcebos.com/Data/ADDS/day_train_all.7z.002 + https://videotag.bj.bcebos.com/Data/ADDS/day_train_all_fake_night.7z.001 + https://videotag.bj.bcebos.com/Data/ADDS/day_train_all_fake_night.7z.002 + https://videotag.bj.bcebos.com/Data/ADDS/day_val_451.7z + https://videotag.bj.bcebos.com/Data/ADDS/day_val_451_gt.7z + https://videotag.bj.bcebos.com/Data/ADDS/night_val_411.7z + https://videotag.bj.bcebos.com/Data/ADDS/night_val_411_gt.7z + ``` + 附原始未处理数据下载地址: + ```shell + # 白天数据 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.001 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.002 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.003 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.004 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.005 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.006 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.007 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.008 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.009 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.010 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.011 + https://videotag.bj.bcebos.com/Data/original-ADDS/day_train_all.7z.012 + + # 夜晚数据 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.001 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.002 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.003 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.004 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.005 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.006 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.007 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.008 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.009 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.010 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.011 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.012 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.013 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.014 + https://videotag.bj.bcebos.com/Data/original-ADDS/night_train_all.7z.015 + ``` +## 数据预处理 + +#### 1. 图像去畸变 + +使用官方提供的工具箱[robotcar-dataset-sdk](https://github.com/ori-mrg/robotcar-dataset-sdk/tree/master/python) 对序列2014-12-09和2014-12-16的图像完成去畸变. + + +#### 2. 动态帧筛选 + +由于我们使用自监督的方法, 需要筛选出动态帧用于训练. 筛选原则为帧间位姿变化大于0.1m则认为是动态帧. 经过筛选后获得训练集的序列. + + +#### 3. 图像重命名 + +将原始图像时间戳重命名为连续数字序列. 白天场景对应关系见[1209_all_files.txt](https://videotag.bj.bcebos.com/Data/ADDS/1209_all_files.txt), 夜晚场景对应关系见[1216_all_files.txt](https://videotag.bj.bcebos.com/Data/ADDS/1216_all_files.txt). 重命名后的数据格式如下: +``` +├── oxford_processing + ├── day_train_all #白天训练图像文件夹 (day_train_all.7z.001 ~ day_train_all.7z.012) + ├── night_train_all #夜晚训练图像文件夹 (night_train_all.7z.001 ~ day_train_all.7z.015) + ├── day_val_451 #白天验证图像文件夹 (day_val_451.7z) + ├── day_val_451_gt #白天验证深度真值文件夹 (day_val_451_gt.7z) + ├── night_val_411 #夜晚验证图像文件夹 (night_val_411.7z) + └── night_val_411_gt #夜晚验证深度真值文件夹 (night_val_411_gt.7z) +``` + +其中用于训练和验证的序列如下: + +``` +splits/oxford_day/train_files.txt # 白天训练序列 +splits/oxford_night/train_files.txt # 夜晚训练序列 +splits/oxford_day_451/val_files.txt # 白天验证序列 +splits/oxford_night_411/val_files.txt # 夜晚验证序列 +``` +训练所用路径文本的下载地址: +```shell +https://videotag.bj.bcebos.com/Data/ADDS/train_files.txt +https://videotag.bj.bcebos.com/Data/ADDS/val_day_files.txt +https://videotag.bj.bcebos.com/Data/ADDS/val_night_files.txt +``` + +#### 4. 白天-伪夜晚图像对准备 + +为了用我们的框架提取出白天和夜晚图像的共有信息,我们用[CycleGAN](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix)生成白天-伪夜晚图像对,其中伪夜晚为CycleGAN生成的与白天对应的夜晚图像, 所有图像都缩放为192x640, 夜晚图像用直方图均衡化增强, 训练75个epoch, 最终得到Oxford-RobotCar-for-ADDS. 生成的白天-伪夜晚图像对数据格式如下,可直接用于ADDS-DepthNet的训练和验证: +``` +data +└── oxford + ├── splits + ├── train_files.txt + ├── val_day_files.txt + └── val_night_files.txt + └── oxford_processing_forADDS + ├── day_train_all/ #白天训练图像文件夹 (解压自day_train_all.7z.001 ~ day_train_all.7z.002) + ├── night_train_all/ #夜晚训练图像文件夹 (解压自night_train_all.7z.001 ~ day_train_all.7z.002) + ├── day_val_451/ #白天验证图像文件夹 (解压自day_val_451.7z) + ├── day_val_451_gt/ #白天验证深度真值文件夹 (解压自day_val_451_gt.7z) + ├── night_val_411/ #夜晚验证图像文件夹 (解压自night_val_411.7z) + └── night_val_411_gt/ #夜晚验证深度真值文件夹 (解压自night_val_411_gt.7z) +``` + +其中用于训练和验证的序列与前述保持一致. diff --git a/docs/zh-CN/dataset/SegmentationDataset.md b/docs/zh-CN/dataset/SegmentationDataset.md new file mode 100644 index 0000000000000000000000000000000000000000..c43b5910f302bedf0e7d8e772feafa8bee8d5906 --- /dev/null +++ b/docs/zh-CN/dataset/SegmentationDataset.md @@ -0,0 +1,35 @@ +简体中文 | [English](../../en/dataset/SegmentationDataset.md) + +# 视频动作分割模型数据使用说明 + +视频动作分割模型使用breakfast、50salads和gtea数据集,使用方法为使用预训练模型提取的特征,可以从MS-TCN官方代码库中获取。[feat](https://zenodo.org/record/3625992#.Xiv9jGhKhPY) + +- 数据集文件树形式 +```txt +─── gtea + ├── features + │ ├── S1_Cheese_C1.npy + │ ├── S1_Coffee_C1.npy + │ ├── S1_CofHoney_C1.npy + │ └── ... + ├── groundTruth + │ ├── S1_Cheese_C1.txt + │ ├── S1_Coffee_C1.txt + │ ├── S1_CofHoney_C1.txt + │ └── ... + ├── splits + │ ├── test.split1.bundle + │ ├── test.split2.bundle + │ ├── test.split3.bundle + │ └── ... + └── mapping.txt +``` + +- 数据集存放文件树形式 +```txt +─── data + ├── 50salads + ├── breakfast + ├── gtea + └── ... +``` diff --git a/docs/zh-CN/dataset/fsd.md b/docs/zh-CN/dataset/fsd.md new file mode 100644 index 0000000000000000000000000000000000000000..670cb694f3dafefe0dc4b16705cd1f817e95cfd9 --- /dev/null +++ b/docs/zh-CN/dataset/fsd.md @@ -0,0 +1,56 @@ +[English](../../en/dataset/fsd.md) | 简体中文 + +# 基于飞桨实现花样滑冰选手骨骼点动作识别大赛数据准备 + +- [数据集介绍](#数据集介绍) +- [数据下载](#数据下载) + +--- + + +## 数据集介绍 + +基于飞桨实现花样滑冰选手骨骼点动作识别大赛数据集旨在通过花样滑冰研究人体的运动。在花样滑冰运动中,人体姿态和运动轨迹相较于其他运动呈现复杂性强、类别多的特点,有助于细粒度图深度学习新模型、新任务的研究。 + + +在FSD-10 中,所有的视频素材从2017 到2018 年的花样滑冰锦标赛中采集。源视频素材中视频的帧率被统一标准化至每秒30 帧,并且图像大小是1080 * 720 来保证数据集的相对一致性。之后我们通过2D姿态估计算法Open Pose对视频进行逐帧骨骼点提取,最后以.npy格式保存数据集。 + +训练数据集与测试数据集的目录结构如下所示: + +```txt +train_data.npy # 2922 +train_label.npy # 2922 +test_A_data.npy # 628 +test_B_data.npy # 634 +``` + +其中train_label.npy通过np.load()读取后会得到一个一维张量,每一个元素为一个值在0-29之间的整形变量代表动作的标签;data.npy文件通过np.load()读取后,会得到一个形状为N×C×T×V×M的五维张量,每个维度的具体含义如下: + +| 维度符号 | 维度值大小 | 维度含义 | 补充说明 | +| :---- | :----: | :----: | :---- | +| N | 样本数 | 代表N个样本 | 无 | +| C | 3 | 分别代表每个关节点的x, y坐标和置信度 | 每个x,y均被放缩至-1到1之间 | +| T | 1500 | 代表动作的持续时间长度,共有1500帧 | 有的动作的实际长度可能不足1500,例如可能只有500的有效帧数,我们在其后重复补充0直到1500帧,来保证T维度的统一性 | +| V | 25 | 代表25个关节点 | 具体关节点的含义可看下方的骨架示例图 | +| M | 1 | 代表1个运动员个数 | 无 | + +骨架示例图: + + +
    +
    +
    + + + +## 数据下载 + +在[2021 CCF BDCI 基于飞桨实现花样滑冰选手骨骼点动作识别比赛](https://aistudio.baidu.com/aistudio/competition/detail/115/0/introduction)主页报名后即可获取下载链接 + +| 数据集 | Data | Label | +| :---- | :----: | :----: | +| 训练集 | [train_data.npy](https://videotag.bj.bcebos.com/Data/FSD_train_data.npy) | [train_label.npy](https://videotag.bj.bcebos.com/Data/FSD_train_label.npy) | +| 测试集A | comming soon | comming soon | + + +> 由于版权原因,RGB数据暂不开放。 diff --git a/docs/zh-CN/dataset/howto100m.md b/docs/zh-CN/dataset/howto100m.md new file mode 100644 index 0000000000000000000000000000000000000000..63711a4c8b61d7482a28b8324dd7319abe887f64 --- /dev/null +++ b/docs/zh-CN/dataset/howto100m.md @@ -0,0 +1,31 @@ +# HowTo100M 数据准备 + +HowTo100M 数据相关准备,包括HowTo100M数据下载和数据下载后文件组织结构。 + +## 数据下载 + +HowTo100M 从1.2M Youtube 教学视频中切分出136M包含字幕的视频片段,涵盖23k活动类型,包括做饭、手工制作、日常护理、园艺、健身等等,数据集约10T大小。 + +因为完整数据集体积过大,这里我们只提供少量数据,供大家跑通训练前向。如需下载全量数据,请参考:[HowTo100M](https://www.di.ens.fr/willow/research/howto100m/) + +为了方便使用,我们提供的数据版本已对HowTo100M数据集中的物体特征和动作特征进行了特征提取。 + +首先,请确保在 `data/howto100m` 目录下,输入如下命令,下载数据集。 + +```bash +bash download_features.sh +``` + +下载完成后,data目录下文件组织形式如下: + +``` +├── data +| ├── howto100m +| │ ├── actbert_train_data.npy +| │ ├── caption_train.json +| | ├── caption_val.json + +``` + +## 参考论文 +- Antoine Miech, Dimitri Zhukov, Jean-Baptiste Alayrac, Makarand Tapaswi, Ivan Laptev, and Josef Sivic. Howto100m: Learning a text-video embedding by watching hundred million narrated video clips. In ICCV, 2019. diff --git a/docs/zh-CN/dataset/k400.md b/docs/zh-CN/dataset/k400.md new file mode 100644 index 0000000000000000000000000000000000000000..7eeceacb2dc590a68520d18008d025d057900ab4 --- /dev/null +++ b/docs/zh-CN/dataset/k400.md @@ -0,0 +1,77 @@ +[English](../../en/dataset/k400.md) | 简体中文 + +# Kinetics-400 数据准备 + +- [数据集介绍](#数据集介绍) +- [下载video数据](#下载video数据) +- [提取frames数据](#提取frames数据) + +--- + + +## 数据集介绍 + +Kinetics-400是视频领域benchmark常用数据集,详细介绍可以参考其官方网站[Kinetics](https://deepmind.com/research/open-source/kinetics)。下载方式可参考官方地址[ActivityNet](https://github.com/activitynet/ActivityNet/tree/master/Crawler/Kinetics),使用其提供的下载脚本下载数据集。 + +## 下载video数据 + +考虑到K400数据集下载困难的问题,我们提供了两种下载方式: (1) 百度网盘下载 (2) 脚本下载 + +### 百度网盘下载 + +网盘链接:https://pan.baidu.com/s/1S_CGBjWOUAuxL_cCX5kMPg +提取码:ppvi + +### 脚本下载 + +- 下载训练集链接列表文件[train_link.list](https://ai-rank.bj.bcebos.com/Kinetics400/train_link.list)和验证集链接列表文件[val_link.list](https://ai-rank.bj.bcebos.com/Kinetics400/val_link.list)。 + +编写下载脚本`download.sh`如下: +```bash +file=$1 + +while read line +do + wget "$line" +done <$file +``` + +下载训练集命令: +```bash +bash download.sh train_link.list +``` + +下载验证集命令: +```bash +bash download.sh val_link.list +``` + +--- + +|类别 | 数据条数 | list文件 | +| :------: | :----------: | :----: | +|训练集 | 234619 | [train.list](https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train.list)| +|验证集 | 19761 | [val.list](https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val.list)| + +- 下载后自行解压,并将数据路径添加到相应的list文件中。 + +- 由于部分视频原始链接失效,数据有部分缺失,全部文件大概需要135G左右的存储空间,PaddleVideo使用的也是这份数据。 + +> 此份数据仅限于学术研究,若对您有帮助,欢迎给[项目](https://github.com/PaddlePaddle/PaddleVideo)star~ + + +## 提取frames数据 +为了加速网络的训练过程,我们首先对视频文件(K400视频文件为mp4格式)提取帧 (frames)。相对于直接通过视频文件进行网络训练的方式,frames的方式能够极大加快网络训练的速度。 + +输入如下命令,即可提取K400视频文件的frames + +```python +python extract_rawframes.py ./videos/ ./rawframes/ --level 2 --ext mp4 +``` + +视频文件frames提取完成后,会存储在指定的`./rawframes`路径下,大小约为2T左右。 + +|类别 | 数据条数 | list文件 | +| :------: | :----------: | :----: | +|训练集 | 234619 | [train_frames.list](https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train_frames.list)| +|验证集 | 19761 | [val_frames.list](https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val_frames.list)| diff --git a/docs/zh-CN/dataset/msrvtt.md b/docs/zh-CN/dataset/msrvtt.md new file mode 100644 index 0000000000000000000000000000000000000000..b2cfdedc305cf2fe4006ad54ca9b13e7d548b123 --- /dev/null +++ b/docs/zh-CN/dataset/msrvtt.md @@ -0,0 +1,72 @@ +[English](../../en/dataset/msrvtt.md) | 简体中文 + +# MSR-VTT 数据准备 + +- [数据集介绍](#数据集介绍) +- [T2VLAD模型数据准备](#T2VLAD模型数据准备) +- [ActBERT模型数据准备](#T2VLAD模型数据准备) +- [参考文献](#参考文献) + +## 数据集介绍 + +MSR-VTT(Microsoft Research Video to Text) 是一个包含视频及字幕的大规模数据集,由来自20个类别的10,000个视频片段组成,每个视频片段由20个英文句子注释。我们使用9000个视频片段用于训练,1000个用于测试。更多详细信息可以参考网站:[MSRVTT](https://www.microsoft.com/en-us/research/publication/msr-vtt-a-large-video-description-dataset-for-bridging-video-and-language/) + +## T2VLAD模型数据准备 +[T2VLAD模型文档](../../../applications/T2VLAD/README.md) + +为了方便使用,我们提供的数据版本已对MSR-VTT数据集中对视频进行了特征提取。 + +首先,请确保在 `applications/T2VLAD/data` 目录下,输入如下命令,下载数据集。 + +```bash +bash download_features.sh +``` + +下载完成后,data目录下文件组织形式如下: + +``` +├── data +| ├── MSRVTT +| │ ├── raw-captions.pkl +| │ ├── train_list_jsfusion.txt +| │ ├── val_list_jsfusion.txt +| │ ├── aggregated_text_feats +| | | ├── w2v_MSRVTT_openAIGPT.pickle +| | ├── mmt_feats +| │ │ ├── features.audio.pkl +| │ │ ├── features.face_agg.pkl +| │ │ ├── features.flos_agg.pkl +| │ │ ├── features.ocr.pkl +| │ │ ├── features.rgb_agg.pkl +| │ │ ├── features.s3d.pkl +| │ │ ├── features.scene.pkl +| │ │ ├── features.speech.pkl + +``` + +## ActBERT模型数据准备 +[ActBERT模型文档](../model_zoo/multimodal/actbert.md) + +下载数据特征: +``` +wget https://videotag.bj.bcebos.com/Data/ActBERT/msrvtt_test.lmdb.tar +wget https://videotag.bj.bcebos.com/Data/ActBERT/MSRVTT_JSFUSION_test.csv +``` + +将下载得到的`msrvtt_test.lmdb.tar`解压: +``` +tar -zxvf msrvtt_test.lmdb.tar +``` + +最终得到的文件组织形式如下: +``` +├── data +| ├── MSR-VTT +| │ ├── MSRVTT_JSFUSION_test.csv +| │ ├── msrvtt_test.lmdb +| │ ├── data.mdb +| │ ├── lock.mdb +``` + +## 参考论文 +- Valentin Gabeur, Chen Sun, Karteek Alahari, and Cordelia Schmid. Multi-modal transformer for video retrieval. In ECCV, 2020. diff --git a/docs/zh-CN/dataset/ntu-rgbd.md b/docs/zh-CN/dataset/ntu-rgbd.md new file mode 100644 index 0000000000000000000000000000000000000000..c0910fa48ca07f7b4cd5f05e92932b1276e60356 --- /dev/null +++ b/docs/zh-CN/dataset/ntu-rgbd.md @@ -0,0 +1,158 @@ +[English](../../en/dataset/ntu-rgbd.md) | 简体中文 + +# NTU-RGB+D 数据准备 + +- [数据集介绍](#数据集介绍) +- [ST-GCN数据集准备](#ST-GCN数据集准备) +- [CTR-GCN数据集准备](#CTR-GCN数据集准备) + +--- + + +## 数据集介绍 + +NTU-RGB+D是基于骨骼的行为识别数据集,包含60个种类的动作,56880个样本,详细介绍可以参考其官方网站[NTU-RGB+D](https://rose1.ntu.edu.sg/dataset/actionRecognition/)。该数据集在划分训练集和测试集时采用了两种不同的划分标准。Cross-Subject按照人物ID划分,训练集40320个样本,测试集16560个样本。Cross-View安装相机划分,相机2和3采集的样本为训练集,包含37930个样本,相机1采集的样本为测试集,包含18960个样本。 + + +## ST-GCN数据集准备 + +以下是ST-GCN模型的数据集准备流程介绍。 + +### 数据集下载 + +我们提供处理好的数据集下载地址[NTU-RGB-D.tar](https://videotag.bj.bcebos.com/Data/NTU-RGB-D.tar)(~3.1G),下载后通过命令```tar -zxvf NTU-RGB-D.tar ```进行解压,得到的数据目录如下: + +```txt +─── NTU-RGB-D + ├── xsub + │ ├── train_data.npy + │ ├── train_label.pkl + │ ├── val_data.npy + │ └── val_label.pkl + └── xview + ├── train_data.npy + ├── train_label.pkl + ├── val_data.npy + └── val_label.pkl +``` + +> 数据来源于[st-gcn](https://github.com/open-mmlab/mmskeleton/blob/master/doc/SKELETON_DATA.md)。 + +## CTR-GCN数据集准备 + +以下是CTR-GCN模型的数据集准备流程介绍。 + +### 数据集下载 + +在`data\ntu-rgb-d`目录有下载其官方网站[NTU-RGB+D](https://rose1.ntu.edu.sg/dataset/actionRecognition/)提供的数据集的脚本`download_dataset.sh` + +```bash +sh data/ntu-rgb-d/download_dataset.sh +``` + +运行脚本后会得到如下的数据目录: +```txt +─── ntu-rgb-d + ├── download_dataset.sh + ├── nturgb+d_skeletons + │   ├── S001C001P001R001A001.skeleton + │   ├── S001C001P001R001A002.skeleton + │   ├── S001C001P001R001A003.skeleton + │   ├── S001C001P001R001A004.skeleton + │   ├── S001C001P001R001A005.skeleton + │   ├── S001C001P001R001A006.skeleton + │   ├── S001C001P001R001A007.skeleton + │ ├── .... + │   └── S017C003P020R002A060.skeleton + ├── get_raw_denoised_data.py + ├── get_raw_skes_data.py + ├── seq_transformation.py + └── statistics +    ├── camera.txt +    ├── label.txt +    ├── performer.txt +    ├── replication.txt +    ├── setup.txt +    └── skes_available_name.txt + +``` + +### 数据集处理 + +运行如下脚本,将数据处理成CTR-GCN所需的格式。 + +> 注:若自定义数据集,提前准备好`data/ntu-rgb-d/statistics/skes_available_name.txt`文件,该文件是待处理的骨骼点数据文件名清单。 + +```bash +cd ./data/ntu-rgb-d +# Get skeleton of each performer +python get_raw_skes_data.py +# Remove the bad skeleton +python get_raw_denoised_data.py +# Transform the skeleton to the center of the first frame +python seq_transformation.py +``` + +最终数据集处理后得到如下文件树形式 + +```txt +─── ntu-rgb-d + ├── download_dataset.sh + ├── nturgb+d_skeletons + │   ├── S001C001P001R001A001.skeleton + │   ├── S001C001P001R001A002.skeleton + │   ├── S001C001P001R001A003.skeleton + │   ├── S001C001P001R001A004.skeleton + │   ├── S001C001P001R001A005.skeleton + │   ├── S001C001P001R001A006.skeleton + │   ├── S001C001P001R001A007.skeleton + │ ├── .... + │   └── S017C003P020R002A060.skeleton + ├── denoised_data + │   ├── actors_info + │   │   ├── S001C001P001R001A024.txt + │   │   ├── S001C001P001R001A025.txt + │   │   ├── S001C001P001R001A026.txt + │   │   ├── .... + │   │   ├── S017C003P020R002A059.txt + │   │   └── S017C003P020R002A060.txt + │   ├── denoised_failed_1.log + │   ├── denoised_failed_2.log + │   ├── frames_cnt.txt + │   ├── missing_skes_1.log + │   ├── missing_skes_2.log + │   ├── missing_skes.log + │   ├── noise_length.log + │   ├── noise_motion.log + │   ├── noise_spread.log + │   ├── raw_denoised_colors.pkl + │   ├── raw_denoised_joints.pkl + │   └── rgb+ske + ├── raw_data + │   ├── frames_cnt.txt + │   ├── frames_drop.log + │   ├── frames_drop_skes.pkl + │   └── raw_skes_data.pkl + ├── get_raw_denoised_data.py + ├── get_raw_skes_data.py + ├── seq_transformation.py + ├── statistics + │   ├── camera.txt + │   ├── label.txt + │   ├── performer.txt + │   ├── replication.txt + │   ├── setup.txt + │   └── skes_available_name.txt + ├── xview + │ ├── train_data.npy + │ ├── train_label.pkl + │ ├── val_data.npy + │ └── val_label.pkl + └── xsub + ├── train_data.npy + ├── train_label.pkl + ├── val_data.npy + └── val_label.pkl +``` + +> 注:文件夹`denoised_data`、`raw_data`和`nturgb+d_skeletons`都为处理处理的临时文件,可在提取出`xview`和`xsub`后删除。 diff --git a/docs/zh-CN/dataset/ucf101.md b/docs/zh-CN/dataset/ucf101.md new file mode 100644 index 0000000000000000000000000000000000000000..83b422d80d091ea4913c01e6ab3baab5b31ba932 --- /dev/null +++ b/docs/zh-CN/dataset/ucf101.md @@ -0,0 +1,93 @@ +# UCF101数据准备 +UCF101数据的相关准备。主要包括UCF101的video文件下载,video文件提取frames,以及生成文件的路径list。 + +--- +## 1. 数据下载 +UCF101数据的详细信息可以参考网站[UCF101](https://www.crcv.ucf.edu/data/UCF101.php)。 为了方便使用,PaddleVideo提供了UCF101数据的annotations文件和videos文件的下载脚本。 + +### 下载annotations文件 +首先,请确保在[data/ucf101/ 目录](../../../data/ucf101)下,输入如下UCF101数据集的标注文件的命令。 +```shell +bash download_annotations.sh +``` + +### 下载UCF101的视频文件 +同样需要确保在[data/ucf101/ 目录](../../../data/ucf101)下,输入下述命令下载视频文件 + +```shell +bash download_videos.sh +``` +- 运行该命令需要安装unrar解压工具,可使用pip方式安装。 + +- 下载完成后视频文件会存储在[data/ucf101/videos/ 文件夹](../../../data/ucf101/videos)下,视频文件大小为6.8G。 + +--- +## 2. 提取视频文件的frames +为了加速网络的训练过程,我们首先对视频文件(ucf101视频文件为avi格式)提取帧 (frames)。相对于直接通过视频文件进行网络训练的方式,frames的方式能够加快网络训练的速度。 + +直接输入如下命令,即可提取ucf101视频文件的frames + +``` python +python extract_rawframes.py ./videos/ ./rawframes/ --level 2 --ext avi +``` + +视频文件frames提取完成后,会存储在`./rawframes`文件夹下,大小为56G。 + +--- +## 3. 生成frames文件和视频文件的路径list +生成视频文件的路径list,输入如下命令 + +```python +python build_ucf101_file_list.py videos/ --level 2 --format videos --out_list_path ./ +``` +生成frames文件的路径list,输入如下命令: +```python +python build_ucf101_file_list.py rawframes/ --level 2 --format rawframes --out_list_path ./ +``` + +**参数说明** + +`videos/` 或者 `rawframes/` : 表示视频或者frames文件的存储路径 + +`--level 2` : 表示文件的存储结构 + +`--format`: 表示是针对视频还是frames生成路径list + +`--out_list_path `: 表示生成的路径list文件存储位置 + + +# 以上步骤完成后,文件组织形式如下所示 + +``` +├── data +| ├── dataset +| │ ├── ucf101 +| │ │ ├── ucf101_{train,val}_split_{1,2,3}_rawframes.txt +| │ │ ├── ucf101_{train,val}_split_{1,2,3}_videos.txt +| │ │ ├── annotations +| │ │ ├── videos +| │ │ │ ├── ApplyEyeMakeup +| │ │ │ │ ├── v_ApplyEyeMakeup_g01_c01.avi +| │ │ │ │ └── ... +| │ │ │ ├── YoYo +| │ │ │ │ ├── v_YoYo_g25_c05.avi +| │ │ │ │ └── ... +| │ │ │ └── ... +| │ │ ├── rawframes +| │ │ │ ├── ApplyEyeMakeup +| │ │ │ │ ├── v_ApplyEyeMakeup_g01_c01 +| │ │ │ │ │ ├── img_00001.jpg +| │ │ │ │ │ ├── img_00002.jpg +| │ │ │ │ │ ├── ... +| │ │ │ │ │ ├── flow_x_00001.jpg +| │ │ │ │ │ ├── flow_x_00002.jpg +| │ │ │ │ │ ├── ... +| │ │ │ │ │ ├── flow_y_00001.jpg +| │ │ │ │ │ ├── flow_y_00002.jpg +| │ │ │ ├── ... +| │ │ │ ├── YoYo +| │ │ │ │ ├── v_YoYo_g01_c01 +| │ │ │ │ ├── ... +| │ │ │ │ ├── v_YoYo_g25_c05 + +``` diff --git a/docs/zh-CN/dataset/youtube8m.md b/docs/zh-CN/dataset/youtube8m.md new file mode 100644 index 0000000000000000000000000000000000000000..e0a62680a8caaa63c69810164f8703fdff2b3523 --- /dev/null +++ b/docs/zh-CN/dataset/youtube8m.md @@ -0,0 +1,59 @@ +[English](../../en/dataset/youtube8m.md) | 简体中文 + +# YouTube-8M数据准备 + +- [数据集简介](#数据集简介) +- [数据集下载](#数据集下载) +- [数据格式转化](#数据格式转化) + + +## 数据集简介 + +YouTube-8M 是一个大规模视频分类数据集,包含800多万个视频url,标签体系涵盖3800多种知识图谱实体,1个视频对应多个标签(平均3-4个),使用机器进行标注。 + +**每个视频的长度在120s到500s之间 +由于视频数据量太大,因此预先使用图像分类模型提取了frame-level的特征,并使用PCA对特征进行了降维处理得到多帧1024维的特征,类似地用音频模型处理得到多帧128维的音频特征。** +> 这里用到的是YouTube-8M 2018年更新之后的数据集(May 2018 version (current): 6.1M videos, 3862 classes, 3.0 labels/video, 2.6B audio-visual features)。 + + +## 数据集下载 + +1. 新建存放特征的目录(以PaddleVideo目录下为例) + ```bash + cd data/yt8m + mkdir frame + cd frame + ``` +2. 下载训练、验证集到frame文件夹中 + ```bash + curl data.yt8m.org/download.py | partition=2/frame/train mirror=asia python + curl data.yt8m.org/download.py | partition=2/frame/validate mirror=asia python + ``` + 下载过程如图所示 + ![image](https://user-images.githubusercontent.com/23737287/140709613-1e2d6ec0-a82e-474d-b220-7803065b0153.png) + + 数据下载完成后,将会得到3844个训练数据文件和3844个验证数据文件(TFRecord格式) + + +## 数据格式转化 +1. 安装tensorflow-gpu用于读入tfrecord数据 + ```bash + python3.7 -m pip install tensorflow-gpu==1.14.0 + ``` +3. 将下载的TFRecord文件转化为pickle文件以便PaddlePaddle使用 + ```bash + cd .. # 从frame目录回到yt8m目录 + python3.7 tf2pkl.py ./frame ./pkl_frame/ # 将frame文件夹下的train*.tfrecord和validate*.tfrecord转化为pkl格式 + ``` +2. 生成单个pkl文件路径集合,并根据此文件将pkl拆分为多个小pkl文件,并生成最终需要的拆分pkl文件路径 + ```bash + ls pkl_frame/train*.pkl > train.list # 将train*.pkl的路径写入train.list + ls pkl_frame/validate*.pkl > val.list # 将validate*.pkl的路径写入val.list + + python3.7 split_yt8m.py train.list # 拆分每个train*.pkl变成多个train*_split*.pkl + python3.7 split_yt8m.py val.list # 拆分每个validate*.pkl变成多个validate*_split*.pkl + + ls pkl_frame/train*_split*.pkl > train.list # 将train*_split*.pkl的路径重新写入train.list + ls pkl_frame/validate*_split*.pkl > val.list # 将validate*_split*.pkl的路径重新写入val.list + ``` + diff --git a/docs/zh-CN/install.md b/docs/zh-CN/install.md new file mode 100644 index 0000000000000000000000000000000000000000..2f5bd7e187683f78bf3a51b0f13fe9e6114e6abc --- /dev/null +++ b/docs/zh-CN/install.md @@ -0,0 +1,87 @@ +简体中文 | [English](../en/install.md) + +# 安装说明 + +--- + +- [简介](#简介) +- [安装PaddlePaddle](#安装PaddlePaddle) +- [安装PaddleVideo](#安装PaddleVideo) + +## 简介 + +使用PaddleVideo之前,请先安装PaddlePaddle及相关依赖项。 + + +## 安装PaddlePaddle + +运行PaddleVideo需要`PaddlePaddle 2.0`或更高版本。请参照[安装文档](http://www.paddlepaddle.org.cn/install/quick)中的说明进行操作。 +PaddleVideo只支持python3.7及以上的运行环境,依赖项请安装python3.7及以上的安装包 + +如果已经安装好了cuda、cudnn、nccl或者安装好了nvidia-docker运行环境,可以pip3安装最新GPU版本PaddlePaddle + +```bash +pip3 install paddlepaddle-gpu --upgrade +``` + +也可以从源码编译安装PaddlePaddle,请参照[安装文档](http://www.paddlepaddle.org.cn/install/quick)中的说明进行操作。 + +使用以下命令可以验证PaddlePaddle是否安装成功。 + +```python3 +import paddle +paddle.utils.run_check() +``` + +查看PaddlePaddle版本的命令如下: + +```bash +python3 -c "import paddle; print(paddle.__version__)" +``` + +注意: +- 从源码编译的PaddlePaddle版本号为0.0.0,请确保使用了PaddlePaddle 2.0及之后的源码编译。 +- PaddleVideo基于PaddlePaddle高性能的分布式训练能力,若您从源码编译,请确保打开编译选项,**WITH_DISTRIBUTE=ON**。具体编译选项参考[编译选项表](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/install/Tables.html#id3)。 +- 在docker中运行时,为保证docker容器有足够的共享内存用于Paddle的数据读取加速,在创建docker容器时,请设置参数`--shm_size=32g`,条件允许的话可以设置为更大的值。 + +**运行环境需求:** + +- Python3.7 or later version (当前只支持Linux系统) +- CUDA >= 10.1 +- cuDNN >= 7.6.4 +- nccl >= 2.1.2 + + +## 安装PaddleVideo + +**克隆PaddleVideo模型库:** + +``` +cd path_to_clone_PaddleVideo +git clone https://github.com/PaddlePaddle/PaddleVideo.git +cd PaddleVideo +``` + +**安装Python依赖库:** + +Python依赖库在[requirements.txt](https://github.com/PaddlePaddle/PaddleVideo/blob/master/requirements.txt)中给出,可通过如下命令安装: + +``` +python3.7 -m pip install --upgrade pip +pip3.7 install --upgrade -r requirements.txt +``` + +**从python安装包安装PaddleVideo:** + +使用pypi安装 + +```bash +pip install paddlevideo==0.0.1 +``` + +安装完成后,可以使用命令行方式启动程序 +```bash +ppvideo --model_name='ppTSM' --video_file='data/example.avi' +``` + +--- diff --git a/docs/zh-CN/model_zoo/README.md b/docs/zh-CN/model_zoo/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3d5788beb0e878273cf22433c985dc03497bef91 --- /dev/null +++ b/docs/zh-CN/model_zoo/README.md @@ -0,0 +1,40 @@ +简体中文 | [English](../../en/model_zoo/README.md) + + +# 概要 +PaddleVideo包含视频分类和动作定位方向的多个主流领先模型,其中TSN, TSM和SlowFast是End-to-End的视频分类模型,Attention LSTM是比较流行的视频特征序列模型,BMN是视频动作定位模型,TransNetV2是视频切分模型。TSN是基于2D-CNN的经典解决方案,TSM是基于时序移位的简单高效视频时空建模方法,SlowFast是FAIR在ICCV2019提出的3D视频分类模型,特征序列模型Attention LSTM速度快精度高。BMN模型是百度自研模型,为2019年ActivityNet夺冠方案。基于百度飞桨产业实践,我们自研并开源了ppTSM,该模型基于TSM进行优化,在保持模型参数量和计算量不增加的前提下,精度得到大幅提升。同时,我们的通用优化策略可以广泛适用于各种视频模型,未来我们将进行更多的模型优化工作,比如TPN、SlowFast、X3D等,敬请期待。 + + +## 模型概览 + +| 领域 | 模型 | 配置 | 测试集 | 精度指标 | 精度% | 下载链接 | +| :--------------- | :--------: | :------------: | :------------: | :------------: | :------------: | :------------: | +| 行为识别 | [**PP-TSM**](./recognition/pp-tsm.md) | [pptsm.yaml](../../../configs/recognition/pptsm/pptsm_k400_frames_dense.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 76.16 | [PPTSM.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_dense_distill.pdparams) | +| 行为识别| [**PP-TSN**](./recognition/pp-tsn.md) | [pptsn.yaml](../../../configs/recognition/pptsn/pptsn_k400_frames.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 75.06 | [PPTSN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSN_k400_8.pdparams) | +| 行为识别 | [**PP-TimeSformer**](./recognition/pp-timesformer.md) | [pptimesformer.yaml](../../../configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 79.44 | [ppTimeSformer_k400_16f_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTimeSformer_k400_16f_distill.pdparams) | +| 行为识别 | [AGCN](./recognition/agcn.md) | [agcn.yaml](../../../configs/recognition/agcn/agcn_fsd.yaml) | [FSD](../dataset/fsd.md) | Top-1 | 62.29 | [AGCN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AGCN_fsd.pdparams) | +| 行为识别 | [ST-GCN](./recognition/stgcn.md) | [stgcn.yaml](../../../configs/recognition/stgcn/stgcn_fsd.yaml) | [FSD](../dataset/fsd.md) | Top-1 | 59.07 | [STGCN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/STGCN_fsd.pdparams) | +| 行为识别 | [VideoSwin](./recognition/videoswin.md) | [videoswin.yaml](../../../configs/recognition/videoswin/videoswin_k400_videos.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 82.40 | [VideoSwin.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/VideoSwin_k400.pdparams) | +| 行为识别 | [TimeSformer](./recognition/timesformer.md) | [timesformer.yaml](../../../configs/recognition/timesformer/timesformer_k400_videos.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 77.29 | [TimeSformer.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TimeSformer_k400.pdparams) | +| 行为识别 | [SlowFast](./recognition/slowfast.md) | [slowfast_multigrid.yaml](../../../configs/recognition/slowfast/slowfast_multigrid.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 75.84 | [SlowFast.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast_8*8.pdparams) | +| 行为识别 | [TSM](./recognition/tsm.md) | [tsm.yaml](../../../configs/recognition/tsm/tsm_k400_frames.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 70.86 | [TSM.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams) | +| 行为识别 | [TSN](./recognition/tsn.md) | [tsn.yaml](../../../configs/recognition/tsn/tsn_k400_frames.yaml) | [Kinetics-400](../dataset/k400.md) | Top-1 | 69.81 | [TSN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TSN_k400.pdparams) | +| 行为识别 | [AttentionLSTM](./recognition/attention_lstm.md) | [attention_lstm.yaml](../../../configs/recognition/attention_lstm/attention_lstm.yaml) | [Youtube-8M](../dataset/youtube8m.md) | Hit@1 | 89.0 | [AttentionLstm.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/AttentionLstm/AttentionLstm.pdparams) | +| 视频动作定位| [BMN](./localization/bmn.md) | [bmn.yaml](../../../configs/localization/bmn.yaml) | [ActivityNet](../dataset/ActivityNet.md) | AUC | 67.23 | [BMN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/BMN/BMN.pdparams) | +| 视频切分 | [TransNetV2](./partition/transnetv2.md) | [transnetv2.yaml](../../../configs/partitioners/transnetv2/transnetv2.yaml) | ClipShots | F1 scores | 76.1 | | +| 深度估计 | [ADDS](./estimation/adds.md) | [adds.yaml](../../../configs/estimation/adds/adds.yaml) | Oxford_RobotCar | Abs Rel | 0.209 | [ADDS_car.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ADDS_car.pdparams) | + + +# 参考文献 + +- [Attention Clusters: Purely Attention Based Local Feature Integration for Video Classification](https://arxiv.org/abs/1711.09550), Xiang Long, Chuang Gan, Gerard de Melo, Jiajun Wu, Xiao Liu, Shilei Wen +- [BMN: Boundary-Matching Network for Temporal Action Proposal Generation](https://arxiv.org/abs/1907.09702), Tianwei Lin, Xiao Liu, Xin Li, Errui Ding, Shilei Wen. +- [SlowFast Networks for Video Recognition](https://arxiv.org/abs/1812.03982), Feichtenhofer C, Fan H, Malik J, et al. +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859), Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoou Tang, Luc Van Gool +- [Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/abs/1811.08383v1), Ji Lin, Chuang Gan, Song Han +- [Is Space-Time Attention All You Need for Video Understanding?](https://arxiv.org/pdf/2102.05095.pdf) Gedas Bertasius, Heng Wang, Lorenzo Torresani +- [Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1801.07455), Sijie Yan, Yuanjun Xiong, Dahua Lin +- [Two-Stream Adaptive Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1805.07694), Lei Shi, Yifan Zhang, Jian Cheng, Hanqing Lu +- [Skeleton-Based Action Recognition with Multi-Stream Adaptive Graph Convolutional Networks](https://arxiv.org/abs/1912.06971), Lei Shi, Yifan Zhang, Jian Cheng, Hanqing Lu +- [TransNet V2: An effective deep network architecture for fast shot transition detection](https://arxiv.org/abs/2008.04838), Tomáš Souček, Jakub Lokoč +- [Self-supervised Monocular Depth Estimation for All Day Images using Domain Separation](https://arxiv.org/abs/2108.07628), Lina Liu, Xibin Song, Mengmeng Wang diff --git a/docs/zh-CN/model_zoo/detection/SlowFast_FasterRCNN.md b/docs/zh-CN/model_zoo/detection/SlowFast_FasterRCNN.md new file mode 100644 index 0000000000000000000000000000000000000000..d0a64e948060f256d63586a83aff0ffdcc722144 --- /dev/null +++ b/docs/zh-CN/model_zoo/detection/SlowFast_FasterRCNN.md @@ -0,0 +1,140 @@ +简体中文 | [English](../../../en/model_zoo/detection/SlowFast_FasterRCNN_en.md) + +# SlowFast_FasterRCNN + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install moviepy +python -m pip install et_xmlfile +python -m pip install paddledet +``` + +## 模型简介 + +[SlowFast](https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/model_zoo/recognition/slowfast.md)模型是视频领域的高精度模型之一,对于动作识别任务,还需要检测出当前画面人物,因此SlowFast_FasterRCNN模型以人的检测结果和视频数据为输入,通过SlowFast模型提取时空特征,然后利用FasterRCNN的head得到画面中每个人的动作和位置。 + +我们提供了详尽理论及代码讲解,并可使用免费在线GPU算力资源,一键运行的AI Studio Notebook项目,使用链接:[基于SlowFast+FasterRCNN的动作识别](https://aistudio.baidu.com/aistudio/projectdetail/3267637?contributionType=1) + +详细内容请参考论文[SlowFast Networks for Video Recognition](https://arxiv.org/pdf/1812.03982.pdf)中AVA Action Detection相关内容。 + +## 数据准备 + +本项目利用[AVA数据集](https://research.google.com/ava/download.html)进行动作检测。AVA v2.2数据集包括430个视频,其中235个用于训练,64个用于验证,131个用于测试。对每个视频中15分钟的帧进行了标注,每秒标注一帧。标注文件格式为CSV。 + +相关处理脚本在`data/ava/script`目录下。 + +### 1 下载视频 +``` +bash download_videos.sh +``` + +### 2 下载标注 +``` +bash download_annotations.sh +``` + +### 3 下载检测结果 + +``` +bash fetch_ava_proposals.sh +``` + +### 4 视频切割 +把下载的视频中第15分钟起后面的15分钟的片段切割出来: + +``` +bash cut_videos.sh +``` + +### 5 提取视频帧 +``` +bash extract_rgb_frames.sh +``` + +此处以AVA v2.1版本为例,进行关键文件介绍: +* ava_videos_15min_frames文件夹中存放以FPS为帧率抽取的视频帧; +* ava_train_v2.1.csv文件存放训练数据标注; +* ava_train_excluded_timestamps_v2.1.csv文件中存放废弃的时间戳数据; +* ava_dense_proposals_train.FAIR.recall_93.9.pkl文件中为每个关键帧中人的位置和置信度数据; +* ava_action_list_v2.1_for_activitynet_2018.pbtxt为动作类别数据。 + +## 模型训练 + +下载预训练模型: +``` +wget https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast_8*8.pdparams +``` + + +* `-c`后面的参数是配置文件的路径。 +* `-w`后面的参数是finetuning或者测试时的权重,本案例将在Kinetics 400上训练的SlowFast R50模型作为预训练权重,通过下面的表格可获取。 +* `--validate`参数表示在训练过程中进行模型评估。 + +``` +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +python -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=logdir.ava main.py --validate -w SlowFast_8*8.pdparams -c configs/detection/ava/ava.yaml +``` + +## 模型评估 + +基于训练好的模型进行评估: +``` +python main.py --test \ + -w output/AVA_SlowFast_FastRcnn/AVA_SlowFast_FastRcnn_best.pdparams \ + -c configs/detection/ava/ava.yaml +``` + +| architecture | depth | Pretrain Model | frame length x sample rate | MAP | AVA version | model | +| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |------------- | +| SlowFast | R50 | [Kinetics 400](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast_8*8.pdparams) | 8 x 8 | 23.2 | 2.1 | [`link`](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/SlowFastRCNN_AVA.pdparams) | + + +## 模型推理 + +本项目动作识别分成两个阶段,第一个阶段得到人的proposals,然后再输入到SlowFast+FasterRCNN模型中进行动作识别。 + +对于画面中人的检测,可利用[PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection)中的模型。 + +PaddleDetection安装: +``` +# 安装其他依赖 +cd PaddleDetection/ +pip install -r requirements.txt + +# 编译安装paddledet +python setup.py install +``` + +下载训练好的检测模型参数: +``` +wget https://paddledet.bj.bcebos.com/models/faster_rcnn_r50_fpn_1x_coco.pdparams +``` + +导出模型: + +``` +!python tools/export_model.py \ + -c configs/detection/ava/ava.yaml \ + -o inference_output \ + -p output/AVA_SlowFast_FastRcnn/AVA_SlowFast_FastRcnn_best.pdparams +``` + +基于导出的模型做推理: + +``` +python tools/predict.py \ + -c configs/detection/ava/ava.yaml \ + --input_file "data/-IELREHXDEMO.mp4" \ + --model_file "inference_output/AVA_SlowFast_FastRcnn.pdmodel" \ + --params_file "inference_output/AVA_SlowFast_FastRcnn.pdiparams" \ + --use_gpu=True \ + --use_tensorrt=False +``` diff --git a/docs/zh-CN/model_zoo/estimation/adds.md b/docs/zh-CN/model_zoo/estimation/adds.md new file mode 100644 index 0000000000000000000000000000000000000000..339507687e18c1a506cd565290bd7a735fd2b15c --- /dev/null +++ b/docs/zh-CN/model_zoo/estimation/adds.md @@ -0,0 +1,133 @@ +[English](../../../en/model_zoo/estimation/adds.md) | 简体中文 + +# ADDS-DepthNet模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install scikit-image +python -m pip install matplotlib +``` + +## 模型简介 + +本模型以百度机器人与自动驾驶实验室的**ICCV 2021论文 [Self-supervised Monocular Depth Estimation for All Day Images using Domain Separation](https://arxiv.org/abs/2108.07628)** 为参考, +复现了基于白天和夜晚图像的自监督单目深度估计模型,其利用了白天和夜晚的图像数据互补性质,减缓了昼夜图像较大的域偏移以及照明变化对深度估计的精度带来的影响,在具有挑战性的牛津RobotCar数据集上实现了全天图像的最先进的深度估计结果。 + + +## 数据准备 + +Oxford RobotCar dataset数据下载及准备请参考[Oxford RobotCar dataset数据准备](../../dataset/Oxford_RobotCar.md) + + +## 模型训练 + +### Oxford RobotCar dataset数据集训练 + +#### 下载并添加预训练模型 + +1. 下载图像预训练模型[resnet18.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/Resnet18_Imagenet.pdparams)作为Backbone初始化参数,或通过wget命令下载 + + ```bash + wget -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.2/Resnet18_Imagenet.pdparams + ``` + +2. 打开`PaddleVideo/configs/estimation/adds/adds.yaml`,将下载好的权重存放路径填写到下方`pretrained:`之后 + + ```yaml + MODEL: #MODEL field + framework: "DepthEstimator" #Mandatory, indicate the type of network, associate to the 'paddlevideo/modeling/framework/' . + backbone: #Mandatory, indicate the type of backbone, associate to the 'paddlevideo/modeling/backbones/' . + name: 'ADDS_DepthNet' + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- Oxford RobotCar dataset数据集使用单卡训练,训练方式的启动命令如下: + + ```bash + python3.7 main.py --validate -c configs/estimation/adds/adds.yaml --seed 20 + ``` + + +## 模型测试 + +- ADDS-DepthNet模型在训练时同步进行验证(只对白天或者夜晚的数据进行验证),您可以通过在训练日志中查找关键字`best`获取模型测试精度,日志示例如下: + + ```bash + Already save the best model (rmse)8.5531 + ``` + +- 由于模型暂时一次只能测试yaml文件中给定路径的一个白天或者夜晚的数据集,因此若要得到本文档开头处的完整测试分数,需要运行4次测试命令并分别记录下它们的指标(白天40m、白天60m、夜晚40m、夜晚60m) + +- 训练好的模型下载地址:[ADDS_car.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ADDS_car.pdparams) + +- 测试命令如下: + + ```bash + # 夜晚40m + python3.7 main.py --test -c configs/estimation/adds/adds.yaml -w "output/ADDS/ADDS_best.pdparams" -o DATASET.test.file_path="data/oxford/splits/oxford_day/val_night_files.txt" -o MODEL.head.max_gt_depth=40 + + # 夜晚60m + python3.7 main.py --test -c configs/estimation/adds/adds.yaml -w "output/ADDS/ADDS_best.pdparams" -o DATASET.test.file_path="data/oxford/splits/oxford_day/val_night_files.txt" -o MODEL.head.max_gt_depth=60 + + # 白天40m + python3.7 main.py --test -c configs/estimation/adds/adds.yaml -w "output/ADDS/ADDS_best.pdparams" -o DATASET.test.file_path="data/oxford/splits/oxford_day/val_day_files.txt" -o MODEL.head.max_gt_depth=40 + + # 白天60m + python3.7 main.py --test -c configs/estimation/adds/adds.yaml -w "output/ADDS/ADDS_best.pdparams" -o DATASET.test.file_path="data/oxford/splits/oxford_day/val_day_files.txt" -o MODEL.head.max_gt_depth=60 + ``` + + 在Oxford RobotCar dataset的validation数据集上的测试指标如下: + + | version | Max Depth | Abs Rel | Sq Rel | RMSE | RMSE log | | | | + | ----------- | --------- | ------- | ------ | ----- | -------- | ----------------- | ------------------- | ------------------- | + | ours(night) | 40 | 0.209 | 1.741 | 6.031 | 0.243 | 0.708 | 0.923 | 0.975 | + | ours(night) | 60 | 0.207 | 2.052 | 7.888 | 0.258 | 0.686 | 0.909 | 0.970 | + | ours(day) | 40 | 0.114 | 0.574 | 3.411 | 0.157 | 0.860 | 0.977 | 0.993 | + | ours(day) | 60 | 0.119 | 0.793 | 4.842 | 0.173 | 0.838 | 0.967 | 0.991 | + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/estimation/adds/adds.yaml -p data/ADDS_car.pdparams -o inference/ADDS +``` + +上述命令将生成预测所需的模型结构文件`ADDS.pdmodel`和模型权重文件`ADDS.pdiparams`以及`ADDS.pdiparams.info`文件,均存放在`inference/ADDS/`目录下 + +上述bash命令中各个参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.png \ + --config configs/estimation/adds/adds.yaml \ + --model_file inference/ADDS/ADDS.pdmodel \ + --params_file inference/ADDS/ADDS.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +推理结束会默认以伪彩的方式保存下模型估计出的深度图。 + +以下是样例图片和对应的预测深度图: + +image + +depth + + +## 参考论文 + +- [Self-supervised Monocular Depth Estimation for All Day Images using Domain Separation](https://arxiv.org/abs/2108.07628), Liu, Lina and Song, Xibin and Wang, Mengmeng and Liu, Yong and Zhang, Liangjun diff --git a/docs/zh-CN/model_zoo/localization/bmn.md b/docs/zh-CN/model_zoo/localization/bmn.md new file mode 100644 index 0000000000000000000000000000000000000000..f923e86a6fe5598153bd6101695d04a3115937f4 --- /dev/null +++ b/docs/zh-CN/model_zoo/localization/bmn.md @@ -0,0 +1,128 @@ +[English](../../../en/model_zoo/localization/bmn.md) | 简体中文 + +# BMN 视频动作定位模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +BMN模型是百度自研,2019年ActivityNet夺冠方案,为视频动作定位问题中proposal的生成提供高效的解决方案,在PaddlePaddle上首次开源。此模型引入边界匹配(Boundary-Matching, BM)机制来评估proposal的置信度,按照proposal开始边界的位置及其长度将所有可能存在的proposal组合成一个二维的BM置信度图,图中每个点的数值代表其所对应的proposal的置信度分数。网络由三个模块组成,基础模块作为主干网络处理输入的特征序列,TEM模块预测每一个时序位置属于动作开始、动作结束的概率,PEM模块生成BM置信度图。 + +AI Studio项目使用链接:[ActivityNet Challenge 2019 冠军模型:BMN](https://aistudio.baidu.com/aistudio/projectdetail/2250674?contributionType=1) + +

    +
    +BMN Overview +

    + +## 数据准备 + +BMN的训练数据采用ActivityNet1.3提供的数据集,数据下载及准备请参考[ActivityNet数据说明](../../dataset/ActivityNet.md) + +## 模型训练 + +数据准备完毕后,可以通过如下方式启动训练: + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_bmn main.py --validate -c configs/localization/bmn.yaml +``` + +- 从头开始训练,使用上述启动命令行或者脚本程序即可启动训练,不需要用到预训练模型 + +### 单卡训练 + +单卡训练请将配置文件中的`DATASET.batch_size`字段修改为16,如下: + +```yaml +DATASET: #DATASET field + batch_size: 16 #single card bacth size +``` + +单卡训练启动方式如下: + +```bash +python -B main.py --validate -c configs/localization/bmn.yaml +``` + + +## 模型测试 + +可通过如下方式进行模型测试: + +```bash +python main.py --test -c configs/localization/bmn.yaml -w output/BMN/BMN_epoch_00009.pdparams -o DATASET.test_batch_size=1 +``` + +- 目前仅支持**单卡**, `batch_size`为**1**进行模型测试, + +- 请下载[activity\_net\_1\_3\_new.json](https://paddlemodels.bj.bcebos.com/video_detection/activity_net_1_3_new.json)文件,并通过`METRIC.ground_truth_filename`字段指定该ground_truth文件,相较于原始的activity\_net.v1-3.min.json文件,我们过滤了其中一些失效的视频条目。 + +- 通过 `-w`参数指定待测试模型文件的路径,您可以下载我们训练好的模型进行测试[BMN.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/BMN/BMN.pdparams) + +- 上述程序会将运行结果保存在配置文件`METRIC.output_path`字段指定的路径,默认为`data/bmn/BMN_Test_output`文件夹下,测试结果保存在配置文件`METRIC.result_path`字段指定的文件,默认为`data/bmn/BMN_Test_results/bmn_results_validation.json`文件。 + +- 我们基于ActivityNet官方提供的测试脚本,计算AR@AN和AUC。具体计算过程请参考[anet_prop.py](https://github.com/PaddlePaddle/PaddleVideo/blob/main/paddlevideo/metrics/ActivityNet/anet_prop.py)文件。 + +- 注:评估时可能会出现loss为nan的情况。这是由于评估时用的是单个样本,可能存在没有iou>0.6的样本,所以为nan,对最终的评估结果没有影响。 + +在ActivityNet1.3数据集下评估精度如下: + +| AR@1 | AR@5 | AR@10 | AR@100 | AUC | +| :---: | :---: | :---: | :---: | :---: | +| 33.26 | 49.48 | 56.86 | 75.19 | 67.23% | + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/localization/bmn.yaml \ + -p data/BMN.pdparams \ + -o inference/BMN +``` + +上述命令将生成预测所需的模型结构文件`BMN.pdmodel`和模型权重文件`BMN.pdiparams`。 + +- 各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example_feat.list \ + --config configs/localization/bmn.yaml \ + --model_file inference/BMN/BMN.pdmodel \ + --params_file inference/BMN/BMN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +- `input_file`为文本文件,指定待推断的文件信息,包括特征文件路径`feat_path`和视频时长(单位:s)`duration_second`。 + +输出示例如下: + +``` +BMN Inference results of data/example_feat.npy : +{'score': 0.7968077063560486, 'segment': [0.0, 122.9877]} +{'score': 0.49097609519958496, 'segment': [12.423000000000002, 124.23]} +{'score': 0.21395835280418396, 'segment': [39.7536, 122.9877]} +{'score': 0.2106524258852005, 'segment': [0.0, 109.3224]} +{'score': 0.06876271963119507, 'segment': [23.6037, 114.2916]} +``` + +- 默认只打印前5个得分最高的proposal,所有的预测结果可在输出文件中查看,默认输出文件路径为`data/bmn/BMN_INFERENCE_results`。输出路径可在配置文件中的`INFERENCE.result_path`自行修改。 + +## 参考论文 + +- [BMN: Boundary-Matching Network for Temporal Action Proposal Generation](https://arxiv.org/abs/1907.09702), Tianwei Lin, Xiao Liu, Xin Li, Errui Ding, Shilei Wen. diff --git a/docs/zh-CN/model_zoo/multimodal/actbert.md b/docs/zh-CN/model_zoo/multimodal/actbert.md new file mode 100644 index 0000000000000000000000000000000000000000..3853968c25ea4e00274124e801282f244c1c9f87 --- /dev/null +++ b/docs/zh-CN/model_zoo/multimodal/actbert.md @@ -0,0 +1,103 @@ +[English](../../../en/model_zoo/multimodal/actbert.md) | 简体中文 + +# ActBERT多模态预训练模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [参考论文](#参考论文) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install paddlenlp +python -m pip install lmdb +``` + +## 模型简介 + +ActBERT是百度在CVPR2020提出的多模态预训练模型,它结合输入文本、图像和视频动作三种模态,使用一种全新的纠缠编码模块从三个来源进行多模态特征学习,以增强两个视觉输入和语言之间的互动功能。模型采用RandomMask和NSP的方式进行训练,在文本视频搜索、视频描述生成等5个下游任务中表现优异。 + +
    +
    +
    + + +## 数据准备 + +HowTo100M数据下载及准备请参考[HowTo100M数据准备](../../dataset/howto100m.md) + +MSR-VTT数据下载及准备请参考[MSR-VTT数据准备](../../dataset/msrvtt.md) + + +## 模型训练 + +### HowTo100M数据集训练 + +#### 下载并添加预训练模型 + +下载BERT预训练模型[bert-base-uncased](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/bert-base-uncased.pdparams)作为Backbone初始化参数,或是通过命令行下载 + +```bash +wget https://videotag.bj.bcebos.com/PaddleVideo-release2.2/bert-base-uncased.pdparams +``` + +并将文件路径添加到配置文件中的`MODEL.framework.backbone.pretrained`字段,如下: + +```yaml +MODEL: + framework: "ActBert" + backbone: + name: "BertForMultiModalPreTraining" + pretrained: 将路径填写到此处 +``` + +- 由于训练数据集过大,本代码提供小数据训练功能,训练配置仅供参考~ + +#### 开始训练 + +- 训练启动命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_actbert main.py --validate -c configs/multimodal/actbert/actbert.yaml +``` + +- 开启amp混合精度训练,可加速训练过程,其训练启动命令如下: + +```bash +export FLAGS_conv_workspace_size_limit=800 #MB +export FLAGS_cudnn_exhaustive_search=1 +export FLAGS_cudnn_batchnorm_spatial_persistent=1 + +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_actbert main.py --amp --validate -c configs/multimodal/actbert/actbert.yaml +``` + +- 另外您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的。 + + +## 模型测试 + +- 对下游任务:文本-视频检索,在MSR-VTT数据集上评估性能,评估脚本启动方式如下: + + +```bash +python3.7 main.py --test -c configs/multimodal/actbert/actbert_msrvtt.yaml -w Actbert.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + + +MSR-VTT数据集测试精度: + +| R@1 | R@5 | R@10 | Median R | Mean R | checkpoints | +| :------: | :----------: | :----: | :----: | :----: | :----: | +| 8.6 | 31.2 | 45.5 | 13.0 | 28.5 | [ActBERT.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ActBERT.pdparams) | + + +## 参考论文 + +- [ActBERT: Learning Global-Local Video-Text Representations +](https://arxiv.org/abs/2011.07231), Linchao Zhu, Yi Yang diff --git a/docs/zh-CN/model_zoo/partition/transnetv2.md b/docs/zh-CN/model_zoo/partition/transnetv2.md new file mode 100644 index 0000000000000000000000000000000000000000..51c2510941e59624f9b68000b580396b426f3cc5 --- /dev/null +++ b/docs/zh-CN/model_zoo/partition/transnetv2.md @@ -0,0 +1,85 @@ +[English](../../../en/model_zoo/partition/transnetv2.md) | 简体中文 + +# TransNetV2视频切分模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + +在开始使用之前,您需要按照以下命令安装额外的依赖包: +```bash +python -m pip install ffmpeg-python==0.2.0 +``` + +## 模型简介 + +TransNetV2是一种基于深度学习的视频切分模型,通过DDCNN V2结构进行特征学习,并加入RGB颜色直方图、视频帧相似度进行更有效的特征提取,最终获取每一帧是否是镜头边界帧的概率,从而完成视频切分。该算法效果较好,且计算高效,十分适合工业落地。 + +![](../../../images/transnetv2.png) + +本代码当前仅支持模型推理,模型的训练和测试将在后续提供。 + + +## 数据准备 + +coming soon + + +## 模型训练 + +coming soon + + +## 模型测试 + +coming soon + + +## 模型推理 + +下载在ClipShots和TRECVID IACC.3上训练好的TransNetV2模型参数 [TransNetV2_shots.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TransNetV2_shots.pdparams),也可以通过命令行下载 + +```bash +wget https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TransNetV2_shots.pdparams +``` + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/partitioners/transnetv2/transnetv2.yaml -p data/TransNetV2_shots.pdparams -o inference/TransNetV2 +``` + +上述命令将生成预测所需的模型结构文件`TransNetV2.pdmodel`和模型权重文件`TransNetV2.pdiparams`以及`TransNetV2.pdiparams.info`文件,均存放在`inference/TransNetV2/`目录下 + +上述bash命令中各个参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/partitioners/transnetv2/transnetv2.yaml \ + --model_file inference/TransNetV2/TransNetV2.pdmodel \ + --params_file inference/TransNetV2/TransNetV2.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +通过定义配置文件`transnetv2.yaml`中`output_path`参数,可以将每帧的预测概率输出到`{output_path}/example_predictions.txt`中,预测得到的镜头边界输出到`{output_path}/example_scenes.txt`中。 +通过定义配置文件`transnetv2.yaml`中`visualize`参数为True,可以将预测结果可视化,可视化结果保存至`{output_path}/example_vis.png`。 + +输出示例如下: + +```bash +Current video file: data/example.avi + Shot Boundarys: [[ 0 130]] +``` + +可以看到,使用TransNetV2模型对`data/example.avi`进行预测,输出的视频镜头边界帧为[0,130]。 +## 参考论文 + +- [TransNet V2: An effective deep network architecture for fast shot transition detection](https://arxiv.org/abs/2008.04838), Tomáš Souček, Jakub Lokoč diff --git a/docs/zh-CN/model_zoo/recognition/agcn.md b/docs/zh-CN/model_zoo/recognition/agcn.md new file mode 100644 index 0000000000000000000000000000000000000000..5e66a74f30a6e8a4c90db36f3ecf344d5d002d1a --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/agcn.md @@ -0,0 +1,134 @@ +[English](../../../en/model_zoo/recognition/agcn.md) | 简体中文 + +# AGCN基于骨骼的行为识别模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + + +我们对[ST-GCN模型](./stgcn.md)进行了优化,实现了精度更高的AGCN模型,模型优化细节参考[AGCN模型解析](https://www.bilibili.com/video/BV1w3411172G). + + +## 数据准备 + +花样滑冰比赛数据下载及准备请参考[花样滑冰数据准备](../../dataset/fsd.md) + +NTU-RGBD数据下载及准备请参考[NTU-RGBD数据准备](../../dataset/ntu-rgbd.md) + +## 模型训练 + +### 花样滑冰比赛数据集训练 + +- 花样滑冰比赛数据集使用单卡训练,启动命令如下: + +```bash +python3.7 main.py -c configs/recognition/agcn/agcn_fsd.yaml +``` + +- 由于赛事未提供验证集数据,因此训练时不做valid。 + +- 您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的,参数用法请参考[config](../../tutorials/config.md)。 + +### NTU-RGBD数据集训练 + +- NTU-RGBD数据集使用4卡训练,启动命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_agcn main.py --validate -c configs/recognition/agcn/agcn_ntucs.yaml +``` + +- `agcn_ntucs.yaml`配置文件为NTU-RGB+D数据集按cross-subject划分方式对应的训练配置。 + + +## 模型测试 + +### 花样滑冰比赛数据集模型测试 + +- 模型测试的启动命令如下: + +```bash +python3.7 main.py --test -c configs/recognition/agcn/agcn_fsd.yaml -w output/AGCN/AGCN_epoch_00100.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + +- 评估结果保存在submission.csv文件中,可在[评测官网](https://aistudio.baidu.com/aistudio/competition/detail/115)提交查看得分。 + +模型在花样滑冰比赛数据集上baseline实验精度如下: + +| Test_Data | Top-1 | checkpoints | +| :----: | :----: | :---- | +| Test_A | 62.29 | [AGCN_fsd.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AGCN_fsd.pdparams) | + + +### NTU-RGB+D数据集模型测试 + +- 模型测试的启动命令如下: + +```bash +python3.7 main.py --test -c configs/recognition/agcn/agcn_ntucs.yaml -w output/AGCN/AGCN_best.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + +模型在NTU-RGB+D数据集上实验精度如下: + +| split | Top-1 | checkpoints | +| :----: | :----: | :---- | +| cross-subject | 83.27 | [AGCN_ntucs.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AGCN_ntucs.pdparams) | + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/agcn/agcn_fsd.yaml \ + -p data/AGCN_fsd.pdparams \ + -o inference/AGCN +``` + +上述命令将生成预测所需的模型结构文件`AGCN.pdmodel`和模型权重文件`AGCN.pdiparams`。 + +- 各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/fsd10/example_skeleton.npy \ + --config configs/recognition/agcn/agcn_fsd.yaml \ + --model_file inference/AGCN/AGCN.pdmodel \ + --params_file inference/AGCN/AGCN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +``` +Current video file: data/fsd10/example_skeleton.npy + top-1 class: 27 + top-1 score: 0.8965644240379333 +``` + +可以看到,使用在FSD上训练好的AGCN模型对`data/example_skeleton.npy`进行预测,输出的top1类别id为`27`,置信度为0.89。 + +## 参考论文 + +- [Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1801.07455), Sijie Yan, Yuanjun Xiong, Dahua Lin + +- [Two-Stream Adaptive Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1805.07694), Lei Shi, Yifan Zhang, Jian Cheng, Hanqing Lu + +- [Skeleton-Based Action Recognition with Multi-Stream Adaptive Graph Convolutional Networks](https://arxiv.org/abs/1912.06971), Lei Shi, Yifan Zhang, Jian Cheng, Hanqing Lu + +- Many thanks to [li7819559](https://github.com/li7819559) and [ZhaoJingjing713](https://github.com/ZhaoJingjing713) for contributing the code. diff --git a/docs/zh-CN/model_zoo/recognition/attention_lstm.md b/docs/zh-CN/model_zoo/recognition/attention_lstm.md new file mode 100644 index 0000000000000000000000000000000000000000..df04f07f2c780d3eac543d0cbbcd74852a81eb0b --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/attention_lstm.md @@ -0,0 +1,86 @@ +简体中文 | [English](../../../en/model_zoo/recognition/attention_lstm.md) + +# AttentionLSTM + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + +## 模型简介 + +循环神经网络(RNN)常用于序列数据的处理,可建模视频连续多帧的时序信息,在视频分类领域为基础常用方法。 +该模型采用了双向长短时记忆网络(LSTM),将视频的所有帧特征依次编码。与传统方法直接采用LSTM最后一个时刻的输出不同,该模型增加了一个Attention层,每个时刻的隐状态输出都有一个自适应权重,然后线性加权得到最终特征向量。参考论文中实现的是两层LSTM结构,而**本模型实现的是带Attention的双向LSTM**。 + +Attention层可参考论文[AttentionCluster](https://arxiv.org/abs/1711.09550) + +## 数据准备 + +PaddleVide提供了在Youtube-8M数据集上训练和测试脚本。Youtube-8M数据下载及准备请参考[YouTube-8M数据准备](../../dataset/youtube8m.md) + +## 模型训练 + +### Youtube-8M数据集训练 + +#### 开始训练 + +- Youtube-8M数据集使用8卡训练,feature格式下会使用视频和音频特征作为输入,数据的训练启动命令如下 + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_attetion_lstm main.py --validate -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml + ``` + +## 模型测试 + +命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_attetion_lstm main.py --test -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml -w "output/AttentionLSTM/AttentionLSTM_best.pdparams" +``` + +当测试配置采用如下参数时,在Youtube-8M的validation数据集上的测试指标如下: + +| Hit@1 | PERR | GAP | checkpoints | +| :-----: | :---------: | :---: | ----- | +| 89.05 | 80.49 | 86.30 | [AttentionLSTM_yt8.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AttentionLSTM_yt8.pdparams) | + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml \ + -p data/AttentionLSTM_yt8.pdparams \ + -o inference/AttentionLSTM +``` + +上述命令将生成预测所需的模型结构文件`AttentionLSTM.pdmodel`和模型权重文件`AttentionLSTM.pdiparams`。 + +各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-模型推理) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.pkl \ + --config configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml \ + --model_file inference/AttentionLSTM/AttentionLSTM.pdmodel \ + --params_file inference/AttentionLSTM/AttentionLSTM.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` +输出示例如下: +```bash +Current video file: data/example.pkl + top-1 class: 11 + top-1 score: 0.9841002225875854 +``` +可以看到,使用在Youtube-8M上训练好的AttentionLSTM模型对data/example.pkl进行预测,输出的top1类别id为11,置信度为0.98。 +## 参考论文 + +- [Attention Clusters: Purely Attention Based Local Feature Integration for Video Classification](https://arxiv.org/abs/1711.09550), Xiang Long, Chuang Gan, Gerard de Melo, Jiajun Wu, Xiao Liu, Shilei Wen +- [YouTube-8M: A Large-Scale Video Classification Benchmark](https://arxiv.org/abs/1609.08675), Sami Abu-El-Haija, Nisarg Kothari, Joonseok Lee, Paul Natsev, George Toderici, Balakrishnan Varadarajan, Sudheendra Vijayanarasimhan + diff --git a/docs/zh-CN/model_zoo/recognition/ctrgcn.md b/docs/zh-CN/model_zoo/recognition/ctrgcn.md new file mode 100644 index 0000000000000000000000000000000000000000..b1d7a2721c638a0e6c01924f5415ae6464114d1d --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/ctrgcn.md @@ -0,0 +1,132 @@ +[English](../../../en/model_zoo/recognition/ctrgcn.md) | 简体中文 + +# CTR-GCN基于骨骼的行为识别模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +[CTR-GCN](https://github.com/Uason-Chen/CTR-GCN.git)是ICCV 2021提出的基于骨骼的行为识别模型,通过将改动应用在具有拓扑结构的人体骨骼数据上的图卷积,使用时空图卷积提取时空特征进行行为识别,提升了基于骨骼的行为识别任务精度。 + +
    +
    +
    + + +## 数据准备 + +NTU-RGBD数据下载及准备请参考[NTU-RGBD数据准备](../../dataset/ntu-rgbd.md) + + +## 模型训练 + +### NTU-RGBD数据集训练 + +- NTU-RGBD数据集单卡训练,启动命令如下: + +```bash +# joint modality +python main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml --seed 1 + +# bone modality +python main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_bone.yaml --seed 1 + +# motion modality +python main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_motion.yaml --seed 1 + +# bone motion modality +python main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_bone_motion.yaml --seed 1 +``` + +- NTU-RGBD数据集使用4卡训练,启动命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_ctrgcn main.py --validate -c configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml +``` + +- 配置文件`ctrgcn_ntucs_joint.yaml`为NTU-RGB+D数据集按cross-subject划分方式对应的训练配置。 + + +## 模型测试 + +### NTU-RGB+D数据集模型测试 + +- 模型测试的启动命令如下: + +```bash +# joint modality +python3.7 main.py --test -c configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml -w data/CTRGCN_ntucs_joint.pdparams + +# bone modality +python3.7 main.py --test -c configs/recognition/ctrgcn/ctrgcn_ntucs_bone.yaml -w data/CTRGCN_ntucs_bone.pdparams + +# motion modality +python3.7 main.py --test -c configs/recognition/ctrgcn/ctrgcn_ntucs_motion.yaml -w data/CTRGCN_ntucs_motion.pdparams + +# bone motion modality +python3.7 main.py --test -c configs/recognition/ctrgcn/ctrgcn_ntucs_bone_motion.yaml -w data/CTRGCN_ntucs_bone_motion.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + +模型在NTU-RGB+D数据集上实验精度如下: + +| split | modality | Top-1 | checkpoints | +| :----: | :----: | :----: | :----: | +| cross-subject | joint | 89.93 | [CTRGCN_ntucs_joint.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/CTRGCN_ntucs_joint.pdparams) | +| cross-subject | bone | 85.24 | [CTRGCN_ntucs_bone.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/CTRGCN_ntucs_bone.pdparams) | +| cross-subject | motion | 85.33 | [CTRGCN_ntucs_motion.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/CTRGCN_ntucs_motion.pdparams) | +| cross-subject | bone motion | 84.53 | [CTRGCN_ntucs_bone_motion.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/CTRGCN_ntucs_bone_motion.pdparams) | + + + + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml \ + -p data/CTRGCN_ntucs_joint.pdparams \ + -o inference/CTRGCN +``` + +上述命令将生成预测所需的模型结构文件`CTRGCN_joint.pdmodel`和模型权重文件`CTRGCN_joint.pdiparams`。 + +- 各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example_NTU-RGB-D_sketeton.npy \ + --config configs/recognition/ctrgcn/ctrgcn_ntucs_joint.yaml \ + --model_file inference/CTRGCN_joint/CTRGCN_joint.pdmodel \ + --params_file inference/CTRGCN_joint/CTRGCN_joint.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +``` +Current video file: data/example_NTU-RGB-D_sketeton.npy + top-1 class: 4 + top-1 score: 0.999988317489624 +``` + +可以看到,使用在NTU-RGBD数据集上训练好的ST-GCN模型对`data/example_NTU-RGB-D_sketeton.npy`进行预测,输出的top1类别id为`4`,置信度为0.999988317489624。 + + +## 参考论文 + +- [Channel-wise Topology Refinement Graph Convolution for Skeleton-Based Action Recognition](https://arxiv.org/abs/2107.12213), Chen, Yuxin and Zhang, Ziqi and Yuan, Chunfeng and Li, Bing and Deng, Ying and Hu, Weiming diff --git a/docs/zh-CN/model_zoo/recognition/movinet.md b/docs/zh-CN/model_zoo/recognition/movinet.md new file mode 100644 index 0000000000000000000000000000000000000000..6f5ba7dd40664ff260a2686d61abdc47e9ae9f10 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/movinet.md @@ -0,0 +1,90 @@ +[English](../../../en/model_zoo/recognition/movinet.md) | 简体中文 + +# MoViNet视频分类模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +MoViNet是Google Research研发的移动视频网络。它使用神经结构搜索的方法来搜索MoViNet空间结构,使用因果卷积算子和流缓冲区来弥补准确率的损失,Temporal Ensembles提升准确率,是一个可以用于在线推理视频流的,轻量高效视频模型。 + +## 数据准备 + +Kinetics-400数据下载及准备请参考[kinetics-400数据准备](../../dataset/k400.md) + +## 模型训练 + +数据准备完成后,可通过如下方式启动训练: + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_movinet main.py --validate -c configs/recognition/movinet/movinet_k400_frame.yaml +``` + +## 模型测试 + +- MoViNet模型在训练时同步进行测试,您可以通过在训练日志中查找关键字`best`获取模型测试精度,日志示例如下: + +```txt +Already save the best model (top1 acc)0.6489 +``` + +- 若需单独运行测试代码,其启动命令如下: + +```bash +python3.7 main.py --test -c configs/recognition/movinet/movinet_k400_frame.yaml -w output/MoViNet/MoViNet_best.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + +当测试配置采用如下参数时,在Kinetics-400的validation数据集上的评估精度如下: + +| Config | Sampling method | num_seg | target_size | Top-1 | checkpoints | +| :------: | :--------: | :-------: | :-------: | :-----: | :-----: | +| A0 | Uniform | 50 | 172 | 66.62 | [MoViNetA0_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.3/MoViNetA0_k400.pdparams) | + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/movinet/movinet_k400_frame.yaml \ + -p data/MoViNetA0_k400.pdparams \ + -o inference/MoViNetA0 +``` + +上述命令将生成预测所需的模型结构文件`MoViNetA0.pdmodel`和模型权重文件`MoViNetA0.pdiparams`。 + +各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/movinet/movinet_k400_frame.yaml \ + --model_file inference/MoViNetA0/MoViNet.pdmodel \ + --params_file inference/MoViNetA0/MoViNet.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: +```txt +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.7667049765586853 +``` + +## 参考论文 + +- [MoViNets: Mobile Video Networks for Efficient Video Recognition](https://arxiv.org/abs/2103.11511) diff --git a/docs/zh-CN/model_zoo/recognition/pp-timesformer.md b/docs/zh-CN/model_zoo/recognition/pp-timesformer.md new file mode 100644 index 0000000000000000000000000000000000000000..0cb3cf25cda66526d38d22a51d29ae7cbdd756b9 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/pp-timesformer.md @@ -0,0 +1,157 @@ +[English](../../../en/model_zoo/recognition/pp-timesformer.md) | 简体中文 + +# PP-TimeSformer视频分类模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +我们对[TimeSformer模型](./timesformer.md)进行了改进和优化,得到了更高精度的2D实用视频分类模型**PP-TimeSformer**。在不增加参数量和计算量的情况下,在UCF-101、Kinetics-400等数据集上精度显著超过原版,在Kinetics-400数据集上的精度如下表所示。 + +| Version | Top1 | +| :------ | :----: | +| Ours ([swa](#refer-anchor-1)+distill+16frame) | 79.44 | +| Ours ([swa](#refer-anchor-1)+distill) | 78.87 | +| Ours ([swa](#refer-anchor-1)) | **78.61** | +| [mmaction2](https://github.com/open-mmlab/mmaction2/tree/master/configs/recognition/timesformer#kinetics-400) | 77.92 | + + +## 数据准备 + +K400数据下载及准备请参考[Kinetics-400数据准备](../../dataset/k400.md) + +UCF101数据下载及准备请参考[UCF-101数据准备](../../dataset/ucf101.md) + + +## 模型训练 + +### Kinetics-400数据集训练 + +#### 下载并添加预训练模型 + +1. 下载图像预训练模型[ViT_base_patch16_224_miil_21k.pdparams](https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams)作为Backbone初始化参数,或通过wget命令下载 + + ```bash + wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams + ``` + +2. 打开`PaddleVideo/configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml`,将下载好的权重存放路径填写到下方`pretrained:`之后 + + ```yaml + MODEL: + framework: "RecognizerTransformer" + backbone: + name: "VisionTransformer" + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- Kinetics400数据集使用8卡训练,训练方式的启动命令如下: + + ```bash + # videos数据格式 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --validate -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml + ``` + +- 开启amp混合精度训练,可加速训练过程,其训练启动命令如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + # videos数据格式 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --amp --validate -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml + ``` + +- 另外您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的,建议配置文件的命名方式为`模型_数据集名称_文件格式_数据格式_采样方式.yaml`,参数用法请参考[config](../../tutorials/config.md)。 + + +## 模型测试 + +- PP-TimeSformer模型在训练时同步进行验证,您可以通过在训练日志中查找关键字`best`获取模型测试精度,日志示例如下: + + ``` + Already save the best model (top1 acc)0.7258 + ``` + +- 由于PP-TimeSformer模型测试模式的采样方式是速度稍慢但精度高一些的**UniformCrop**,与训练过程中验证模式采用的**RandomCrop**不同,所以训练日志中记录的验证指标`topk Acc`不代表最终的测试分数,因此在训练完成之后可以用测试模式对最好的模型进行测试获取最终的指标,命令如下: + + ```bash + # 8-frames 模型测试命令 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --test -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml -w "output/ppTimeSformer/ppTimeSformer_best.pdparams" + + # 16-frames模型测试命令 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --test \ + -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml \ + -o MODEL.backbone.num_seg=16 \ + -o MODEL.runtime_cfg.test.num_seg=16 \ + -o MODEL.runtime_cfg.test.avg_type='prob' \ + -o PIPELINE.test.decode.num_seg=16 \ + -o PIPELINE.test.sample.num_seg=16 \ + -w "data/ppTimeSformer_k400_16f_distill.pdparams" + ``` + + + 当测试配置采用如下参数时,在Kinetics-400的validation数据集上的测试指标如下: + + | backbone | Sampling method | num_seg | target_size | Top-1 | checkpoints | + | :----------------: | :-------------: | :-----: | :---------: | :---- | :----------------------------------------------------------: | + | Vision Transformer | UniformCrop | 8 | 224 | 78.61 | [ppTimeSformer_k400_8f.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTimeSformer_k400_8f.pdparams) | + | Vision Transformer | UniformCrop | 8 | 224 | 78.87 | [ppTimeSformer_k400_8f_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTimeSformer_k400_8f_distill.pdparams) | + | Vision Transformer | UniformCrop | 16 | 224 | 79.44 | [ppTimeSformer_k400_16f_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTimeSformer_k400_16f_distill.pdparams) | + + +- 测试时,PP-TimeSformer视频采样策略为使用linspace采样:时序上,从待采样视频序列的第一帧到最后一帧区间内,均匀生成`num_seg`个稀疏采样点(包括端点);空间上,选择长边两端及中间位置(左中右 或 上中下)3个区域采样。1个视频共采样1个clip。 + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml \ + -p data/ppTimeSformer_k400_8f.pdparams \ + -o inference/ppTimeSformer +``` + +上述命令将生成预测所需的模型结构文件`ppTimeSformer.pdmodel`和模型权重文件`ppTimeSformer.pdiparams`。 + +- 各参数含义可参考[模型推理方法](../../start.md#2-模型推理) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml \ + --model_file inference/ppTimeSformer/ppTimeSformer.pdmodel \ + --params_file inference/ppTimeSformer/ppTimeSformer.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9997474551200867 +``` + +可以看到,使用在Kinetics-400上训练好的ppTimeSformer模型对`data/example.avi`进行预测,输出的top1类别id为`5`,置信度为0.99。通过查阅类别id与名称对应表`data/k400/Kinetics-400_label_list.txt`,可知预测类别名称为`archery`。 + +## 参考论文 + +- [Is Space-Time Attention All You Need for Video Understanding?](https://arxiv.org/pdf/2102.05095.pdf), Gedas Bertasius, Heng Wang, Lorenzo Torresani +- [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531), Geoffrey Hinton, Oriol Vinyals, Jeff Dean +
    + +- [Averaging Weights Leads to Wider Optima and Better Generalization](https://arxiv.org/abs/1803.05407v3), Pavel Izmailov, Dmitrii Podoprikhin, Timur Garipov +- [ImageNet-21K Pretraining for the Masses](https://arxiv.org/pdf/2104.10972v4.pdf), Tal Ridnik, Emanuel Ben-Baruch, Asaf Noy diff --git a/docs/zh-CN/model_zoo/recognition/pp-tsm.md b/docs/zh-CN/model_zoo/recognition/pp-tsm.md new file mode 100644 index 0000000000000000000000000000000000000000..fa01b3a0df66d70ab4ae0604c27a5cafc3dc18f4 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/pp-tsm.md @@ -0,0 +1,181 @@ +[English](../../../en/model_zoo/recognition/pp-tsm.md) | 简体中文 + +# PP-TSM视频分类模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +我们对[TSM模型](./tsm.md)进行了改进,提出了高精度2D实用视频分类模型**PP-TSM**。在不增加参数量和计算量的情况下,在UCF-101、Kinetics-400等数据集上精度显著超过原文,在Kinetics-400数据集上的精度如下表所示。模型优化解析请参考[**PP-TSM模型精度优化Tricks详解**](https://zhuanlan.zhihu.com/p/382134297)。 + +| Version | Sampling method | Top1 | +| :------ | :----------: | :----: | +| Ours (distill) | Dense | **76.16** | +| Ours | Dense | 75.69 | +| [mmaction2](https://github.com/open-mmlab/mmaction2/blob/master/configs/recognition/tsm/README.md) | Dense | 74.55 | +| [mit-han-lab](https://github.com/mit-han-lab/temporal-shift-module) | Dense | 74.1 | + +| Version | Sampling method | Top1 | +| :------ | :----------: | :----: | +| Ours (distill) | Uniform | **75.11** | +| Ours | Uniform | 74.54 | +| [mmaction2](https://github.com/open-mmlab/mmaction2/blob/master/configs/recognition/tsm/README.md) | Uniform | 71.90 | +| [mit-han-lab](https://github.com/mit-han-lab/temporal-shift-module) | Uniform | 71.16 | + + +## 数据准备 + +K400数据下载及准备请参考[Kinetics-400数据准备](../../dataset/k400.md) + +UCF101数据下载及准备请参考[UCF-101数据准备](../../dataset/ucf101.md) + + +## 模型训练 + +### Kinetics-400数据集训练 + +#### 下载并添加预训练模型 + +下载图像蒸馏预训练模型[ResNet50_vd_ssld_v2.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams)作为Backbone初始化参数,或是通过命令行下载 + +```bash +wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams +``` + +并将文件路径添加到配置文件中的`MODEL.framework.backbone.pretrained`字段,如下: + +```yaml +MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTweaksTSM" + pretrained: 将路径填写到此处 +``` + +- 如果使用ResNet101作为Backbone进行训练,请下载预训练模型[ResNet101_vd_ssld_pretrained.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ResNet101_vd_ssld_pretrained.pdparams). + +#### 开始训练 + +- Kinetics400数据集使用8卡训练,frames格式数据,uniform训练方式的启动命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +``` + +- Kinetics400数据集使用8卡训练,videos格式数据,uniform训练方式的启动命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_videos_uniform.yaml +``` + +- 开启amp混合精度训练,可加速训练过程,其训练启动命令如下: + +```bash +export FLAGS_conv_workspace_size_limit=800 #MB +export FLAGS_cudnn_exhaustive_search=1 +export FLAGS_cudnn_batchnorm_spatial_persistent=1 + +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --amp --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +``` + +- Kinetics400数据集frames格式数据,dense训练方式的启动命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_dense.yaml +``` + +- Kinetics400数据集frames格式数据,dense训练方式,ResNet101作为Backbone的启动命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_dense_r101.yaml +``` + +- 另外您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的,配置文件命名方式为`模型_数据集_文件格式_数据格式_采样方式.yaml`,参数用法请参考[config](../../tutorials/config.md)。 + + +## 模型测试 + +- 对Uniform采样方式,PP-TSM模型在训练时同步进行测试,您可以通过在训练日志中查找关键字`best`获取模型测试精度,日志示例如下: + +```txt +Already save the best model (top1 acc)0.7454 +``` + +- 对dense采样方式,需单独运行测试代码,其启动命令如下: + +```bash +python3 main.py --test -c configs/recognition/pptsm/pptsm_k400_frames_dense.yaml -w output/ppTSM/ppTSM_best.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + + +Kinetics400数据集测试精度: + +| backbone | distill | Sampling method | num_seg | target_size | Top-1 | checkpoints | +| :------: | :----------: | :----: | :----: | :----: | :----: | :---- | +| ResNet50 | False | Uniform | 8 | 224 | 74.54 | [ppTSM_k400_uniform.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_uniform.pdparams) | +| ResNet50 | False | Dense | 8 | 224 | 75.69 | [ppTSM_k400_dense.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_dense.pdparams) | +| ResNet50 | True | Uniform | 8 | 224 | 75.11 | [ppTSM_k400_uniform_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_uniform_distill.pdparams) | +| ResNet50 | True | Dense | 8 | 224 | 76.16 | [ppTSM_k400_dense_distill.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_dense_distill.pdparams) | +| ResNet101 | True | Uniform | 8 | 224 | 76.35 | [ppTSM_k400_uniform_distill_r101.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSM_k400_uniform_distill_r101.pdparams) | +| ResNet101 | False | Dense | 8 | 224 | 77.15 | [ppTSM_k400_dense_r101.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSM_k400_dense_r101.pdparams) | + +- Uniform采样: 时序上,等分成`num_seg`段,每段中间位置采样1帧;空间上,中心位置采样。1个视频共采样1个clips。 + +- Dense采样:时序上,先等分成10个片段,每段从起始位置开始,以`64//num_seg`为间隔连续采样`num_seg`帧;空间上,左中,中心,右中3个位置采样。1个视频共采样`10*3=30`个clips。 + +- distill为`True`表示使用了蒸馏所得的预训练模型。 + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml \ + -p data/ppTSM_k400_uniform.pdparams \ + -o inference/ppTSM +``` + +上述命令将生成预测所需的模型结构文件`ppTSM.pdmodel`和模型权重文件`ppTSM.pdiparams`。 + +- 各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml \ + --model_file inference/ppTSM/ppTSM.pdmodel \ + --params_file inference/ppTSM/ppTSM.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + + +输出示例如下: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9907386302947998 +``` + + +可以看到,使用在Kinetics-400上训练好的PP-TSM模型对`data/example.avi`进行预测,输出的top1类别id为`5`,置信度为0.99。通过查阅类别id与名称对应表`data/k400/Kinetics-400_label_list.txt`,可知预测类别名称为`archery`。 + + +## 参考论文 + +- [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf), Ji Lin, Chuang Gan, Song Han +- [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531), Geoffrey Hinton, Oriol Vinyals, Jeff Dean diff --git a/docs/zh-CN/model_zoo/recognition/pp-tsn.md b/docs/zh-CN/model_zoo/recognition/pp-tsn.md new file mode 100644 index 0000000000000000000000000000000000000000..3229fdb0dc462df9a222e87f445ad1f5a98c9ef5 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/pp-tsn.md @@ -0,0 +1,148 @@ +[English](../../../en/model_zoo/recognition/pp-tsn.md) | 简体中文 + +# PP-TSN视频分类模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +我们对[TSN模型](./tsn.md)进行了改进,得到了更高精度的2D实用视频分类模型**PP-TSN**。在不增加参数量和计算量的情况下,在UCF-101、Kinetics-400等数据集上精度显著超过原版,在Kinetics-400数据集上的精度如下表所示。 + +| Version | Top1 | +| :------ | :----: | +| Ours (distill) | 75.06 | +| Ours | **73.68** | +| [mmaction2](https://github.com/open-mmlab/mmaction2/tree/master/configs/recognition/tsn#kinetics-400) | 71.80 | + + +## 数据准备 + +K400数据下载及准备请参考[Kinetics-400数据准备](../../dataset/k400.md) + +UCF101数据下载及准备请参考[UCF-101数据准备](../../dataset/ucf101.md) + + +## 模型训练 + +### Kinetics-400数据集训练 + +#### 下载并添加预训练模型 + +1. 下载图像蒸馏预训练模型[ResNet50_vd_ssld_v2.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams)作为Backbone初始化参数,或通过wget命令下载 + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams + ``` + +2. 打开`PaddleVideo/configs/recognition/pptsn/pptsn_k400_frames.yaml`,将下载好的权重存放路径填写到下方`pretrained:`之后 + + ```yaml + MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTweaksTSN" + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- Kinetics400数据集使用8卡训练,训练方式的启动命令如下: + + ```bash + # frames数据格式 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --validate -c configs/recognition/pptsn/pptsn_k400_frames.yaml + + # videos数据格式 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --validate -c configs/recognition/pptsn/pptsn_k400_videos.yaml + ``` + +- 开启amp混合精度训练,可加速训练过程,其训练启动命令如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + # frames数据格式 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --amp --validate -c configs/recognition/pptsn/pptsn_k400_frames.yaml + + # videos数据格式 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --amp --validate -c configs/recognition/pptsn/pptsn_k400_videos.yaml + ``` + +- 另外您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的,建议配置文件的命名方式为`模型_数据集名称_文件格式_数据格式_采样方式.yaml`,参数用法请参考[config](../../tutorials/config.md)。 + + +## 模型测试 + +- PP-TSN模型在训练时同步进行验证,您可以通过在训练日志中查找关键字`best`获取模型测试精度,日志示例如下: + + ``` + Already save the best model (top1 acc)0.7004 + ``` + +- 由于PP-TSN模型测试模式的采样方式是速度稍慢但精度高一些的**TenCrop**,与训练过程中验证模式采用的**CenterCrop**不同,所以训练日志中记录的验证指标`topk Acc`不代表最终的测试分数,因此在训练完成之后可以用测试模式对最好的模型进行测试获取最终的指标,命令如下: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --test -c configs/recognition/pptsn/pptsn_k400_frames.yaml -w "output/ppTSN/ppTSN_best.pdparams" + ``` + + + 当测试配置采用如下参数时,在Kinetics-400的validation数据集上的测试指标如下: + + + | backbone | Sampling method | distill | num_seg | target_size | Top-1 | checkpoints | + | :------: | :----------: | :----: | :----: | :----: | :---- | :---: | + | ResNet50 | TenCrop | False | 3 | 224 | 73.68 | [ppTSN_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSN_k400.pdparams) | + | ResNet50 | TenCrop | True | 8 | 224 | 75.06 | [ppTSN_k400_8.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSN_k400_8.pdparams) | + +- PP-TSN视频采样策略为TenCrop采样:时序上,将待输入视频均匀分成`num_seg`段区间,每段的中间位置采样1帧;空间上,从左上角、右上角、中心点、左下角、右下角5个子区域各采样224x224的区域,并加上水平翻转,一共得到10个采样结果。1个视频共采样1个clip。 + +- distill为`True`表示使用了蒸馏所得的预训练模型,具体蒸馏方案参考[PP-TSM蒸馏方案](https://zhuanlan.zhihu.com/p/382134297)。 + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/pptsn/pptsn_k400_frames.yaml -p data/ppTSN_k400.pdparams -o inference/ppTSN +``` + +上述命令将生成预测所需的模型结构文件`ppTSN.pdmodel`和模型权重文件`ppTSN.pdiparams`以及`ppTSN.pdiparams.info`文件,均存放在`inference/ppTSN/`目录下 + +上述bash命令中各个参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/pptsn/pptsn_k400_frames.yaml \ + --model_file inference/ppTSN/ppTSN.pdmodel \ + --params_file inference/ppTSN/ppTSN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +```bash +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.998979389667511 +``` + +可以看到,使用在Kinetics-400上训练好的PP-TSN模型对`data/example.avi`进行预测,输出的top1类别id为`5`,置信度为0.99。通过查阅类别id与名称对应表`data/k400/Kinetics-400_label_list.txt`,可知预测类别名称为`archery`。 + +## 参考论文 + +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/pdf/1608.00859.pdf), Limin Wang, Yuanjun Xiong, Zhe Wang +- [Distilling the Knowledge in a Neural Network](https://arxiv.org/abs/1503.02531), Geoffrey Hinton, Oriol Vinyals, Jeff Dean diff --git a/docs/zh-CN/model_zoo/recognition/slowfast.md b/docs/zh-CN/model_zoo/recognition/slowfast.md new file mode 100644 index 0000000000000000000000000000000000000000..030aaab4b27ccaf9f8546406a494a33a6c1a0947 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/slowfast.md @@ -0,0 +1,140 @@ +简体中文 | [English](../../../en/model_zoo/recognition/slowfast.md) + +# SlowFast视频分类模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +SlowFast是视频分类领域的高精度模型,使用slow和fast两个分支。slow分支以稀疏采样得到的帧作为输入,捕捉视频中的表观信息。fast分支以高频采样得到的帧作为输入,捕获视频中的运动信息,最终将两个分支的特征拼接得到预测结果。 + +

    +
    +SlowFast Overview +

    + +详细内容请参考ICCV 2019论文[SlowFast Networks for Video Recognition](https://arxiv.org/abs/1812.03982) + + +## 数据准备 + +SlowFast模型的训练数据采用Kinetics400数据集,数据下载及准备请参考[Kinetics-400数据准备](../../dataset/k400.md) + + +## 模型训练 + +数据准备完成后,可通过如下方式启动训练: + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +python -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_slowfast main.py --validate -c configs/recognition/slowfast/slowfast.yaml +``` + +- 从头开始训练,使用上述启动命令行或者脚本程序即可启动训练,不需要用到预训练模型。 + +- 建议使用多卡训练方式,单卡由于batch\_size减小,精度可能会有损失。 + + +### 训练资源要求 + +* 8卡V100,总batch\_size=64,单卡batch\_size=8,单卡显存占用约9G。 +* 训练速度相较原始实现提速100%,详细参考[benchmark](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/benchmark.md#实验结果) + +### 训练加速 + +SlowFast为3D模型,训练异常耗时,为进一步加速模型的训练,我们实现了[Multigrid加速策略算法](https://arxiv.org/abs/1912.00998),其训练启动方式如下: + +```bash +python -B -m paddle.distributed.launch --selected_gpus="0,1,2,3,4,5,6,7" --log_dir=log-slowfast main.py --validate --multigrid -c configs/recognition/slowfast/slowfast_multigrid.yaml +``` + +性能数据如下: + +| 训练策略 | 单个epoch平均耗时/min | 训练总时间/min | 加速比 | +| :------ | :-----: | :------: |:------: | +| Multigrid | 27.25 | 9758(6.7天) | 2.89x | +| Normal | 78.76 | 15438(10.7天) | base | + +速度详细数据说明可参考[加速文档](https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/tutorials/accelerate.md#%E8%AE%AD%E7%BB%83%E7%AD%96%E7%95%A5%E5%8A%A0%E9%80%9F)。 + +## 模型测试 + +可通过如下命令进行模型测试: + +```bash +python -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_slowfast_test main.py --test -c configs/recognition/slowfast/slowfast.yaml -w output/SlowFast/SlowFast_epoch_000196.pdparams +``` + +- 通过 `-w`参数指定待测试模型文件的路径,您可以下载我们训练好的模型进行测试[SlowFast.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast.pdparams) + +- 使用```multi_crop```的方式进行评估,因此评估有一定耗时,建议使用多卡评估,加快评估速度。若使用默认方式进行多卡评估,耗时约4小时。 + +- 模型最终的评估精度会打印在日志文件中。 + +若使用单卡评估,启动方式如下: + +```bash +python -B main.py --test -c configs/recognition/slowfast/slowfast.yaml -w output/SlowFast/SlowFast_epoch_000196.pdparams +``` + + +在Kinetics400数据集下评估精度及权重文件如下: + +| Configs | Acc1 | Acc5 | Weights | +| :---: | :---: | :---: | :---: | +| [slowfast.yaml](../../../../configs/recognition/slowfast/slowfast.yaml) | 74.35 | 91.33 | [slowfast_4x16.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast.pdparams) | +| [slowfast_multigrid.yaml](../../../../configs/recognition/slowfast/slowfast_multigrid.yaml) | 75.84 | 92.33 | [slowfast_8x8.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast_8*8.pdparams) | + +- 由于Kinetics400数据集部分源文件已缺失,无法下载,我们使用的数据集比官方数据少~5%,因此精度相比于论文公布的结果有一定损失。相同数据下,精度已与原实现对齐。 + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/slowfast/slowfast.yaml \ + -p data/SlowFast.pdparams \ + -o inference/SlowFast +``` + +上述命令将生成预测所需的模型结构文件`SlowFast.pdmodel`和模型权重文件`SlowFast.pdiparams`。 + +- 各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/slowfast/slowfast.yaml \ + --model_file inference/SlowFast/SlowFast.pdmodel \ + --params_file inference/SlowFast/SlowFast.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 1.0 +``` + +可以看到,使用在Kinetics-400上训练好的SlowFast模型对`data/example.avi`进行预测,输出的top1类别id为`5`,置信度为1.0。通过查阅类别id与名称对应表`data/k400/Kinetics-400_label_list.txt`,可知预测类别名称为`archery`。 + + +## 参考论文 + +- [SlowFast Networks for Video Recognition](https://arxiv.org/abs/1812.03982), Feichtenhofer C, Fan H, Malik J, et al. +- [A Multigrid Method for Efficiently Training Video Models](https://arxiv.org/abs/1912.00998), Chao-Yuan Wu, Ross Girshick, et al. diff --git a/docs/zh-CN/model_zoo/recognition/stgcn.md b/docs/zh-CN/model_zoo/recognition/stgcn.md new file mode 100644 index 0000000000000000000000000000000000000000..bd8fd884f8b16a3969c48d05fbda6d6c468d1eb9 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/stgcn.md @@ -0,0 +1,136 @@ +[English](../../../en/model_zoo/recognition/stgcn.md) | 简体中文 + +# ST-GCN基于骨骼的行为识别模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +ST-GCN是AAAI 2018提出的经典的基于骨骼的行为识别模型,通过将图卷积应用在具有拓扑结构的人体骨骼数据上,使用时空图卷积提取时空特征进行行为识别,极大地提升了基于骨骼的行为识别任务精度。 + +我们提供了详尽理论及代码讲解,并可使用免费在线GPU算力资源,一键运行的AI Studio Notebook项目, 使用链接:[基于飞桨实现花样滑冰选手骨骼点动作识别大赛baseline](https://aistudio.baidu.com/aistudio/projectdetail/2417717?contributionType=1) + +
    +
    +
    + + +## 数据准备 + +花样滑冰比赛数据下载及准备请参考[花样滑冰数据准备](../../dataset/fsd.md) + +NTU-RGBD数据下载及准备请参考[NTU-RGBD数据准备](../../dataset/ntu-rgbd.md) + + +## 模型训练 + +### 花样滑冰数据集训练 + +- 花样滑冰数据集使用单卡训练,启动命令如下: + +```bash +python3.7 main.py -c configs/recognition/stgcn/stgcn_fsd.yaml +``` + +- 由于赛事未提供验证集数据,因此训练时不做valid。 + +- 您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的,参数用法请参考[config](../../tutorials/config.md)。 + + +### NTU-RGBD数据集训练 + +- NTU-RGBD数据集使用4卡训练,启动命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_stgcn main.py --validate -c configs/recognition/stgcn/stgcn_ntucs.yaml +``` + +- 配置文件`stgcn_ntucs.yaml`为NTU-RGB+D数据集按cross-subject划分方式对应的训练配置。 + + +## 模型测试 + +### 花样滑冰数据集模型测试 + +- 模型测试的启动命令如下: + +```bash +python3.7 main.py --test -c configs/recognition/stgcn/stgcn_fsd.yaml -w output/STGCN/STGCN_epoch_00090.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + +- 评估结果保存在submission.csv文件中,可在[评测官网](https://aistudio.baidu.com/aistudio/competition/detail/115)提交查看得分。 + +模型在花样滑冰数据集上baseline实验精度如下: + +Test_Data| Top-1 | checkpoints | +| :----: | :----: | :---- | +| Test_A | 59.07 | [STGCN_fsd.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/STGCN_fsd.pdparams) | + + +### NTU-RGB+D数据集模型测试 + +- 模型测试的启动命令如下: + +```bash +python3.7 main.py --test -c configs/recognition/stgcn/stgcn_ntucs.yaml -w output/STGCN/STGCN_best.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + +模型在NTU-RGB+D数据集上实验精度如下: + +| split | Top-1 | checkpoints | +| :----: | :----: | :---- | +| cross-subject | 82.28 | [STGCN_ntucs.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/STGCN_ntucs.pdparams) | + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/stgcn/stgcn_fsd.yaml \ + -p data/STGCN_fsd.pdparams \ + -o inference/STGCN +``` + +上述命令将生成预测所需的模型结构文件`STGCN.pdmodel`和模型权重文件`STGCN.pdiparams`。 + +- 各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/fsd10/example_skeleton.npy \ + --config configs/recognition/stgcn/stgcn_fsd.yaml \ + --model_file inference/STGCN/STGCN.pdmodel \ + --params_file inference/STGCN/STGCN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +``` +Current video file: data/fsd10/example_skeleton.npy + top-1 class: 27 + top-1 score: 0.9912770986557007 +``` + +可以看到,使用在花样滑冰数据集上训练好的ST-GCN模型对`data/example_skeleton.npy`进行预测,输出的top1类别id为`27`,置信度为0.9912770986557007。 + + +## 参考论文 + +- [Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition](https://arxiv.org/abs/1801.07455), Sijie Yan, Yuanjun Xiong, Dahua Lin diff --git a/docs/zh-CN/model_zoo/recognition/timesformer.md b/docs/zh-CN/model_zoo/recognition/timesformer.md new file mode 100644 index 0000000000000000000000000000000000000000..ae30f957493e36f48be82d264e41b31ff255b11c --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/timesformer.md @@ -0,0 +1,136 @@ +[English](../../../en/model_zoo/recognition/timesformer.md) | 简体中文 + +# TimeSformer视频分类模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +TimeSformer是基于vision transformer的视频分类模型,具有无卷积、全局感受野、时间序列建模能力强的特点。目前在Kinetics-400数据集上达到了SOTA精度,超过了经典的基于CNN的视频分类模型TSN和TSM以及Slowfast,而且具有更短的训练用时(Kinetics-400数据集训练用时39小时)。**本代码实现的是论文中的时间-空间分离的注意力级联网络**。 + +
    +image-20210628210446041image-20210628210446041 +
    + + +## 数据准备 + +K400数据下载及准备请参考[Kinetics-400数据准备](../../dataset/k400.md) + +UCF101数据下载及准备请参考[UCF-101数据准备](../../dataset/ucf101.md) + + +## 模型训练 + +### Kinetics-400数据集训练 + +#### 下载并添加预训练模型 + +1. 下载图像预训练模型[ViT_base_patch16_224](https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams)作为Backbone初始化参数,或通过wget命令下载 + + ```bash + wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams + ``` + +2. 打开`PaddleVideo/configs/recognition/timesformer/timesformer_k400_videos.yaml`,将下载好的权重存放路径填写到下方`pretrained:`之后 + + ```yaml + MODEL: + framework: "RecognizerTransformer" + backbone: + name: "VisionTransformer" + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- Kinetics400数据集使用8卡训练,训练方式的启动命令如下: + + ```bash + # videos数据格式 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_timesformer main.py --validate -c configs/recognition/timesformer/timesformer_k400_videos.yaml + ``` + +- 开启amp混合精度训练,可加速训练过程,其训练启动命令如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + # videos数据格式 + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_timesformer main.py --amp --validate -c configs/recognition/timesformer/timesformer_k400_videos.yaml + ``` + +- 另外您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的,建议配置文件的命名方式为`模型_数据集名称_文件格式_数据格式_采样方式.yaml`,参数用法请参考[config](../../tutorials/config.md)。 + + +## 模型测试 + +- TimeSformer模型在训练时同步进行验证,您可以通过在训练日志中查找关键字`best`获取模型测试精度,日志示例如下: + + ``` + Already save the best model (top1 acc)0.7258 + ``` + +- 由于TimeSformer模型测试模式的采样方式是速度稍慢但精度高一些的**UniformCrop**,与训练过程中验证模式采用的**RandomCrop**不同,所以训练日志中记录的验证指标`topk Acc`不代表最终的测试分数,因此在训练完成之后可以用测试模式对最好的模型进行测试获取最终的指标,命令如下: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_timesformer main.py --test -c configs/recognition/timesformer/timesformer_k400_videos.yaml -w "output/TimeSformer/TimeSformer_best.pdparams" + ``` + + + 当测试配置采用如下参数时,在Kinetics-400的validation数据集上的测试指标如下: + + | backbone | Sampling method | num_seg | target_size | Top-1 | checkpoints | + | :----------------: | :-------------: | :-----: | :---------: | :---- | :----------------------------------------------------------: | + | Vision Transformer | UniformCrop | 8 | 224 | 77.29 | [TimeSformer_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TimeSformer_k400.pdparams) | + + +- 测试时,TimeSformer视频采样策略为使用Linspace采样:时序上,从待采样视频序列的第一帧到最后一帧区间内,均匀生成`num_seg`个稀疏采样点(包括端点);空间上,选择长边两端及中间位置(左中右 或 上中下)3个区域采样。1个视频共采样1个clip。 + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/timesformer/timesformer_k400_videos.yaml \ + -p data/TimeSformer_k400.pdparams \ + -o inference/TimeSformer +``` + +上述命令将生成预测所需的模型结构文件`TimeSformer.pdmodel`和模型权重文件`TimeSformer.pdiparams`。 + +- 各参数含义可参考[模型推理方法](../../start.md#2-模型推理) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/timesformer/timesformer_k400_videos.yaml \ + --model_file inference/TimeSformer/TimeSformer.pdmodel \ + --params_file inference/TimeSformer/TimeSformer.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9997474551200867 +``` + +可以看到,使用在Kinetics-400上训练好的TimeSformer模型对`data/example.avi`进行预测,输出的top1类别id为`5`,置信度为0.99。通过查阅类别id与名称对应表`data/k400/Kinetics-400_label_list.txt`,可知预测类别名称为`archery`。 + +## 参考论文 + +- [Is Space-Time Attention All You Need for Video Understanding?](https://arxiv.org/pdf/2102.05095.pdf), Gedas Bertasius, Heng Wang, Lorenzo Torresani diff --git a/docs/zh-CN/model_zoo/recognition/tsm.md b/docs/zh-CN/model_zoo/recognition/tsm.md new file mode 100644 index 0000000000000000000000000000000000000000..b3591040a2bb3e6e4ceb76a4fa69c038a0b36306 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/tsm.md @@ -0,0 +1,231 @@ +[English](../../../en/model_zoo/recognition/tsm.md) | 简体中文 + +# TSM视频分类模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [实现细节](#实现细节) +- [参考论文](#参考论文) + +## 模型简介 + +Temporal Shift Module (TSM) 是当前比较受关注的视频分类模型,通过通道移动的方法在不增加任何额外参数量和计算量的情况下,极大地提升了模型对于视频时间信息的利用能力,并且由于其具有轻量高效的特点,十分适合工业落地。 + +我们提供了详尽理论及代码讲解,并可使用免费在线GPU算力资源,一键运行的AI Studio Notebook项目, +使用链接:[Paddle2.1实现视频理解经典模型-TSM](https://aistudio.baidu.com/aistudio/projectdetail/2310889?contributionType=1) + +
    +
    +
    + + + +本代码实现的模型为**基于单路RGB图像**的TSM网络,Backbone采用ResNet-50结构。 + +详细内容请参考ICCV 2019年论文 [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf) + +## 数据准备 + +Kinetics400数据下载及准备请参考[k400数据准备](../../dataset/k400.md) + +UCF101数据下载及准备请参考[ucf101数据准备](../../dataset/ucf101.md) + +## 模型训练 + +### Kinetics-400数据集训练 + +#### 下载并添加预训练模型 + +1. 加载在ImageNet1000上训练好的ResNet50权重作为Backbone初始化参数[ResNet50_pretrain.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams),也可以通过命令行下载 + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams + ``` + +2. 打开`PaddleVideo/configs/recognition/tsm/tsm_k400_frames.yaml`,将下载好的权重路径填写到下方`pretrained:`之后 + + ```yaml + MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTSM" + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- Kinetics400数据集使用8卡训练,frames格式数据的训练启动命令如下: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_k400_frames.yaml + ``` + +- Kinetics400数据集使用8卡训练,videos格式数据的训练启动命令如下: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_k400_videos.yaml + ``` + +- 开启amp混合精度训练,可加速训练过程,其训练启动命令如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 #MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_k400_frames.yaml + ``` + +- 使用amp混合精度训练时,配合`nhwc`的数据格式有更好的加速效果,其训练启动方式如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 #MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_k400_frames_nhwc.yaml + ``` + +- 另外您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的,配置文件命名方式为`模型_数据集_文件格式_数据格式.yaml`,具体参数用法请参考[config](../../tutorials/config.md)。 + + + +### UCF-101数据集训练 + +#### 下载并添加预训练模型 + +1. 加载在Kinetics-400上训练好的权重作为Backbone初始化参数[TSM_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams),也可以通过命令行下载 + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams + ``` + +2. 打开`PaddleVideo/configs/recognition/tsm/tsm_ucf101_frames.yaml`,将下载好的权重路径填写到下方`pretrained:`之后 + + ```yaml + MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNetTSM" + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- UCF-101数据集使用4卡训练,frames格式数据的训练启动命令如下: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_ucf101_frames.yaml + ``` + +- UCF-101数据集使用4卡训练,videos格式数据的训练启动命令如下: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_ucf101_videos.yaml + ``` + +- 开启amp混合精度训练,可加速训练过程,其训练启动命令如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 #MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_ucf101_frames.yaml + ``` + +- 使用amp混合精度训练时,配合`nhwc`的数据格式有更好的加速效果,其训练启动方式如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 #MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_ucf101_frames_nhwc.yaml + ``` + + +## 模型测试 + +- TSM模型在训练时同步进行测试,您可以通过在训练日志中查找关键字`best`获取模型测试精度,日志示例如下: + +```txt +Already save the best model (top1 acc)0.7106 +``` + +- 若需单独运行测试代码,其启动命令如下: + +```bash +python3.7 main.py --test -c configs/recognition/tsm/tsm_k400_frames.yaml -w output/TSM/TSM_best.pdparams +``` +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + +--- + +当测试配置采用如下参数时,在Kinetics-400的validation数据集上的评估精度如下: + +| backbone | Sampling method | Training Strategy | num_seg | target_size | Top-1 | checkpoints | +| :--------: | :---------------: | :-------: | :-----------: | :-----: | :-----------: | :-----------: | +| ResNet50 | Uniform | NCHW | 8 | 224 | 71.06 | [TSM_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams) | + +当测试配置采用如下参数时,在UCF-101的validation数据集上的评估精度如下: + +| backbone | Sampling method | Training Strategy | num_seg | target_size | Top-1 | checkpoints | +| :------: | :-------------: | :-----------------: | :-----: | :---------: | :---: | :---------: | +| ResNet50 | Uniform | NCHW | 8 | 224 | 94.42 | [TSM_ucf101_nchw.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_ucf101_nchw.pdparams) | +| ResNet50 | Uniform | NCHW+AMP | 8 | 224 | 94.40 | [TSM_ucf101_amp_nchw.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_ucf101_amp_nchw.pdparams) | +| ResNet50 | Uniform | NHWC+AMP | 8 | 224 | 94.55 | [TSM_ucf101_amp_nhwc.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_ucf101_amp_nhwc.pdparams) | + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/tsm/tsm_k400_frames.yaml \ + -p data/TSM_k400.pdparams \ + -o inference/TSM +``` + +上述命令将生成预测所需的模型结构文件`TSM.pdmodel`和模型权重文件`TSM.pdiparams`。 + +各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/tsm/tsm_k400_frames.yaml \ + --model_file inference/TSM/TSM.pdmodel \ + --params_file inference/TSM/TSM.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +## 实现细节 + +**数据处理** + +- 模型读取Kinetics-400数据集中的`mp4`数据,首先将每条视频数据划分成`num_seg`段,然后均匀地从每段中抽取1帧图像,得到稀疏采样的`num_seg`张视频帧,再对这`num_seg`帧图像做同样的随机数据增强,包括多尺度的随机裁剪、随机左右翻转、数据归一化等,最后缩放至`target_size`。 + +**训练策略** + +- 采用Momentum优化算法训练,momentum=0.9 +- 采用L2_Decay,权重衰减系数为1e-4 +- 采用全局梯度裁剪,裁剪系数为20.0 +- 总epoch数为50,学习率在epoch达到20、40进行0.1倍的衰减 +- FC层的权重与偏置的学习率分别为为整体学习率的5倍、10倍,且偏置不设置L2_Decay +- Dropout_ratio=0.5 + +**参数初始化** + +- 以Normal(mean=0, std=0.001)的正态分布来初始化FC层的权重,以常数0来初始化FC层的偏置 + +## 参考论文 + +- [TSM: Temporal Shift Module for Efficient Video Understanding](https://arxiv.org/pdf/1811.08383.pdf), Ji Lin, Chuang Gan, Song Han + diff --git a/docs/zh-CN/model_zoo/recognition/tsn.md b/docs/zh-CN/model_zoo/recognition/tsn.md new file mode 100644 index 0000000000000000000000000000000000000000..6564e26af5adc1aee47873d3bc23a24a06ae0902 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/tsn.md @@ -0,0 +1,120 @@ +简体中文 | [English](../../../en/model_zoo/recognition/tsn.md) + +# TSN + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [实现细节](#实现细节) +- [参考论文](#参考论文) + +## 模型简介 + +Temporal Segment Network (TSN) 是视频分类领域经典的基于2D-CNN的解决方案。该方法主要解决视频的长时间行为识别问题,通过稀疏采样视频帧的方式代替稠密采样,既能捕获视频的全局信息,也能去除冗余,降低计算量。核心思想是将每帧的特征做平均融合作为视频的整体特征,再输入分类器进行分类。本代码实现的模型为**基于单路RGB图像**的TSN网络,Backbone采用ResNet-50结构。 + +我们提供了详尽理论及代码讲解,并可使用免费在线GPU算力资源,一键运行的AI Studio Notebook项目,使用链接:[Paddle 2.1实现视频理解经典模型-TSN](https://aistudio.baidu.com/aistudio/projectdetail/2250682?contributionType=1) + +
    +
    +
    + +详细内容请参考ECCV 2016年的论文[Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859) + +## 数据准备 + +PaddleVide提供了在Kinetics-400数据集上训练和测试练脚本。Kinetics-400数据下载及准备请参考[Kinetics-400数据准备](../../dataset/k400.md) + +## 模型训练 + +### Kinetics-400数据集训练 + +#### 下载并添加预训练模型 + +1. 加载在ImageNet1000上训练好的ResNet50权重作为Backbone初始化参数[ResNet50_pretrain.pdparams](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams),也可以通过命令行下载 + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams + ``` + +2. 打开`PaddleVideo/configs/recognition/tsn/tsn_k400_frames.yaml`,将下载好的权重路径填写到下方`pretrained:`之后 + + ```yaml + MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNet" + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- Kinetics-400数据集使用8卡训练,frames格式数据的训练启动命令如下 + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsn main.py --validate -c configs/recognition/tsn/tsn_k400_frames.yaml + ``` + +## 模型测试 + +由于TSN模型测试模式的采样方式是速度稍慢但精度高一些的**TenCrop**,与训练过程中验证模式采用的**CenterCrop**不同,所以训练日志中记录的验证指标`topk Acc`不代表最终的测试分数,因此在训练完成之后可以用测试模式对最好的模型进行测试获取最终的指标,命令如下: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsn main.py --test -c configs/recognition/tsn/tsn_k400_frames.yaml -w "output/TSN/TSN_best.pdparams" +``` + +当测试配置采用如下参数时,在Kinetics-400的validation数据集上的测试指标如下: + +| backbone | Sampling method | Training Strategy | num_seg | target_size | Top-1 | checkpoints | +| :------: | :-------------: | :---------------: | :-----: | :---------: | :---: | ------------------------------------------------------------ | +| ResNet50 | TenCrop | NCHW | 3 | 224 | 69.81 | [TSN_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TSN_k400.pdparams) | +| ResNet50 | TenCrop | NCHW | 8 | 224 | 71.70 | [TSN_k400_8.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TSN_k400_8.pdparams) | +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/tsn/tsn_k400_frames.yaml \ + -p data/TSN_k400.pdparams \ + -o inference/TSN +``` + +上述命令将生成预测所需的模型结构文件`TSN.pdmodel`和模型权重文件`TSN.pdiparams`。 + +各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-模型推理) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/tsn/tsn_k400_frames.yaml \ + --model_file inference/TSN/TSN.pdmodel \ + --params_file inference/TSN/TSN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +## 实现细节 + +**数据处理:** + +- 模型读取Kinetics-400数据集中的`mp4`数据,首先将每条视频数据划分成`num_seg`段,然后均匀地从每段中抽取1帧图像,得到稀疏采样的`num_seg`张视频帧,再对这`num_seg`帧图像做同样的随机数据增强,包括多尺度的随机裁剪、随机左右翻转、数据归一化等,最后缩放至`target_size` + +**训练策略:** + +- 采用Momentum优化算法训练,momentum=0.9 +- 采用L2_Decay,权重衰减系数为1e-4 +- 采用全局梯度裁剪,裁剪系数为40.0 +- 总epoch数为100,学习率在epoch达到40、80进行0.1倍的衰减 +- Dropout_ratio=0.4 + +**参数初始化** + +- TSN模型的卷积层采用Paddle默认的[KaimingNormal](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api/paddle/nn/initializer/KaimingNormal_cn.html#kaimingnormal)和[Constant](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api/paddle/nn/initializer/Constant_cn.html#constant)初始化方法,以Normal(mean=0, std=0.01)的正态分布来初始化FC层的权重,以常数0来初始化FC层的偏置 + +## 参考论文 + +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859), Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoou Tang, Luc Van Gool diff --git a/docs/zh-CN/model_zoo/recognition/tsn_dali.md b/docs/zh-CN/model_zoo/recognition/tsn_dali.md new file mode 100644 index 0000000000000000000000000000000000000000..b9b2d1ed67a22994052fa435d653474501c0b0f2 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/tsn_dali.md @@ -0,0 +1,111 @@ +[English](../../../en/model_zoo/recognition/tsn_dali.md) | 简体中文 + +# TSN模型-DALI训练加速 + +- [方案简介](#方案简介) +- [环境配置](#环境配置) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考文献](#参考文献) + +## 方案简介 +训练速度慢是视频模型训练常见的问题,PaddleVideo使用飞桨2.0的dataloader接口进行数据读取,凭借其优异的多进程加速能力,模型的训练速度可以显著增加。TSN是视频领域常用的2D模型,我们对其训练速度进行了进一步优化。基于[nvidia DALI](https://github.com/NVIDIA/DALI)的GPU解码能力,我们对nvidia DALI进行了二次开发,实现了其均匀分段的帧采样方式,进一步提升了TSN模型的训练速度。 + +### 性能 + +测试环境: +``` +机器: Tesla v100 +显存: 4卡16G +Cuda: 9.0 +单卡batch_size: 32 +``` + +训练速度对比如下: + +| 加速方式 | batch耗时/s | reader耗时/s | ips:instance/sec | 加速比 | +| :--------------- | :--------: | :------------: | :------------: | :------------: | +| DALI | 2.083 | 1.804 | 15.36597 | 1.41x | +| Dataloader: 单卡num_workers=4 | 2.943 | 2.649 | 10.87460| base | +| pytorch实现 | TODO | TODO | TODO | TODO | + + +## 环境配置 + +我们提供docker运行环境方便您使用,基础镜像为: + +``` + huangjun12/paddlevideo:tsn_dali_cuda9_0 +``` + +基于以上docker镜像创建docker容器,运行命令为: + +```bash +nvidia-docker run --name tsn-DALI -v /home:/workspace --network=host -it --shm-size 64g -e NVIDIA_DRIVER_CAPABILITIES=compute,utility,video huangjun12/paddlevideo:tsn_dali_cuda9_0 /bin/bash +``` +- docker中安装好了飞桨2.0.0-rc1版本和我们二次开发后的DALI,创建容器后您可以在docker环境中直接开始tsn模型训练,无需额外配置环境。 + +## 数据准备 + +PaddleVide提供了在K400和UCF101两种数据集上训练TSN的训练脚本。 + +- K400数据下载及准备请参考[K400数据准备](../../dataset/k400.md) + +- UCF101数据下载及准备请参考[UCF101数据准备](../../dataset/ucf101.md) + +## 模型训练 + +### 预训练模型下载 + +加载在ImageNet1000上训练好的ResNet50权重作为Backbone初始化参数,请下载此[模型参数](https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams), +或是通过命令行下载 + +```bash +wget https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams +``` + +并将路径添加到configs中backbone字段下 + +```yaml +MODEL: + framework: "Recognizer2D" + backbone: + name: "ResNet" + pretrained: 将路径填写到此处 +``` + +### 开始训练 + +模型训练的启动命令为: + +```bash +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsn main.py --train_dali -c configs/recognition/tsn/tsn_dali.yaml -o log_level="INFO" +``` + +- 通过`-c`指定模型训练参数配置文件,模型及训练参数配置请参考配置文件```configs/recognition/tsn/tsn_dali.yaml```。 + +- 如若进行finetune,请下载PaddleVideo的已发布模型[comming soon](), 通过`--weights`指定权重存放路径可进行模型finetune。 + +- 您可以自定义修改参数配置,参数用法请参考[config](../../tutorials/config.md)。 + +## 模型测试 + +模型测试方法请参考TSN模型使用文档[模型测试部分](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/model_zoo/recognition/tsn.md#模型测试) + +## 模型推理 + +模型推理方法请参考TSN模型使用文档[模型推理部分](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/model_zoo/recognition/tsn.md#模型推理) + +## 参考论文 + +- [Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859), Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoou Tang, Luc Van Gool + + + + + + + + diff --git a/docs/zh-CN/model_zoo/recognition/videoswin.md b/docs/zh-CN/model_zoo/recognition/videoswin.md new file mode 100644 index 0000000000000000000000000000000000000000..8d6541c1e7a7fec4d4f7ba6a6df612713d0586d8 --- /dev/null +++ b/docs/zh-CN/model_zoo/recognition/videoswin.md @@ -0,0 +1,130 @@ +[English](../../../en/model_zoo/recognition/videoswin.md) | 简体中文 + +# Video-Swin-Transformer视频分类模型 + +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + + +## 模型简介 + +Video-Swin-Transformer是基于Swin Transformer的视频分类模型,其利用了Swin Transformer的多尺度建模和高效局部注意力特性,目前在Kinetics-400数据集上达到了SOTA精度,超过了同为transformer结构的TimeSformer模型。 + + +![VideoSwin](../../../images/videoswin.jpg) + +## 数据准备 + +K400数据下载及准备请参考[Kinetics-400数据准备](../../dataset/k400.md) + + +## 模型训练 + +### Kinetics-400数据集训练 + +#### 下载并添加预训练模型 + +1. 下载图像预训练模型[SwinTransformer_imagenet.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/SwinTransformer_imagenet.pdparams)作为Backbone初始化参数,或通过wget命令下载 + + ```bash + wget https://videotag.bj.bcebos.com/PaddleVideo-release2.2/SwinTransformer_imagenet.pdparams + ``` + +2. 打开`configs/recognition/videoswin/videoswin_k400_videos.yaml`,将下载好的权重存放路径填写到下方`pretrained:`之后 + + ```yaml + MODEL: + framework: "RecognizerTransformer" + backbone: + name: "SwinTransformer3D" + pretrained: 将路径填写到此处 + ``` + +#### 开始训练 + +- Kinetics400数据集使用8卡训练,训练方式的启动命令如下: + + ```bash + # videos数据格式 + python3.7 -u -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin main.py --validate -c configs/recognition/videoswin/videoswin_k400_videos.yaml + ``` + +- 开启amp混合精度训练,可加速训练过程,其训练启动命令如下: + + ```bash + export FLAGS_conv_workspace_size_limit=800 # MB + export FLAGS_cudnn_exhaustive_search=1 + export FLAGS_cudnn_batchnorm_spatial_persistent=1 + # videos数据格式 + python3.7 -u -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin main.py --amp --validate -c configs/recognition/videoswin/videoswin_k400_videos.yaml + ``` + +- 另外您可以自定义修改参数配置,以达到在不同的数据集上进行训练/测试的目的,建议配置文件的命名方式为`模型_数据集名称_文件格式_数据格式_采样方式.yaml`,参数用法请参考[config](../../tutorials/config.md)。 + + +## 模型测试 + +- Video-Swin-Transformer模型在训练时同步进行验证,您可以通过在训练日志中查找关键字`best`获取模型测试精度,日志示例如下: + + ``` + Already save the best model (top1 acc)0.7258 + ``` + +- 由于Video-Swin-Transformer模型测试模式的采样方式是速度稍慢但精度高一些的**UniformCrop**,与训练过程中验证模式采用的**CenterCrop**不同,所以训练日志中记录的验证指标`topk Acc`不代表最终的测试分数,因此在训练完成之后可以用测试模式对最好的模型进行测试获取最终的指标,命令如下: + + ```bash + python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin main.py --test -c configs/recognition/videoswin/videoswin_k400_videos.yaml -w "output/VideoSwin/VideoSwin_best.pdparams" + ``` + + + 当测试配置采用如下参数时,在Kinetics-400的validation数据集上的测试指标如下: + + | backbone | Sampling method | num_seg | target_size | Top-1 | checkpoints | + | :----------------: | :-------------: | :-----: | :---------: | :---- | :----------------------------------------------------------: | + | Swin Transformer | UniformCrop | 32 | 224 | 82.40 | [SwinTransformer_k400.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/VideoSwin_k400.pdparams) | + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/recognition/videoswin/videoswin_k400_videos.yaml \ + -p data/VideoSwin_k400.pdparams \ + -o inference/VideoSwin +``` + +上述命令将生成预测所需的模型结构文件`VideoSwin.pdmodel`和模型权重文件`VideoSwin.pdiparams`。 + +- 各参数含义可参考[模型推理方法](../../start.md#2-模型推理) + +### 使用预测引擎推理 + +```bash +python3.7 tools/predict.py --input_file data/example.avi \ + --config configs/recognition/videoswin/videoswin_k400_videos.yaml \ + --model_file inference/VideoSwin/VideoSwin.pdmodel \ + --params_file inference/VideoSwin/VideoSwin.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +``` +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9999829530715942 +``` + +可以看到,使用在Kinetics-400上训练好的Video-Swin-Transformer模型对`data/example.avi`进行预测,输出的top1类别id为`5`,置信度为0.99。通过查阅类别id与名称对应表`data/k400/Kinetics-400_label_list.txt`,可知预测类别名称为`archery`。 + +## 参考论文 + +- [Video Swin Transformer](https://arxiv.org/pdf/2106.13230.pdf), Ze Liu, Jia Ning, Yue Cao, Yixuan Wei diff --git a/docs/zh-CN/model_zoo/segmentation/asrf.md b/docs/zh-CN/model_zoo/segmentation/asrf.md new file mode 100644 index 0000000000000000000000000000000000000000..8394916d81352b7ab2c714be4dc8f4ce4d65a5f4 --- /dev/null +++ b/docs/zh-CN/model_zoo/segmentation/asrf.md @@ -0,0 +1,142 @@ +[English](../../../en/model_zoo/segmentation/asrf.md) | 简体中文 + +# ASRF 视频动作分割模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + +## 模型简介 + +ASRF模型是在视频动作分割模型MS-TCN上的改进,发表在2021年的WACV上。我们对官方实现的pytorch代码进行复现,在PaddleVideo获得了近似的结果。 + +

    +
    +ASRF Overview +

    + +## 数据准备 + +ASRF的训练数据可以选择50salads、breakfast、gtea三个数据集,数据下载及准备请参考[视频动作分割数据集](../../dataset/SegmentationDataset.md) + +不同于MS-TCN,ASRF模型需要额外的数据构建,脚本流程如下 +```bash +python data/50salads/prepare_asrf_data.py --dataset_dir data/ +``` + +## 模型训练 + +数据准备完毕后,可以通过如下方式启动训练: + +```bash +# gtea数据集 +export CUDA_VISIBLE_DEVICES=3 +python3.7 main.py --validate -c configs/segmentation/asrf/asrf_gtea.yaml --seed 1538574472 +``` + +- 从头开始训练,使用上述启动命令行或者脚本程序即可启动训练,不需要用到预训练模型,视频动作分割模型通常为全卷积网络,由于视频的长度不一,故视频动作分割模型的batch_size字段通常设为1,即不需要批量训练,目前也仅支持**单样本**训练 + +## 模型测试 + +可通过如下方式进行模型测试: + +```bash +python main.py --test -c configs/segmentation/asrf/asrf_gtea.yaml --weights=./output/ASRF/ASRF_split_1.pdparams +``` + +- 指标的具体实现是参考MS-TCN作者[evel.py](https://github.com/yabufarha/ms-tcn/blob/master/eval.py)提供的测试脚本,计算Acc、Edit和F1分数。 + +- pytorch的复现来源于官方提供的[代码库](https://github.com/yiskw713/asrf) + +- 数据集的评估方法采用MS-TCN论文中的折交验证方法,而折交的划分方式与MS-TCN论文中相同。 + +在Breakfast数据集下评估精度如下(采用4折交验证): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 67.6% | 72.4% | 74.3% | 68.9% | 56.1% | +| pytorch | 65.8% | 71.0% | 72.3% | 66.5% | 54.9% | +| paddle | 66.1% | 71.9% | 73.3% | 67.9% | 55.7% | + +在50salads数据集下评估精度如下(采用5折交验证): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 84.5% | 79.3% | 82.9% | 83.5% | 77.3% | +| pytorch | 81.4% | 75.6% | 82.7% | 81.2% | 77.2% | +| paddle | 81.6% | 75.8% | 83.0% | 81.5% | 74.8% | + +在gtea数据集下评估精度如下(采用4折交验证): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 77.3% | 83.7% | 89.4% | 87.8% | 79.8% | +| pytorch | 76.3% | 79.6% | 87.3% | 85.8% | 74.9% | +| paddle | 77.1% | 83.3% | 88.9% | 87.5% | 79.1% | + +给出在gtea数据集下的折交的模型权重 + +Test_Data| F1@0.5 | checkpoints | +| :----: | :----: | :---- | +| gtea_split1 | 72.4409 | [ASRF_gtea_split_1.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ASRF_gtea_split_1.pdparams) | +| gtea_split2 | 76.6666 | [ASRF_gtea_split_2.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ASRF_gtea_split_2.pdparams) | +| gtea_split3 | 84.5528 | [ASRF_gtea_split_3.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ASRF_gtea_split_3.pdparams) | +| gtea_split4 | 82.6771 | [ASRF_gtea_split_4.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ASRF_gtea_split_4.pdparams) | + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/segmentation/asrf/asrf_gtea.yaml \ + -p data/ASRF_gtea_split_1.pdparams \ + -o inference/ASRF +``` + +上述命令将生成预测所需的模型结构文件`ASRF.pdmodel`和模型权重文件`ASRF.pdiparams`。 + +- 各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +输入预测模型的txt文件为需要预测的文件列表,如: +``` +S1_Cheese_C1.npy +S1_CofHoney_C1.npy +S1_Coffee_C1.npy +S1_Hotdog_C1.npy +... +``` + +```bash +python3.7 tools/predict.py --input_file data/gtea/splits/test.split1.bundle \ + --config configs/segmentation/asrf/asrf_gtea.yaml \ + --model_file inference/ASRF/ASRF.pdmodel \ + --params_file inference/ASRF/ASRF.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +```bash +result write in : ./inference/infer_results/S1_Cheese_C1.txt +result write in : ./inference/infer_results/S1_CofHoney_C1.txt +result write in : ./inference/infer_results/S1_Coffee_C1.txt +result write in : ./inference/infer_results/S1_Hotdog_C1.txt +result write in : ./inference/infer_results/S1_Pealate_C1.txt +result write in : ./inference/infer_results/S1_Peanut_C1.txt +result write in : ./inference/infer_results/S1_Tea_C1.txt +``` + + +## 参考论文 + +- [Alleviating Over-segmentation Errors by Detecting Action Boundaries](https://arxiv.org/pdf/2007.06866v1.pdf), Yuchi Ishikawa, Seito Kasai, Yoshimitsu Aoki, Hirokatsu Kataoka diff --git a/docs/zh-CN/model_zoo/segmentation/cfbi.md b/docs/zh-CN/model_zoo/segmentation/cfbi.md new file mode 100644 index 0000000000000000000000000000000000000000..fac110bce3cbaee3a47c15df712e431814f89c33 --- /dev/null +++ b/docs/zh-CN/model_zoo/segmentation/cfbi.md @@ -0,0 +1,49 @@ +[English](../../../en/model_zoo/segmentation/cfbi.md) | 简体中文 + +# CFBI视频分割模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型测试](#模型测试) +- [参考论文](#参考论文) + + +## 模型简介 + +CFBI是百度在ECCV 2020提出的视频目标分割模型,该模型基于前背景整合的协作式方法,将前景目标对象与背景对象的嵌入特征进行对比,从而提升视频分割的效果。给定参考帧(第一帧)和前一帧的图像和目标分割,模型会预测出当前帧的分割。 + +
    +
    +
    + + +## 数据准备 + +DAVIS数据下载及准备请参考[DAVIS数据准备](../../../../applications/Ma-Net/dataloaders/DAVIS2017_cn.md) + + +## 模型测试 + +- 测试启动脚本如下: + +```bash +python3.7 main.py --test -c configs/segmentation/cfbip_davis.yaml -w CFBIp_davis.pdparams +``` + +- 通过`-c`参数指定配置文件,通过`-w`指定权重存放路径进行模型测试。 + +- 运行上述命令,会将结果保存在配置文件中指定的`result_root`下,获取数值评估指标,请使用[davis2017-evaluation工具](https://github.com/davisvideochallenge/davis2017-evaluation)。 + +DAVIS数据集测试精度: + +| J&F-Mean | J-Mean | J-Recall | J-Decay | F-Mean | F-Recall | F-Decay | checkpoints | +| :------: | :-----: | :----: | :----: | :----: | :----: | :----: | :----: | +| 0.823 | 0.793 | 0.885 | 0.083 | 0.852 | 0.932 | 0.100 | [CFBIp_r101_davis.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/CFBIp_r101_davis.pdparams) | + + +## 参考论文 + +- [Collaborative Video Object Segmentation by Foreground-Background Integration](https://arxiv.org/abs/2003.08333), Zongxin Yang, Yunchao Wei, Yi Yang diff --git a/docs/zh-CN/model_zoo/segmentation/mstcn.md b/docs/zh-CN/model_zoo/segmentation/mstcn.md new file mode 100644 index 0000000000000000000000000000000000000000..fac5b7b8764be40801e2c2ba3be0fd38f429a888 --- /dev/null +++ b/docs/zh-CN/model_zoo/segmentation/mstcn.md @@ -0,0 +1,131 @@ +[English](../../../en/model_zoo/segmentation/mstcn.md) | 简体中文 + +# MS-TCN 视频动作分割模型 + +--- +## 内容 + +- [模型简介](#模型简介) +- [数据准备](#数据准备) +- [模型训练](#模型训练) +- [模型测试](#模型测试) +- [模型推理](#模型推理) +- [参考论文](#参考论文) + +## 模型简介 + +MS-TCN模型是视频动作分割模型的经典的模型,发表在2019年的CVPR上。我们对官方实现的pytorch代码进行一些优化,在PaddleVideo获得了更高精度的结果。 + +

    +
    +MS-TCN Overview +

    + +## 数据准备 + +MS-TCN的训练数据可以选择50salads、breakfast、gtea三个数据集,数据下载及准备请参考[视频动作分割数据集](../../dataset/SegmentationDataset.md) + +## 模型训练 + +数据准备完毕后,可以通过如下方式启动训练: + +```bash +# gtea数据集 +export CUDA_VISIBLE_DEVICES=3 +python3.7 main.py --validate -c configs/segmentation/ms_tcn/ms_tcn_gtea.yaml --seed 1538574472 +``` + +- 从头开始训练,使用上述启动命令行或者脚本程序即可启动训练,不需要用到预训练模型,视频动作分割模型通常为全卷积网络,由于视频的长度不一,故视频动作分割模型的batch_size字段通常设为1,即不需要批量训练,目前也仅支持**单样本**训练 + +## 模型测试 + +可通过如下方式进行模型测试: + +```bash +python main.py --test -c configs/segmentation/ms_tcn/ms_tcn_gtea.yaml --weights=./output/MSTCN/MSTCN_split_1.pdparams +``` + +- 指标的具体实现是参考MS-TCN作者[evel.py](https://github.com/yabufarha/ms-tcn/blob/master/eval.py)提供的测试脚本,计算Acc、Edit和F1分数。 + +- 数据集的评估方法采用MS-TCN论文中的折交验证方法,而折交的划分方式与MS-TCN论文中相同。 + +在Breakfast数据集下评估精度如下(采用4折交验证): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 66.3% | 61.7% | 48.1% | 48.1% | 37.9% | +| paddle | 65.2% | 61.5% | 53.7% | 49.2% | 38.8% | + +在50salads数据集下评估精度如下(采用5折交验证): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 80.7% | 67.9% | 76.3% | 74.0% | 64.5% | +| paddle | 81.1% | 71.5% | 77.9% | 75.5% | 66.5% | + +在gtea数据集下评估精度如下(采用4折交验证): + +| Model | Acc | Edit | F1@0.1 | F1@0.25 | F1@0.5 | +| :---: | :---: | :---: | :---: | :---: | :---: | +| paper | 79.2% | 81.4% | 87.5% | 85.4% | 74.6% | +| paddle | 76.9% | 81.8% | 86.4% | 84.7% | 74.8% | + +给出在gtea数据集下的折交的模型权重 + +Test_Data| F1@0.5 | checkpoints | +| :----: | :----: | :---- | +| gtea_split1 | 70.2509 | [MSTCN_gtea_split_1.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MSTCN_gtea_split_1.pdparams) | +| gtea_split2 | 70.7224 | [MSTCN_gtea_split_2.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MSTCN_gtea_split_2.pdparams) | +| gtea_split3 | 80.0 | [MSTCN_gtea_split_3.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MSTCN_gtea_split_3.pdparams) | +| gtea_split4 | 78.1609 | [MSTCN_gtea_split_4.pdparams](https://videotag.bj.bcebos.com/PaddleVideo-release2.2/MSTCN_gtea_split_4.pdparams) | + + +## 模型推理 + +### 导出inference模型 + +```bash +python3.7 tools/export_model.py -c configs/segmentation/ms_tcn/ms_tcn_gtea.yaml \ + -p data/MSTCN_gtea_split_1.pdparams \ + -o inference/MSTCN +``` + +上述命令将生成预测所需的模型结构文件`MSTCN.pdmodel`和模型权重文件`MSTCN.pdiparams`。 + +- 各参数含义可参考[模型推理方法](https://github.com/PaddlePaddle/PaddleVideo/blob/release/2.0/docs/zh-CN/start.md#2-%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86) + +### 使用预测引擎推理 + +输入预测模型的txt文件为需要预测的文件列表,如: +``` +S1_Cheese_C1.npy +S1_CofHoney_C1.npy +S1_Coffee_C1.npy +S1_Hotdog_C1.npy +... +``` + +```bash +python3.7 tools/predict.py --input_file data/gtea/splits/test.split1.bundle \ + --config configs/segmentation/ms_tcn/ms_tcn_gtea.yaml \ + --model_file inference/MSTCN/MSTCN.pdmodel \ + --params_file inference/MSTCN/MSTCN.pdiparams \ + --use_gpu=True \ + --use_tensorrt=False +``` + +输出示例如下: + +```bash +result write in : ./inference/infer_results/S1_Cheese_C1.txt +result write in : ./inference/infer_results/S1_CofHoney_C1.txt +result write in : ./inference/infer_results/S1_Coffee_C1.txt +result write in : ./inference/infer_results/S1_Hotdog_C1.txt +result write in : ./inference/infer_results/S1_Pealate_C1.txt +result write in : ./inference/infer_results/S1_Peanut_C1.txt +result write in : ./inference/infer_results/S1_Tea_C1.txt +``` + +## 参考论文 + +- [MS-TCN: Multi-Stage Temporal Convolutional Network for Action Segmentation](https://arxiv.org/pdf/1903.01945.pdf), Y. Abu Farha and J. Gall. diff --git a/docs/zh-CN/tools.md b/docs/zh-CN/tools.md new file mode 100644 index 0000000000000000000000000000000000000000..50d5c7e47cb6261d70bac312211eb88675a2210c --- /dev/null +++ b/docs/zh-CN/tools.md @@ -0,0 +1,19 @@ +简体中文 | [English](../en/tools.md) + +# 小工具 + +这篇文档主要介绍PaddleVideo的一些小工具 + +## 统计 Params + +```shell +python3.7 tools/summary.py -c configs/recognition/tsm/tsm.yaml +``` + +## 统计FLOPS + +```shell +python3.7 tools/summary.py -c configs/recognition/tsm/tsm.yaml --FLOPs +``` + +## 测试导出模型 coming soon diff --git a/docs/zh-CN/tutorials/I3D.md b/docs/zh-CN/tutorials/I3D.md new file mode 100644 index 0000000000000000000000000000000000000000..bd2c32a540cefcee43637a2faf87217e14c75ba4 --- /dev/null +++ b/docs/zh-CN/tutorials/I3D.md @@ -0,0 +1,90 @@ +# I3D + +## 简介 +本文提出了一种基于 2D-ConvNet 扩展的双流膨胀模型 I3D,作者将图像分类的 2D 卷积网络的滤波器和池化核扩展到 3D 中,使得从视频中学习无缝时空特征提取器成为可能。 + +## 重点贡献 +* 提出了 Kinetics 数据集 +* 提出了双流 3D 卷积模型 + +## kinetics数据集 +Kinetics 数据集有 400 个人体动作类别,每个类别有 400 多个视频片段,这些数据来自真实有挑战的 YouTube 视频。数据集包括的动作大类别有: +1. 单人动作:绘画、饮酒、笑 +2. 人与人之间的动作:拥抱、亲吻、握手 +3. 人与物之间的动作:打开礼物、洗碗、除草 +4. 需要细分的动作,比如不同类型的游泳 +5. 侧重于物体的信息,比如不同类型的乐器 + +## 动机 +图像领域有一个超大规模的 ImageNet 数据集,很多图像任务采用的都是 ImageNet 预训练模型,并且取得了不错了效果。在视频领域中,如果我们有一个超大规模的数据集,将在该数据集上预训练好的的动作分类模型应用到其他时序任务或不同的数据集上是否会有类似性能的提升。为了验证这个猜想,作者将在 Kinetics 上的预训练模型应用到 HMDB-51 和 UCF-101 这种小的数据集上。实验结果表明,性能总是会得到提升,提升的程度与模型的结构有关。 + +基于此发现,作者提出了 I3D,基于 InceptionV1 的 I3D 模型在经过 Kinetics 预训练后,其性能远远超过了当时最先进的水平。 + +## 主要工作 +1. 在 Kinetics 数据集上做模型预训练,将预训练模型应用到 HMDB-51 和 UCF101 数据集上,验证大规模视频数据的有效性; +2. 基于 2D-ConvNet,提出了新的行为识别模型 I3D; + +## 行为识别方法分析 +当前,行为识别模型主要的不同点: +1. 卷积和层运算使用的是 2D 核还是 3D 核; +2. 网络的输入仅仅包含的是 RGB 视频还是也包括预计算的光流; +3. 在 2D-ConvNet 情况下,帧之间的信息如何传播; + +## 模型分析 +作者比较和研究了一些模型,这些模型有的基于 2D-ConvNet,有的基于 3D-ConvNet。之前基于 3D-ConvNet 的模型由于可用的训练数据少,网络结构相对较浅。于是本文将非常深的 2D-ConvNet 图像分类网络膨胀为 3D-ConvNet 的时空特征提取网络,同时将其作为 two-stream 框架的主干网络。由于之前的 2D-ConvNet 网络本身比较深,又可以使用 2D-ConvNet 的参数初始化相应 3D-ConvNet 的网络,因此可以解决之前训练数据不足的问题。 + +这里作者分析五种网络结构,如下图所示。 + +
    +

    网络结构
    +

    + +### The Old I: ConvNet+LSTM +将图像分类模型应用到视频分析上的一个直接想法是,把视频中的每帧看作一张独立的图像,提取每张图像后,对整个视频求均值。但这样做完全忽略了视频中的时序信息,一个比较合理的方法是在网络的末端添加一个 LSTM 层,用于学习视频帧之间的时序关系。因此 ConvNet+LSTM 的文章在 InceptionV1 的最后一个 average-pooling 后面加了一个包含 512 个隐含节点的 LSTM,最后接了一个用于分类的全连接层。 + +### The Old 2 :3D ConvNets +3D-ConvNets 是建模视频任务一个很自然的想法,与标准的卷积网络相比,增加了一个时空维度。由于时空维度的增加,使得 3D-ConvNets 比 2D-ConvNets 有更多的参数,增加了网络训练的困难。此外,网络结构是 3D 的,无法直接复用 2D 模型的参数。 + +### The Old III: Two-Stream Networks +ConvNet+LSTM 的模型结构仅仅捕获高层信息的变化,对于帧和帧之间在底层动作信息的捕获是不够的,并且底层动作信息在行为识别中是非常重要的。于是一些研究人员提出了 Two-Stream 网络,Two-Stream 分为两路,一路用于提取 RGB 信息,一路用于提取光流信息;这样的网络设计对空间维度和时间维度的提取都比较好。这种方法比较容器训练和测试,并且在公开数据集上取得了比较不错的效果。 + +> Two-Stream 中的两个模型是分开训练的。 + +### The New: Two-Stream Inflated 3D ConvNets +#### 1 inflating 2D ConvNets into 3D +把在 ImageNet 上表现好的 2D 模型直接扩展为 3D 模型,具体做法是将 2D 结构中的二维卷积核与池化核扩展一维,由之前的 变成 。 + +#### Bootstrapping 3D filters from 2D Filters +作者将一帧图像沿着时间轴复制 N 次,将其变为一个 boring video。为了保证在这个 boring video 上做卷积操作后池化激活与原始图像经过卷积操作后的池化激活相同,这里用到的方法是将 2D 卷积核 在时间维度上重复 N 次,得到 ,之后再除以 N 的方式,确保滤波器的响应是相同的。 + +#### Pacing receptive field growth in space,time and network depth +将 2D-ConvNet 扩展到 3D-ConvNet 后,如何设置时间维度上的 kernel。目前几乎所有的图像相关的模型都平等的看待空间维度中的水平和垂直两个方向,两个方向上的 kernel 是相等的。当加入时间维度后,再使用完全对称的感受野并不是最好的选择,应该考虑帧速率和图像尺寸。 +* 如果时间维度比空间维度增长过快,可能会影响物体边缘信息,从而破坏物体的特征检测; +* 如果时间维度比空间维度增长过慢,可能无法很好的捕捉场景的动态信息,从而影响对动作的检测; + +因此,作者对 InceptinV1 进行扩展时,大多数保持了对称特征,如第一个卷积核由 变成了 ,stride 也从原来的 (2,2) 变成了 (2,2,2);只对少数做了改变,如前面两个 max-pool,并不是 ,而是 ,这样能够比较好的保留时间维度的信息,以及最后的 avg-pool 不是 而是 。 + +
    +

    网络扩展
    +

    + + +#### Two 3D Streams +虽然,3D-ConvNet 已经能够比较好的提取视频中的动作特征,但带有光流的 two-stream 结构对动作识别依然有巨大的帮助。因此作者将 3D-ConvNet 设计成 two-stream 形式,训练时左右两个网络分开训练,预测时对两个网络的预测结果做均值。 + +## 实验结果 +在 UCF-101,HMDB-51 或 Kinetics 上进行训练和测试时的分类准确度。 + +
    +

    实验结果1
    +

    + +从 ImageNet 预训练或没有进行预训练模型在 Kinetics 上的表现。 + +
    +

    实验结果2
    +

    + + +## 参考 +[Quo Vadis, Action Recognition? A New Model and the Kinetics Dataset](https://arxiv.org/abs/1705.07750) diff --git a/docs/zh-CN/tutorials/SlowFast.md b/docs/zh-CN/tutorials/SlowFast.md new file mode 100644 index 0000000000000000000000000000000000000000..b80e8d12d3a5a696f5690be3db77e483c0e9f81d --- /dev/null +++ b/docs/zh-CN/tutorials/SlowFast.md @@ -0,0 +1,172 @@ +# SlowFast + +## 背景 +SlowFast 由 Facebook FAIR 的何恺明团队提出,用于视频识别。SlowFast 包含两条路径: +* Slow pathway +* Fast pathway + +Slow pathway 运行低帧率,用于捕捉空间语义信息;Fast pathway 运行高帧率,获取精确的时间运动信息。通过降低通道数量,Fast pathway 分支可以变成轻量的网络,同时也能够学到视频中有用的时域信息。SlowFast 在没有任何预训练的情况下,在 Kinetics 数据集上的准确率达到了 79.0%。 + +## 动机 +SlowFast 受到灵长类视觉系统中视网膜神经节细胞的生物学研究的启发。研究发现,这些细胞中约80%的都是P-cell,约15~20% 是 M-cell。M-cell 以较高的时间频率工作,能够对快速的时间变化作出响应,但是对空间细节和颜色不敏感。P-cell 则提供良好的空间细节和颜色信息,但时间分辨率较低,对刺激反应比较慢。 + +SlowFast 与此相似: +* SlowFast 有两条路径,分别处理低帧率和高帧率; +* Fast pathway 用于捕捉快速变化的动作,单涉及到的细节信息较少,与M-cell类似; +* Fast pathway 是轻量的,与M-cell的占比类似。 + +## 简介 +在图像识别领域,对称处理图像 I(x,y) 中两个空间维度 x 和 y 是常见的做法,自然图像的统计也证明了其合理性。这是由于自然图像具有第一近似各向同性(所有方向具有相同的可能性)和平移不变性。但对于视频信号 I(x,y,t)来说,并不是所有的时空方向都有相同的可能性。因此不能像时空卷积那样对称地处理空间和时间。此时需要将网络结构分开,分别处理空间结构和时间事件。 + +视觉内容的类别空间语义变化通常比较缓慢。比如,挥手不会在这个动作进行期间改变“手”的类别;一个人从走路变为跑步,识别结果也一直是“人”。因此类别语义的识别(以及颜色、纹理、光照等)可以以较慢的速度刷新。另一方面,正在执行的动作比其主体识别变化的速度要快得多,如拍手、挥手、摇摆、走路或跳跃。因此需要用较快的帧率刷新(高时间分辨率),来对快速变化的动作进行建模。 + +## 思路 +基于上述想法作者提出了一种用于视频识别的双路径模型 SlowFast 。 + +

    +
    +网络结构 +

    + + +如上图所示,一条路径用于捕获图像或稀疏帧提供的语义信息,以低帧率运行,刷新速度慢。另一条路径用于捕获快速变化的动作,刷新速度快、时间分辨率高,该路径是轻量级的,仅占整体计算量的20%。这是由于这条路径通道较少,处理空间信息的能力较差,但空间信息可以由第一个路径以简洁的方式来处理。 + +依据两条路径运行的帧率高低不同,作者将第一条路径称为“Slow pathway”;第二条路径称为“Fast pathway”;两条路径通过横向连接进行融合。 + +## SlowFast +SlowFast 的两个分支以不同的速率运行,作者通过使用两个分支模拟生物学上的大小细胞。 + +### Slow Pathway +Slow pathway 可以是任意在视频片段上做时空卷积的模型,如时空残差网络,C3D,I3D,Non-local网络等。Slow pathway 的关键之处在于对视频帧进行采样时,时间步长 较大,也就是说,只处理 帧中的一帧。这里,作者建议 的取值为 16,对于 30fps 的视频,差不多每秒采样 2 帧。如果 Slow pathway 采样的帧数是 T,那么原始视频片段的长度为 。 + +### Fast Pathway +#### 高帧率 +Fast pathway 的目的为了在时间维度上有良好的特征表示,Fast pathway 的时间步长 较小,其中 是 Slow pathway 和 Fast pathway 之间帧率比,作者建议 的取值为 8。由于两条路径在同一个视频上进行操作,因此 Fast pathway 采样到的帧数量为 ,比 Slow pathway 密集 倍。 + +#### 高时间分辨率特征 +Fast pathway 具有高输入分辨率,同时整个网络结构会运行高分辨率特征。在最后的分类全局池化层之前作者没有采用时间下采样层,因此在特征张量在时间维度上一直保持在 帧。 + +#### 低通道容量 +Fast pathway 是一个与 Slow pathway 相似的卷积网络,但通道数只有 Slow pathway 的 倍,其中 ,作者建议 的取值为 。这是的 Fast pathway 比 Slow pathway 的计算更高效。 + +低通道容量可以理解为表示空间语义信息的能力较弱。由于 Fast pathway 的通道数更少,因此 Fast pathway 的空间建模能力应该弱于 Slow pathway。但 SlowFast 的实验结果表明这反而是有利的,它弱化了空间建模能力,却增强了时间建模能力。 + +### 横向连接 +作者通过横向连接对两条路径的信息进行融合,使得 Slow pathway 知道 Fast pathway 在学习什么。作者在两条路径中的每个“阶段”上使用一个横向连接,由于两条路径的时间维度不同,因此在进行横向连接时需要通过变换对两条路径的维度进行匹配。最后,将两条路径的输出进行全局平均池化,并将池化后的特征拼接在一起作为全连接分类器层的输入。 + +### 实例化 +SlowFast 模型的思想是通用的,可以用不同的主干网络来实现。如下图所示是一个 SlowFast 实例化的例子,其中黄色是通道数量,绿色是时序帧分辨率。 + +

    +
    +实例化 +

    + +作者用表示时空尺度,其中 T 是时间长度,S 是正方形裁剪区域的宽和高。 + +#### Slow Pathway +Slow pathway 是一个具有时间步长的 3D ResNet,网络时间维度的输入帧数 T = 4,从 64 帧视频片段中稀疏采样得到,时间步长 。作者没有采用时间下采样在实例化中,由于当输入步长较大时,这样做是有害的。 + +Slow pathway 与 C3D/I3D 模型不同,从 conv_1 到 res_3 的滤波器本质上都是2D卷积核,只有 res_4 和 res_5 使用的是非退化时间卷积。之所以采用这种设计是由于作者通过实验发现,在早期层使用时间卷积会降低准确率。作者认为是由于当物体快速移动且时间步长较大时,在一个时间感受野内的相关性就很小,除非空间感受野也足够地大。 + +#### Fast Pathway +Fast pathway 的时间分辨率较高,通道容量较低。Fast pathway 的每个模块中都使用了非退化时间的卷积,并且没有使用时间下采样层。之所以这样设计是因为作者发现 Fast pathway 的时间卷积有很好的时间分辨率,可以捕捉细节动作。 + +#### 横向连接 +横向连接是从 Fast pathway 到 Slow pathway,在融合之前需要保证两个维度是匹配的,Slow pathway 的特征维度是 ,Fast pathway 的特征维度是 ,在连接方案上作者进行了如下实验: +* Time-to-channel:对进行变形和转置,得到 ,也就是说将所有的 帧放入一帧的多个通道内。 +* Time-strided sampling:每 帧,采样一帧,所以 就变成了 。 +* Time-strided convolution:使用 3D 卷积,卷积核大小是 ,输出通道数为 ,步长为。 + +## PaddleVideo +PaddleVideo 关于采样的核心代码 +```python +class PackOutput(object): + """ + In slowfast model, we want to get slow pathway from fast pathway based on + alpha factor. + Args: + alpha(int): temporal length of fast/slow + """ + def __init__(self, alpha): + self.alpha = alpha + + def __call__(self, results): + fast_pathway = results['imgs'] + + # sample num points between start and end + slow_idx_start = 0 + slow_idx_end = fast_pathway.shape[0] - 1 + slow_idx_num = fast_pathway.shape[0] // self.alpha # slow 的采样数量 + # 在区间[slow_idx_start, slow_idx_end] 内均匀采样 + slow_idxs_select = np.linspace(slow_idx_start, slow_idx_end, + slow_idx_num).astype("int64") + slow_pathway = fast_pathway[slow_idxs_select] # 取出采样到的图片 + + # T H W C -> C T H W. + slow_pathway = slow_pathway.transpose(3, 0, 1, 2) # 对维度做转换 + fast_pathway = fast_pathway.transpose(3, 0, 1, 2) + + # slow + fast + frames_list = [slow_pathway, fast_pathway] + results['imgs'] = frames_list + return results +``` + +PaddleVideo 中关于特征融合的核心代码 +```python +class FuseFastToSlow(paddle.nn.Layer): + """ + Fuses the information from the Fast pathway to the Slow pathway. Given the + tensors from Slow pathway and Fast pathway, fuse information from Fast to + Slow, then return the fused tensors from Slow and Fast pathway in order. + """ + def __init__(self, + dim_in, + fusion_conv_channel_ratio, + fusion_kernel, + alpha, + eps=1e-5, + norm_module=paddle.nn.BatchNorm3D): + """ + Args: + dim_in (int): the channel dimension of the input. + fusion_conv_channel_ratio (int): channel ratio for the convolution + used to fuse from Fast pathway to Slow pathway. + fusion_kernel (int): kernel size of the convolution used to fuse + from Fast pathway to Slow pathway. + alpha (int): the frame rate ratio between the Fast and Slow pathway. + eps (float): epsilon for batch norm. + """ + super(FuseFastToSlow, self).__init__() + fan = (dim_in * fusion_conv_channel_ratio) * (fusion_kernel * 1 * 1) + initializer_tmp = get_conv_init(fan) + + self._conv_f2s = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_in * fusion_conv_channel_ratio, + kernel_size=[fusion_kernel, 1, 1], + stride=[alpha, 1, 1], + padding=[fusion_kernel // 2, 0, 0], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self._bn = norm_module(num_features=dim_in * fusion_conv_channel_ratio, + epsilon=eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + def forward(self, x): + x_s = x[0] + x_f = x[1] + fuse = self._conv_f2s(x_f) + fuse = self._bn(fuse) + fuse = F.relu(fuse) + x_s_fuse = paddle.concat(x=[x_s, fuse], axis=1, name=None) + + return [x_s_fuse, x_f] +``` + + + +## 参考 +[SlowFast Networks for Video Recognition](https://arxiv.org/abs/1812.03982) diff --git a/docs/zh-CN/tutorials/TSM.md b/docs/zh-CN/tutorials/TSM.md new file mode 100644 index 0000000000000000000000000000000000000000..cae140867805a231711e28a2d1989a35d4577109 --- /dev/null +++ b/docs/zh-CN/tutorials/TSM.md @@ -0,0 +1,75 @@ +# TSM模型原理及PaddleVideo实践 +# 1. 背景与动机 +目前互联网视频数据日益增多,用户观看短视频、小视频的时长也迅速增长,如何对海量的视频资源快速准确地分析、处理、归类是一个亟待解决的问题。视频理解技术可以多维度解析视频内容,理解视频语义,自动分类打标签,极大节省人工审核效率,节约成本;同时实现精准用户推荐,提升体验效果。 +本文将给大家介绍视频理解领域的经典模型**TSM (Temporal Shift Module)**, TSM是由**MIT**和**IBM Watson AI Lab**的`Ji Lin,Chuang Gan和SongHan`等人提出的通过时间位移模拟3D建模达到效果和性能的平衡,提高视频理解能力的模块。 + + + +跟TSM最相关的视频理解模型当属Limin Wang等人在ECCV2016上发表的Temporal Segment Network (TSN)了。TSN模型从视频中采样N帧图像并通过最简单直接地对N帧图像分类结果进行平均的方式进行时序信息融合,取得了当时State-of-the-art的性能,并得到大规模的应用。考虑到TSN模型对时序信息的建模不够充分,以I3D,S3D, P3D等为代表的一系列工作通过3D卷积进行端到端联合时空建模,这一系列工作尽管能捕获时空特征,但是相比TSN,由2D卷积到3D卷积不可避免地引入了额外计算量。TSM巧妙的通过时间维度特征map移位的想法,理论上用零额外计算开销达到了不同帧之间特征融合联合建模的目的。 + +论文传送门: [Temporal Shift Module for Efficient VideoUnderstanding](https://arxiv.org/pdf/1811.08383v2.pdf) + +先看一下下图的例子:如果图片分别从左往右播放和从右往左播放,测试者会给出不同但是正确的理解结果,说明对视频的理解强依赖于视频的时序关系,你猜对了!这就是TSM提出的动机,即捕捉视频的时间信息。 +

    +
    +

    +看起来好像很有意思,我们下面继续深入解析一下TSM的核心模块。 + +# 2. TSM关键技术介绍 + +在传统的图片分析的基础上,视频分析需要研究者补充关于时间信息(temporal information)的建模结构。目前,2D CNN和3D CNN是视频理解中最常用的两个方法:使用2D CNN 模型运算量少但会丧失部分时间信息;而使用3D CNN虽然效果好但运算量极大。面对这样的情况,MIT和IBM Watson AI Lab的Ji Lin,Chuang Gan和Song Han等人提出了Temporal Shift Module (TSM)模块。他们将时间位移模块嵌入2D CNN,从而可以在不添加任何额外的计算量和参数的情况下,轻松地达到与3D CNN效果相当的视频理解能力。 +

    +
    +

    +上图中矩阵的行和列分别表示特征图中的temporal和channel维度。在TSM模块中,将一部分的channel在temporal维度上向前位移一步,一部分的channel在temporal维度上向后位移一步,位移后的空缺补零。通过这种方式在特征图中引入temporal维度上的上下文交互,通过通道移动操作可以使得在当前帧中包含了前后两帧的通道信息,这样再进行2D卷积操作就能像3D卷积一样直接提取视频的时空信息, +提高了模型在时间维度上的建模能力。在此基础上,研发人员将模块进一步细分为适合在线视频使用的TSM模块和适合离线视频使用的TSM模块。 +

    +
    +

    + +双向(bi-direction)的TSM模块可获取过去和未来的时空信息,适合高吞吐量的离线视频使用;而单向(uni-direction)的TSM模块仅可比对现在和过去的时空信息,适用于低延迟在线视频的识别。 +此外,论文中作者还考虑了TSM模块插入的位置,对比了两种TSM插入方式:**Residual tsm** 和 **In-place tsm**,作者发现使用**Residual tsm**方式会比 **In-place tsm** 的方式效果更好,文中作者解释为**In-place tsm** 会影响模型对空间信息的提取。 +

    +
    +

    + + +好了,TSM模块基本原理搞清楚了是不是**So Easy !!!**,接下来问题来了,代码该如何实现呢? + +# 3. 关键代码解析 + +原理搞清楚了,下面来看看代码如何实现,首先我们来看看torch版本如何实现的,呃呃呃...,不好意思torch框架并未提供TSM的API,我们只能自己动手啦,具体实现代码如下图所示: +

    +
    +

    + +这意味着你只需要在TSN的代码基础上添加4行代码就能将准确率在Something-Something这样的数据集上**翻上一倍!!!** 是不是简单高效的模型 ?不得不向大佬低头! + +But..., + + +飞桨框架充分考虑到广大用户的需求已经为各位童鞋实现了TSM的OP +

    +
    +

    + +所以各位童鞋再也不用自己实现了,**直接调用就可以啦!!!,直接调用就可以啦!!!,直接调用就可以啦!!!**,重要的事情讲三遍, + +是不是以为事情到这里就结束啦 ? 唉! **Too young Too simple !!!** + +我们在此基础上还对其进行了性能优化,在降低显存消耗的同时,可以提速5倍以上,详细信息可以参考[加速文档](./accelerate.md) + +下面我们来看看使用飞桨如何实现TSM: + +`import paddle.nn.functional as F` + + +`shifts = F.temporal_shift(inputs, self.num_seg, 1.0 / self.num_seg)` + +两行代码就可以实现TSM了,是不是很简单? + +# Reference +[1] [Lin Ji , Gan Chuang , Han Song . TSM: Temporal Shift Module for Efficient Video Understanding. arXiv:1811.08383,2018](https://arxiv.org/pdf/1811.08383v2.pdf). + + +[2] [Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoo Tang,and Luc Van Gool. Temporal segment networks for action recognition in videos? In Proceedings of the European Conference on Computer Vision,pages 20–36. Springer, 2016](https://arxiv.org/abs/1608.00859). diff --git a/docs/zh-CN/tutorials/TSN.md b/docs/zh-CN/tutorials/TSN.md new file mode 100644 index 0000000000000000000000000000000000000000..7e6804821d6327366d6f5c642ce754899daafe8a --- /dev/null +++ b/docs/zh-CN/tutorials/TSN.md @@ -0,0 +1,140 @@ +# TSN + +## 背景 +TSN 可以看作是对 two-stream 的改进,通过设计有效的卷积网络体系结构 TSN 解决视频动作分类中的两个主要问题: +* 长距离时序依赖问题(有些动作在视频中持续的时间较长); +* 解决数据量较少的问题; + +## 贡献 +TSN 的贡献可概括为以下两点: +* TSN 模型基于 long-range temporal structure 建模,结合了 sparse temporal sampling strategy 和 video-level supervision 从而保证对整段视频学习的有效性和高效性; +* 提出了一系列最佳实践方案; + +## 原理 +由于 two-stream 网络处理的是单帧图像(空间网络)或者短片段中的一堆帧图像(时序网络),因此 two-stream 网络无法满足时间跨度较长的视频动作。为了能够处理长范围时序结构的情况,可以使用密集帧采样方式从视频中获取长时间信息,但这样会增加时间成本同时采样到的连续帧之间存在冗余。于是在 TSN 模型中作者使用稀疏采用的方式来替代密集采样,降低计算量的同时一定程度上也去除了冗余信息。 + +TSN 采用和 two-stream 相似的结构,网络由空间流卷积网络和时间流卷积组成。TSN 使用稀疏采样的方式从整段视频采出一系列的短片段,其中每个片段都会有一个对自身动作类别的初步预测,之后通过对这些片段的预测结果进行“融合”得出对整个视频的预测结果。 + +## 网络结构 +如下图所示,一个视频被分为 ![formula](https://render.githubusercontent.com/render/math?math=K) 段( segment );之后对每个段使用稀疏采样的方式采出一个片段( snippet );然后使用“段共识函数”对不同片段的预测结果进行融合生成“段共识”,此时完成了一个视频级的预测;最后对所有模式的预测结果进行融合生成最终的预测结果。 + + +

    +
    +

    + +> 这里注意 segment 和 snippet 的区别 + +TSN 采用与 two-stream 类似的结构,使用空间网络操作一帧 RGB 图像,时序卷积网络操作连续的光流图像。但由于更深的网络结构能够提升对物体的识别能力,因此 TSN 中作者采用 BN-Inception 构建网络。 + +## 损失函数 + +给定一段视频 ![formula](https://render.githubusercontent.com/render/math?math=V),按相等间隔分为 ![formula](https://render.githubusercontent.com/render/math?math=K) 段 ![formula](https://render.githubusercontent.com/render/math?math={S_1,S_2,...,S_K})。 TSN 对一系列片段的建模如下: + + + +其中, 表示片段序列,从每个段 ![formula](https://render.githubusercontent.com/render/math?math=S_k) 中随机采样获取对应的片段 ![formula](https://render.githubusercontent.com/render/math?math=T_k); 表示作用于短片段 ![formula](https://render.githubusercontent.com/render/math?math=T_k) 的卷积网络,![formula](https://render.githubusercontent.com/render/math?math=W) 为网络的参数,返回值为 ![formula](https://render.githubusercontent.com/render/math?math=T_k) 相对于所有类别的得分;段共识函数 ![formula](https://render.githubusercontent.com/render/math?math=G) 用于融合所有片段的预测结果。预测函数 ![formula](https://render.githubusercontent.com/render/math?math=H)用于预测整段视频属于每个动作类别的概率,它的输入为段共识函数 ![formula](https://render.githubusercontent.com/render/math?math=G) 的结果。 + +最后,采用标准分类交叉熵计算部分共识的损失: + + + + +其中,![formula](https://render.githubusercontent.com/render/math?math=C) 是类别总数;![formula](https://render.githubusercontent.com/render/math?math=y_i) 是类别 ![formula](https://render.githubusercontent.com/render/math?math=i) 的 ![formula](https://render.githubusercontent.com/render/math?math=groundtruth);论文中段的数量 ![formula](https://render.githubusercontent.com/render/math?math=K) 设置为 ![formula](https://render.githubusercontent.com/render/math?math=3);共识函数 ![formula](https://render.githubusercontent.com/render/math?math=G) 采用取均值的方式,从所有片段的相同类别中推断出某个类别得分 ![formula](https://render.githubusercontent.com/render/math?math=G_i)。 + +## 模型输入 +对于图像任务而言,只能够使用图像本身提取特征。但对视频来说,除了每一帧图像外,还有视频中的光流信息。为了探索更多输入形式对模型效果影响,TSN 模型在空间卷积网络中除了使用单一 RGB 图像外,还使用了 RGB difference;在时序卷积网络中除了将连续的光流场作为输入外还采用了扭曲的光流场。 + +

    +
    +

    + +单一 RGB 图像只能表示静态信息,缺少上下文信息。但连续两帧之间的差异能够表示动作的改变,因此作者尝试将 RGB difference 作为模型的一种输入。 + +TSN 将光流场作为输入捕获运动信息;将扭曲光流场作为输入抑制背景运动,使得专注于视频中的人物运动。 + +## 训练 +由于数据集较小,为了避免过拟合,作者提出了一系列的训练策略。 + +### 数据增强 +通过数据增强可生成额外的训练样本,一定程度上能够避免模型的过拟合。two-stream 中采用的数据增强方式有随机裁剪和水平翻转,在 TSN 中作者新增了两种数据增强方法: +* 角裁剪:仅从图片的边角或中心提取区域,避免默认关注图片的中心; +* 尺度抖动:将输入图像或者光流场的大小固定为 ,裁剪区域的宽和高随机从 ![formula](https://render.githubusercontent.com/render/math?math={256,224,192,168}) 中选择。最终,裁剪区域将被 用于网络训练。 + +### 交叉预训练 +由于空间网络以 RGB 图片作为输入,因此作者在空间网络上直接使用 ImageNet 预训练模型初始化网络的参数。对于以 RGB difference 和光流作为输入的模型,作者提出了交叉预训练技术,使用 RGB 预训练模型初始化时序网络。首先,通过线性变换将光流场离散到从 0 到 255 的区间,使得光流场和 RGB 的取值范围相同;之后修改 RGB 模型的第一个卷积层,对 RGB 通道上的权重进行取均值操作;然后依据时序网络的输入通道数复制 RGB 均值。该策略能够有效的避免时序网络出现过拟合现象。 + +### 正则化技术 +由于光流分布和 RGB 分布不同,因此除了第一个 BN 层,其余 BN 层的参数都被固定。此外,为了进一步降低过拟合产生的影响,作者在 BN-Inception 的全局 pooling 层后添加一个额外的 dropout 层,其中空间卷积网络的 dropout 比例设置为 0.8;时序卷积网络的 dropout 比例设置为 0.7。 + +## 数据集 +模型在 HMDB51 和 UCF101 两个主流的动作识别数据集上进行。其中,HMDB51 数据集包含 51 个动作分类的 6766 个视频剪辑;UCF101 数据集包含 13320 个视频剪辑,共 101 类动作。 + +## 实现细节 +* 基于动量的小批量随机梯度下降算法,momentum 设置为 0.9; +* batch size 为 256; +* 使用 ImageNet 预训练模型对网络权重进行初始化; +* learning rate 调整,对于空间网络,初始化为 0.01,并且每 2000 次迭代后降变为原来的 0.1 倍,训练过程共迭代 4500 次;对于时序网络,初始化为 0.005,并且在第 12000 和 18000 次迭代之后降为原来的 0.1 倍,训练过程共迭代 20000 次; +* 使用 TVL1 光流算法来提取正常光流场和扭曲光流场。 +* 8 块 TITANX GPUs + +## PaddleVideo +为了加快 TSN 模型的推理速度,PaddleVideo 去掉了与 RGB difference、光流以及扭曲光流相关的部分。 + +PaddleVideo 中实现稀疏采样的关键代码: +```python +frames_len = results['frames_len'] # 视频中总的帧数 +average_dur = int(int(frames_len) / self.num_seg) # 每段中视频的数量 +frames_idx = [] # 存放采样到的索引 +for i in range(self.num_seg): + idx = 0 # 采样的起始位置 + if not self.valid_mode: + # 如果训练 + if average_dur >= self.seg_len: + idx = random.randint(0, average_dur - self.seg_len) + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + else: + # 如果测试 + if average_dur >= self.seg_len: + idx = (average_dur - 1) // 2 + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + # 从采样位置采连续的帧 + for jj in range(idx, idx + self.seg_len): + if results['format'] == 'video': + frames_idx.append(int(jj % frames_len)) + elif results['format'] == 'frame': + frames_idx.append(jj + 1) + else: + raise NotImplementedError +``` + +PaddleVideo 中实现“段共识”的核心代码: +``` +# [N * num_segs, in_channels, 7, 7] +x = self.avgpool2d(x) +# [N * num_segs, in_channels, 1, 1] +if self.dropout is not None: + x = self.dropout(x) +# [N * num_seg, in_channels, 1, 1] +x = paddle.reshape(x, [-1, num_seg, x.shape[1]]) +# [N, num_seg, in_channels] +x = paddle.mean(x, axis=1) +# [N, 1, in_channels] +x = paddle.reshape(x, shape=[-1, self.in_channels]) +# [N, in_channels] +score = self.fc(x) +``` + +## 广告时间 +如果文档对您理解 TSN 模型有帮助,欢迎👍star🌟,👏fork,您的支持是我们前进的动力⛽️。 + +## 参考 +[Temporal Segment Networks: Towards Good Practices for Deep Action Recognition](https://arxiv.org/abs/1608.00859) diff --git a/docs/zh-CN/tutorials/accelerate.md b/docs/zh-CN/tutorials/accelerate.md new file mode 100644 index 0000000000000000000000000000000000000000..fcf7a655e8db6d54ebcaf6d1f2b0edd765e47a78 --- /dev/null +++ b/docs/zh-CN/tutorials/accelerate.md @@ -0,0 +1,242 @@ +简体中文 | [English](../../en/tutorials/accelerate.md) + +- [简介](#简介) +- [模型运算加速](#模型运算加速) +- [数据读取加速](#数据读取加速) +- [训练策略加速](#训练策略加速) +- [分布式训练](#分布式训练) + + +# 简介 + +视频任务相比于图像任务的训练往往更加耗时,其原因主要有两点: +- 数据:视频解码耗时。mp4/mkv等视频文件都是经过encode后的压缩文件,通过需要经过解码和抽帧步骤才能得到原始的图像数据流,之后经过图像变换/增强操作才能将其喂入网络进行训练。如果视频帧数多,解码过程极其耗时。 +- 模型:视频任务使用的模型通常有更大的参数量与计算量。为学习时序特征,视频模型一般会使用3D卷积核/(2+1)D/双流网络,这都会使得模型的参数量与计算量大大增加。 + +本教程介绍如下视频模型训练加速方法: + +- 模型上,通过op融合或混合精度训练的方式提升op运算效率 +- 数据上,通过多进程或者并行计算的方式加速数据读取速度 +- 训练策略上,通过multigrid策略减少训练耗时 +- 多机分布式减少训练耗时 + +以上训练加速方法都已经集成进PaddleVideo中,欢迎试用~ + +如非特别说明,本教程所有实验的测试环境如下: +``` +GPU: v100,4卡*16G +CPU: Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz +PaddlePaddle: 2.0.0-rc1 +Cuda: 10.2 +``` + + +# 模型运算加速 + +- [OP融合](##OP融合) +- [混合精度训练](##混合精度训练) + +## OP融合 + +针对[TSM模型](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/model_zoo/recognition/tsm.md),我们实现了[temporal shift op](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api/paddle/fluid/layers/temporal_shift_cn.html#temporal-shift),在节省显存的同时加速训练过程。 + +测试方法: +使用不同形状的Tensor,以不同的方式实现temporal shift,记录显存占用和运行时间。 + +测试代码: + +- temporal shift op实现方式 +```python +import time +import numpy as np +import paddle +import paddle.nn.functional as F + +SHAPE = [32, 16, 32, 32] +#SHAPE = [128, 64, 128, 128] + +otl = [] +input = paddle.randn(SHAPE) +for i in range(10000): + t1 = time.time() + out1 = F.temporal_shift(x=input, seg_num=2, shift_ratio=0.2) + t2 = time.time() + ot = t2 - t1 + if i > 1000: + otl.append(ot) +print("op time: ", sum(otl)/len(otl)) +``` + +- 组合op实现方式 +```python +import time +import numpy as np +import paddle +import paddle.nn.functional as F + +SHAPE = [32, 16, 32, 32] +#SHAPE = [128, 64, 128, 128] + +def temporal_shift(x, seg_num, shift_ratio): + shape = x.shape #[N*T, C, H, W] + reshape_x = x.reshape((-1, seg_num, shape[1], shape[2], shape[3])) #[N, T, C, H, W] + pad_x = paddle.fluid.layers.pad(reshape_x, [0,0,1,1,0,0,0,0,0,0,]) #[N, T+2, C, H, W] + c1 = int(shape[1] * shift_ratio) + c2 = int(shape[1] * 2 * shift_ratio) + slice1 = pad_x[:, :seg_num, :c1, :, :] + slice2 = pad_x[:, 2:seg_num+2, c1:c2, :, :] + slice3 = pad_x[:, 1:seg_num+1, c2:, :, :] + concat_x = paddle.concat([slice1, slice2, slice3], axis=2) #[N, T, C, H, W] + return concat_x.reshape(shape) + +ctl = [] +input = paddle.randn(SHAPE) +for i in range(10000): + t2 = time.time() + out2 = temporal_shift(x=input, seg_num=2, shift_ratio=0.2) + t3 = time.time() + ct = t3 - t2 + if i > 1000: + ctl.append(ct) +print("combine time: ", sum(ctl)/len(ctl)) +``` + +性能数据如下: + +| 输入tensor形状 | 实现方式 | 显存占用/M| 计算时间/s | 加速比 | +| :------ | :-----: | :------: | :------: | :------: | +| 32\*16\*32\*32 |op组合方式 | 1074 | 0.00029325 | baseline | +| 32\*16\*32\*32 | temporal shift op | 1058 | 0.000045770 | **6.4x** | +| 128\*64\*128\*128 |op组合方式 | 5160 | 0.0099088 | baseline | +| 128\*64\*128\*128 | temporal shift op | 2588 | 0.0018617 | **5.3x** | + + + +## 混合精度训练 + +Comming soon~ + +# 数据读取加速 + +- [更优的解码库Decord](##更优的解码库Decord) +- [多进程加速Dataloader](##多进程加速Dataloader) +- [数据预处理DALI](##数据预处理DALI) +- [预先解码存成图像](##预先解码存成图像) + +对于单机训练,视频模型的训练瓶颈大多是在数据预处理上,因此本节主要介绍在数据处理上的一些加速经验。 + +## 更优的解码库Decord + +视频在喂入网络之前,需要经过一系列的数据预处理操作得到数据流,这些操作通常包括: + +- 解码: 将视频文件解码成数据流 +- 抽帧: 从视频中抽取部分帧用于网络训练 +- 数据增强:缩放、裁剪、随机翻转、正则化 + +其中解码是最为耗时的。相较于传统的opencv或pyAV解码库,这里推荐使用性能更优的解码库[decord](https://github.com/dmlc/decord)。目前[SlowFast模型](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/model_zoo/recognition/slowfast.md)使用decord进行视频解码([源码](https://github.com/PaddlePaddle/PaddleVideo/blob/main/paddlevideo/loader/pipelines/decode_sampler.py)),对单进程的速度提升有较大作用。 + +我们分别以opencv/decord为解码器,实现SlowFast模型数据预处理pipeline,然后随机从kinetics-400数据集中选取200条视频,计算各pipeline处理每条视频的平均时间。 + +性能测试数据如下: + +| 解码库 | 版本 | pipeline处理每条视频的平均时间/s | 加速比 | +| :------ | :-----: | :------: | :------: | +| opencv | 4.2.0 | 0.20965035 | baseline | +| decord | 0.4.2 | 0.13788146 | **1.52x** | + + +## 多进程加速Dataloader + +数据准备好后喂入网络进行训练,网络运算使用GPU并行加速相对较快。对于单个进程来说,速度瓶颈大多在数据处理部分,GPU大部分时间是在等待CPU完成数据预处理。 +飞桨2.0使用[Dataloader](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api/paddle/io/DataLoader_cn.html#dataloader)进行数据加载,DataLoader支持单进程和多进程的数据加载方式,当 num_workers 大于0时,将使用多进程方式异步加载数据。多进程加速协作,可以overlap掉GPU大部分等待的时间,提升GPU利用率,显著加速训练过程。 + +我们分别设置num_workers为0或4,单卡batch_size统一设置为8,统计训练一个batch的平均耗时。 + +性能测试数据对比如下: +| 卡数 | 单卡num_workers | batch_cost/s | ips | 加速比 | +| :------ | :-----: | :------: |:------: |:------: | +| 单卡 | 0 | 1.763 | 4.53887 | 单卡baseline | +| 单卡 | 4 | 0.578 | 13.83729 | **3.04x** | +| 4卡 | 0 | 1.866 | 4.28733 | 多卡baseline | +| 4卡 | 4 | 0.615 | 13.00625 | **3.03x** | + +其中ips = batch_size/batch_cost,即为训练一个instance(一个video)的平均耗时。 + +**结合使用decord和飞桨dataloader,加上在数据增强部分做一些细节优化,SlowFast模型训练速度增益为100%,详细数据可以参考[benchmark](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/benchmark.md)**。 + +## 数据预处理DALI + +既然GPU等待CPU进行数据处理耗时,能否把数据处理放到GPU上呢?[NVIDIA DALI](https://docs.nvidia.com/deeplearning/dali/user-guide/docs/)将数据预处理pipeline转移到GPU上执行,可以显著提升训练速度。针对视频文件,DALI提供`VideoReader`op进行解码抽帧操作,但目前其仅支持连续采样的方式进行抽帧。而视频领域常用的2D模型TSN或TSM,它们均采用分段采样方式,即把视频均匀分成N段segument,然后在每个segument内随机选取一帧,最后把选取的帧组合作为输入张量。为此,我们基于DALI进行了二次开发,实现了支持分段采样方式的`VideoReader`op。为方便用户使用,我们提供了配置好的docker运行环境,具体使用方法参考[TSN-DALI使用教程](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/model_zoo/recognition/tsn_dali.md)。 + +测试环境: +``` +机器: Tesla v100 +显存: 4卡16G +Cuda: 9.0 +单卡batch_size: 32 +``` + +性能测试数据如下: + +| 加速方式 | batch耗时/s | reader耗时/s | ips:instance/sec | 加速比 | +| :--------------- | :--------: | :------------: | :------------: | :------------: | +| DALI | 2.083 | 1.804 | 15.36597 | **1.41x** | +| Dataloader: 单卡num_workers=4 | 2.943 | 2.649 | 10.87460| baseline | +| pytorch实现 | TODO | TODO | TODO | TODO | + + +## 预先解码存成图像 + +这是一种简单直接的方法,既然视频解码耗时,那可以事先将视频解码好,存成图片,模型训练时直接读取图像即可。这种方法可以显著提升视频模型训练速度,但它也有一个很明显的缺点,就是需要耗费大量的内存空间。以kinetics-400数据集为例,共包含24万个训练样本,mp4文件约130G,解码存成图像后,占用的内存空间约为2T,所以这种方法比较适用于较小规模的数据集,如ucf-101。PaddleVideo提供了[预先解码](https://github.com/PaddlePaddle/PaddleVideo/blob/main/data/ucf101/extract_rawframes.py)的脚本,并且[TSN模型](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/model_zoo/recognition/tsn.md)和[TSM模型](https://github.com/PaddlePaddle/PaddleVideo/blob/main/docs/zh-CN/model_zoo/recognition/tsm.md)均支持直接使用frame格式的数据进行训练,详细实现参考[源码](https://github.com/PaddlePaddle/PaddleVideo/blob/main/paddlevideo/loader/dataset/frame.py)。 + + +测试方法: 数据集选用UCF-101,模型为ppTSM,模型参数参考默认配置[pptsm.yaml](https://github.com/PaddlePaddle/PaddleVideo/blob/main/configs/recognition/tsm/pptsm.yaml),Dataloader的num_workers参数设为0,分别以video和frame格式作为输入,单卡训练,性能数据如下: + +| 数据格式 | batch耗时/s | reader耗时/s | ips:instance/sec | reader加速比 | 加速比 | +| :--------------- | :--------: | :------------: | :------------: | :------------: | :------------: | +| frame | 1.008 | 0.591 | 15.87405 | 4.79x | **3.22x** | +| video | 3.249 | 2.832 | 4.92392| baseline | baseline | + + +# 训练策略加速 + +前述方法大多从工程的角度思考训练速度的提升,在算法策略上,FAIR在CVPR 2020中提出了[Multigrid加速策略算法](https://arxiv.org/abs/1912.00998),它的基本思想如下: + +在图像分类任务中,若经过预处理后图像的高度和宽度分别为H和W,batch_size为N,则网络输入batch的Tensor形状为`[N, C, H, W]`,其中C等于3,指RGB三个通道。 +对应到视频任务,由于增加了时序通道,输入batch的Tensor形状为`[N, C, T, H, W]`。 +传统的训练策略中,每个batch的输入Tensor形状都是固定的,即都是`[N, C, T, H, W]`。若以高分辨的图像作为输入,即设置较大的`[T, H, W]`,则模型精度会高一些,但训练会更慢;若以低分辨的图像作为输入,即设置较小的`[T, H, W]`,则可以使用更大的batch size,训练更快,但模型精度会降低。在一个epoch中,能否让不同batch的输入Tensor的形状动态变化,既能提升训练速度,又能保证模型精度? + +基于以上思想,FAIR在实验的基础上提出了Multigrid训练策略: 固定`N*C*T*H*W`的值,降低`T*H*W`时增大`N`的值,增大`T*H*W`时减小`N`的值。具体包含两种策略: + +- Long cycle: 设完整训练需要N个epoch,将整个训练过程分4个阶段,每个阶段对应的输入tensor形状为: +``` +[8N, T/4, H/sqrt(2), W/sqrt(2)], [4N, T/2, H/sqrt(2), W/sqrt(2)], [2N, T/2, H, W], [N, T, H, W] +``` + +- Short cycle: 在Long cycle的基础上,Short-cycle让每个iter的输入Tensor形状都会发生变化,变化策略为: +``` +[H/2, W/2], [H/sqrt(2), W/sqrt(2)], [H, W] +``` + +我们基于飞桨实现了Multigrid训练加速策略,对SlowFast模型训练进行加速,使用方法请参考文档[SlowFast训练加速](https://github.com/PaddlePaddle/PaddleVideo/blob/develop/docs/zh-CN/model_zoo/recognition/slowfast.md#%E8%AE%AD%E7%BB%83%E5%8A%A0%E9%80%9F)。 + +测试环境: +``` +机器: Tesla v100 +显存: 8卡32G +Cuda: 9.0 +单卡batch_size: 8 +数据集: Kinetics-400 +Paddle版本: 2.0-rc0 +``` + +性能数据如下: + +| 训练策略 | 单个epoch平均耗时/min | 训练总时间/min | 加速比 | +| :------ | :-----: | :------: |:------: | +| Multigrid | 27.25 | 9758(6.7天) | 2.89x | +| Normal | 78.76 | 15438(10.7天) | base | + +# 分布式训练 + +Comming soon~ diff --git a/docs/zh-CN/tutorials/deployment.md b/docs/zh-CN/tutorials/deployment.md new file mode 100644 index 0000000000000000000000000000000000000000..28084055aeb57efef109eabc538a0a166c8c6551 --- /dev/null +++ b/docs/zh-CN/tutorials/deployment.md @@ -0,0 +1,58 @@ +简体中文 | [English](../../en/tutorials/deployment.md) + +# 推理 + +## 如何导出一个用于预测的模型? + +为了之后的模型预测和部署,我们需要导出模型结构,模型参数,这里应用了PaddlePaddle最新的动转静能力 +执行脚本 ```tools.export_model.py``` +```python +python3.7 tools/export_model.py -c 配置文件 -o 输出地址 -p 权重文件 +``` + +`export_model.py` 中,首先会重新build一个网络,这里注意,有些用于预测的模型初始化参数可能和训练时不一致,请注意更改。 +`export_model.py` 添加了针对TSM的`num_seg`等参数,会用to_static动转静,并调用jit.save来保存预测模型,注意:这里的inputspec需要指定一个`假` 输入来运行网路。 + +具体原理请参考 [动转静](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/04_dygraph_to_static/index_cn.html) 官方文档。 + +## 如何检查保存的预测模型正确性? + +这里我们提供了```tools/test_export_model.py```脚本用于检查预测模型的正确性。 + +```python +python3 tools/test_export_model.py -p 权重文件 -i 导出的模型文件夹地址 -c 配置文件 +``` + +`test_export_model.py`只是打印了输出的shape信息,可根据实际需求进行更改,完整的测试流程应该包含下一步:使用预测引擎进行推理 + +## 如何使用预测引擎进行推理? + +这里我们提供了```tools/predict.py``` 进行模型推理。 + +```python + python3.7 tools/predict.py -v example.avi --model_file "./inference/example.pdmodel" --params_file "./inference/example.pdiparams" --enable_benchmark=False --model="example" --num_seg=8 + ``` + + 对example.avi进行预测并返回预测结果 + + ## 如何测试推理速度 + 我们提供了统一的测试脚本 + + ```python + python3.7 tools/predict.py --enable_benchmark=True --model_file=模型文件 --params_file=参数文件 + ``` + + ## 如何使用服务器端C++推理? + + coming soon + + # 部署 + + ## 如何使用PaddleHub Serving进行部署? + + coming soon + + ## 如何使用PaddleLite进行端上部署? + + coming soon + diff --git a/docs/zh-CN/tutorials/pp-tsm.md b/docs/zh-CN/tutorials/pp-tsm.md new file mode 100644 index 0000000000000000000000000000000000000000..119db69e3536905caf7d90d51d53b7401f521ca0 --- /dev/null +++ b/docs/zh-CN/tutorials/pp-tsm.md @@ -0,0 +1,45 @@ +# PP-TSM高效实用视频识别模型 + +PP-TSM是PaddleVideo基于TSM优化和改进的视频模型, +其精度(UCF101和Kinetics400数据集top1)和推理速度均优于TSM论文及其他开源的TSM模型5%,3%以上, +要求使用PaddlePaddle2.0(可使用pip安装) 或适当的develop版本。 + +在仅用ImageNet pretrain情况下,PP-TSM在UCF101和Kinetics400数据集top1分别达到89.5%和73.5%, +在单卡V100上FP32推理速度为147 VPS (基于Kinectics400数据集). +在单卡V100上开启TensorRT下FP16推理速度为TODO。 + +pp-TSM在Kinetics400上top1精度为73.5%,是至今为止开源的2D视频模型中在相同条件下的最高性能。 + +PP-TSM从如下方面优化和提升TSM模型的精度和速度: +1、基于知识蒸馏的预训练模型 , +1.3% +2、网络结构微调 ,+2.5% +3、更优的batch size ,+0.2% +4、更优的L2正则化 ,+0.3% +5、label_smoothing ,+0.2% +6、更优的lr decay ,+0.15% +7、数据增广 ,+0.3% +8、更优的epoch num ,+0.15% +9、bn策略 ,+0.4% +10、集成PaddleInference进行预测推理 +11、知识蒸馏、优化器等更多TODO策略 +其中,每项策略的精度提升指标参考上述数据(基于ucf101及k400上进行实验)。 + +## preciseBN + +在介绍preciseBN之前,我们先回顾一下BN(Batch Norm)。BN层是一种正则化层,在训练时,它根据当前batch的数据按通道计算的均值和方差,然后进行归一化运算,公式如图: + +详细介绍可参考[BatchNorm文档](https://paddlepaddle.org.cn/documentation/docs/zh/2.0-rc1/api/paddle/fluid/dygraph/BatchNorm_cn.html#batchnorm)。 + +假设训练数据的分布和测试数据的分布是一致的,在训练时我们会计算并保存滑动均值和滑动方差,供测试时使用。滑动均值和滑动方差的计算方式如下: + +简单的说,moving_mean等于当前batch计算的均值与历史保存的moving_mean的加权和,即为滑动均值。**但滑动均值并不等于真实的均值**,因此测试时的精度仍会受到一定影响。 +为了提升测试精度,我们需要重新计算一个更加精确的均值,这就是preciseBN的目的。 + +真实的均值如何计算?最直观的想法是,把所有训练数据组成一个batch,输入网络进行前向传播,每经过一个BN层,计算一下当前特征的均值和方差。 +由于训练样本过多,实际操作中不可能这么做。 +所以近似做法是,网络训练完成后,固定住网络中的参数不动,将所有训练数据分成N个batch,依次输入网络进行前向计算,在这个过程中保存下来每个iter的均值和方差,最终得到所有训练样本精确的均值和方差。 +这就是preciseBN的计算方法。具体实现参考[preciseBN](https://github.com/PaddlePaddle/PaddleVideo/blob/main/paddlevideo/utils/precise_bn.py)。 + +实际使用时,由于迭代所有训练样本比较耗费时间,一般只会跑200个iter左右。 + + diff --git a/docs/zh-CN/tutorials/ppagcn.md b/docs/zh-CN/tutorials/ppagcn.md new file mode 100644 index 0000000000000000000000000000000000000000..f2807847a5b4289144eec5c69f2cfdfae12e89a6 --- /dev/null +++ b/docs/zh-CN/tutorials/ppagcn.md @@ -0,0 +1,23 @@ +# PP-AGCN模型详解 + +--- + +## 内容 + +- [ST-GCN模型简介](#ST-GCN模型简介) +- [PP-AGCN模型改进](#PP-AGCN模型改进) + +## ST-GCN模型简介 + +ST-GCN模型由香港中文大学-商汤科技联合实验室在AAAI 2018中提出,不仅为解决基于人体骨架关键点的人类动作识别问题提供了新颖的思路,在标准的动作识别数据集上也取得了较大的性能提升。 +时空图卷积网络模型ST-GCN通过将图卷积网络(GCN)和时间卷积网络(TCN)结合起来,扩展到时空图模型,设计出了用于行为识别的骨骼点序列通用表示, +该模型将人体骨骼表示为图,如图2所示,其中图的每个节点对应于人体的一个关节点。图中存在两种类型的边,即符合关节的自然连接的空间边(spatial edge)和在连续的时间步骤中连接相同关节的 +时间边(temporal edge)。在此基础上构建多层的时空图卷积,它允许信息沿着空间和时间两个维度进行整合。 + +ST-GCN的网络结构大致可以分为三个部分,首先,对网络输入一个五维矩阵(N, C, T, V, M),其中N为视频数据量;C为关节特征向量,包括(x,y,acc);T为视频中抽取的关键帧的数量; +V表示关节的数量,在本项目中采用25个关节数量;M则是一个视频中的人数,然后再对输入数据进行Batch Normalization批量归一化,接着,通过设计ST-GCN单元, +引入ATT注意力模型并交替使用GCN图卷积网络和TCN时间卷积网络,对时间和空间维度进行变换,在这一过程中对关节的特征维度进行升维,对关键帧维度进行降维, +最后,通过调用平均池化层、全连接层,并后接SoftMax层输出,对特征进行分类。 + + +## PP-AGCN模型详解 diff --git a/docs/zh-CN/tutorials/reletive_issues b/docs/zh-CN/tutorials/reletive_issues new file mode 100644 index 0000000000000000000000000000000000000000..6db03e0746244ede15351c469afb11fa9d7784cd --- /dev/null +++ b/docs/zh-CN/tutorials/reletive_issues @@ -0,0 +1,77 @@ +video_path is what ? #4510 +https://github.com/PaddlePaddle/models/issues/4510 + +关于BSN/BMN模型 #4411 +https://github.com/PaddlePaddle/models/issues/4411 + +微调nextvald的参数,如何加载部分参数呢 #4367 +https://github.com/PaddlePaddle/models/issues/4367 + +用TSN视频分类模型进行finetune时的问题 #4358 +https://github.com/PaddlePaddle/models/issues/4358 + +用paddle视频分类模型进行finetune开发报错。 #4353 +https://github.com/PaddlePaddle/models/issues/4353 + +BMN/BSN模型评估时报错 #4110 +https://github.com/PaddlePaddle/models/issues/4110 + +The avg losses are not same for training and validation when the same data are used. #4973 +https://github.com/PaddlePaddle/models/issues/4973 + +How can I load a pretrained model in AttentionCluster to train my own data? #4972 +https://github.com/PaddlePaddle/models/issues/4972 + +ETS模型的数据处理 #4957 +https://github.com/PaddlePaddle/models/issues/4957 + +BMN模型推理出现错误 #4881 +https://github.com/PaddlePaddle/models/issues/4881 + +C-TCN模型数据集不支持MP4格式,MP4转为pickle文件格式需要提供相应处理工具脚本 #4782 +https://github.com/PaddlePaddle/models/issues/4782 + +CTCN 有没有使用I3D特征的demo #4756 +https://github.com/PaddlePaddle/models/issues/4756 + +ctcn的数据集.pkl文件的b'feats'和b'scores'是什么?我注意到ctcn_reader.py只用到了b'scores',是否b'scores'才是需要的特征?还有对应的txt文件是什么?假设我需要把BMN的数据集转化为ctcn的数据集,该怎么做? #4750 +https://github.com/PaddlePaddle/models/issues/4750 + +使用BMN预训练模型训练的时候报错 #4749 +https://github.com/PaddlePaddle/models/issues/4749 + +使用BMN进行预测时,输出的json文件 视频ID少了两个字符,所有的文件都是这样 #4745 +https://github.com/PaddlePaddle/models/issues/4745 + +BMN模型batch_size调小之后loss为nan #4738 +https://github.com/PaddlePaddle/models/issues/4738 + +BMN的输入问题 #4724 +https://github.com/PaddlePaddle/models/issues/4724 + +报一个video_tag的BUG #4698 +https://github.com/PaddlePaddle/models/issues/4698 + +PaddleCV-video-ctcn 训练到Epoch21,iter1365停止不动 #4719 +https://github.com/PaddlePaddle/models/issues/4719 + +STNET跑模型推断,显卡显存充足,提示显存不足 #4608 +https://github.com/PaddlePaddle/models/issues/4608 + +训练stnet读取kinetics数据集时出线错误 求解决 #4529 +https://github.com/PaddlePaddle/models/issues/4529 + +有关CTCN视频动作定位的问题 #4508 +https://github.com/PaddlePaddle/models/issues/4508 + +谁有这个yt8m 的tfrecord? #4506 +https://github.com/PaddlePaddle/models/issues/4506 + +The NeXtVLAD final model couldn't be used ??? #4502 +https://github.com/PaddlePaddle/models/issues/4502 + +Hi, I'm wondering if there is an end-to-end solution for the youtube8M attention_lstm model? #4201 +https://github.com/PaddlePaddle/models/issues/4201 + +CTCN模型训练一段时间后NAN #4123 +https://github.com/PaddlePaddle/models/issues/4123 diff --git a/docs/zh-CN/tutorials/summarize.md b/docs/zh-CN/tutorials/summarize.md new file mode 100644 index 0000000000000000000000000000000000000000..c37dc240365b3006f5bd41154a9cc00ab38bdabc --- /dev/null +++ b/docs/zh-CN/tutorials/summarize.md @@ -0,0 +1,138 @@ +# 视频分类和动作识别介绍 + +## 广泛的应用场景 +视频分类在多个领域上都有广泛的应用,如短视频、推荐、搜索、电视台、广告,安防,监控等领域。 + +## 多种细分任务 +与图像任务相似,视频任务也可以分为分类(识别)和检测任务两大类,结合不同的场景还可以对这两类任务具体进行细分: + ++ Task1:修剪视频识别(Trimmed Action Recognition)。输入一段只包含一个动作的修剪视频,输出视频分类,如下图所示: +

    +
    + 行为分类 +

    + + 从使用的数据模态上区分,分类任务还可以继续细分为基于单模态数据的分类和基于多模态数据的分类,基于RGB图像的分类和基于人体骨架的分类等等,如下图所示: + +

    +
    + 多种模态 +

    +从视频的视角上分还可以分为第一人称视角的行为识别和第三人称视角的行为识别,单一视角的识别和多视角融合的识别,有兴趣的用户可自行查阅相关文献。 + ++ Task2:未修剪视频分类(Untrimmed Video Classification)。与修剪视频识别不同的是,未修剪的视频中通常含有多个动作,而且视频很长。有许多动作或许都不是我们所关注的。通过对输入的长视频进行全局分析,然后软分类到多个类别。 + ++ Task3:时序行为提名(Temporal Action Proposal)。类似于图像目标检测任务中的候选框提取。在一段长视频中通常含有很多动作,任务是从视频中找出可能含有动作的视频段。 + ++ Task4:时序行为定位(Temporal Action Localization)。相比于上面的时序行为提名而言,时序行为定位和我们常说的目标检测一致,要求从视频中找到可能存在行为的视频段,并且给视频段分类,如下图所示: +

    +
    + 行为检测 +

    + ++ Task5:密集行为描述(Dense-Captioning Events)。之所以称为密集行为描述,主要是因为该任务要求在时序行为定位(检测)的基础上进行视频行为描述。也就是说,该任务需要将一段**未修剪的视频**进行**时序行为定位**得到许多包含行为的视频段后,并对该视频段进行**行为描述**。 + + +## 数据集简介 + +### 视频分类数据集 + +模型的训练和验证离不开全面、大量以及具有较好标注的数据集。随着视频行为识别研究的不断深入,越来越多的数据集应用于这一领域的研究。典型的数据集如下: ++ KTH数据集[1](#1) + +KTH数据集是一个早期的小型行为识别数据集,包括599段视频6类动作(走、跳、跑、击拳、挥手、拍手)背景相对静止,除了镜头的拉近拉远,摄像机的运动比较轻微。由于该数据集比较小,当训练较大型的3D网络时很容易过拟合,因此当前的大部分研究训练过程多数不基于此数据集。 ++ UCF10数据集[2](#2) + +UCF101是一个中型数据集视频主要来自于YouTube,包含13320段视频,共101类动作,每类动作由25个人完成,每个人做4-7组动作。在Kinetics数据集发布之前UCF101和HMDB51数据集在很长的一段时间里被作为benchmark用于评估行为识别方法的效果。 ++ HMDB51数据集[3](#3) + +Brown university大学提出的HMDB51数据集于2011年发布,视频多数来源于电影,还有一部分来自公共数据库以及YouTube等网络视频库。数据库包含有6849段样本,分为51类,每类至少包含有101段样本。 ++ Kinetics数据集[4](#4) + +Kinetics是当前最为重要的一个大型行为识别数据集,该数据集在2017年由Google的Deepmind团队提出,视频数据同样来自于YouTube,总共400个类别(现已经扩充到700类),30多万段视频数据(Kinetics-700已经扩充到了60多万段视频),每段视频持续10秒左右。动作类别主要分为三大类:“人”,“人与动物”,“人与人互动”。Kinetics数据集可以训练3D-Resnet达到152层而不发生过拟合,解决了之前训练数据集过小难以训练深层3D网络的困境。当前Kinetics已经取代了UCF101和HMDB51成为了行为识别领域的benchmark。当前,大多数研究都采用此数据集进行效果评估和预训练。 ++ Something-Something数据集[5](#5) + +SomethingV1包含108499段标注视频(V2已经扩展到了220847),每一个时长都在2到6秒之间。这些视频包含了174种类别的动作,与前面的数据集不同此数据集的识别需要更强的时间信息,因此在检验模型时域建模能力方面此数据集具有很重要的参考价值。 +除了以上的主流数据集外目前还有复杂动作识别的Charades[6](#6)数据集、Breakfast Action[7](#7)数据集、以及百万级别的体育视频数据集Sports 1M[8](#8)。 + +### 检测任务数据集 + ++ THUMOS 2014 + +来自于THUMOS Challenge 2014,。它的训练集为UCF101数据集,验证集和测试集分别包括1010和1574个未分割的视频片段。在行为检测任务中只有20类动作的未分割视频是有时序行为片段标注的,包括200个验证集(3007个行为片段)和213个测试集视频(包含3358个行为片段)。 + ++ MEXaction2 + +MEXaction2数据集中包含两类动作:骑马和斗牛。该数据集由三个部分组成:YouTube视频,UCF101中的骑马视频以及INA视频。其中YouTube视频片段和UCF101中的骑马视频是分割好的短视频片段,被用于训练集。而INA视频为多段长的未分割的视频,时长共计77小时,且被分为训练,验证和测试集三部分。训练集中共有1336个行为片段,验证集中有310个行为片段,测试集中有329个行为片断。且MEXaction2数据集的特点是其中的未分割视频长度都非常长,被标注的行为片段仅占视频总长的很低比例。 + ++ ActivityNet + +目前最大的数据库,同样包含分类和检测两个任务。这个数据集仅提供视频的youtube链接,而不能直接下载视频,所以还需要用python中的youtube下载工具来自动下载。该数据集包含200个动作类别,20000(训练+验证+测试集)左右的视频,视频时长共计约700小时. + + +## 经典模型简介 +如图所示,动作识别框架主要包括三个步骤:特征提取、运动表示和分类。其中,如何提取视频的时空特征是行为识别和视频分类的核心问题。 +

    +
    +行为识别框架 +

    +依据使用方法的不同可以总体上将行为识别(视频分类)方法概括为基于手工特征方法阶段和基于深度学习方法阶段。基于手工特征的方法阶段比较典型的运动描述子有DTP和IDT,这也是深度学习应用于这一领域之前为大家所公认的最为优秀的运动描述子,感兴趣的读者可以自行查阅文末的相关参考文献。从2014年起,深度学习的方法逐渐开始应用于视频分类领域,目前基于深度学习的方法已经成为了学术界的研究热点,并且在实际的应用效果上看也远远超越了手工设计的运动特征。从2014年至今围绕着如何表征运动特征这一问题,学术界提出了许多经典的网络结构,如下图所示: +

    +
    +典型的方法 +

    + +目前Paddlevideo模型库中已经囊括了TSN[9](#9) ,TSM[10](#10),slowfast[11](#11)等经典的行为识别网络,我们后续会陆续对视频领域的经典模型和论文进行详细解析,敬请期待! + + +## 相关比赛介绍 ++ [ActivityNet](http://activity-net.org/challenges/2020/challenge.html) + +ActivityNet是一个大规模行为识别竞赛,自2016年开始,每年与CVPR同时进行,到今年为止已经连续举办了4届。它侧重于从用户产生的视频中识别出日常生活,高层次,面向目标的活动,视频取自互联网视频门户Youtube。目前,ActivityNet比赛已经成为了行为识别领域影响力最大的比赛。 + + +## Reference + +
    +[1] Schuldt C, Laptev I, Caputo B.Recognizing Human Actions: A Local SVM Approach Proceedings of International Conference on Pattern Recognition. Piscataway, NJ: IEEE, 2004:23-26 +
    +
    +
    +[2] Soomro K, Zamir A R, Shah M. UCF101: A Dataset of 101 Human Actions Classes From Videos in The Wild. arXiv:1212.0402,2012. +
    +
    +
    +[3] Kuehne H, Jhuang H, Garrote E, et al. HMDB: a large video database for human motion recognition Proceedings of IEEE International Conference on Computer Vision. Piscataway, NJ: IEEE, 2011:2556-2563. +
    +
    +
    +[4] Carreira J , Zisserman A . Quo Vadis, Action Recognition? A New Model and the Kinetics Dataset Proceedings of IEEE Conference on Computer Vision and Pattern Recognition. Piscataway, NJ: IEEE, 2017:6299-6308. +
    +
    +
    +[5] Goyal R, Kahou S E, Michalski V. The “something something” video database for learning and evaluating visual common sense. arXiv:1706.04261,2017. +
    +
    +
    +[6] Sigurdsson G A , Varol Gül, Wang Xiaolong, et al. Hollywood in Homes: Crowdsourcing Data Collection for Activity Understanding. arXiv: 604.01753,2016 +
    +
    +
    +[7] Kuehne H, Arslan A, Serre T. The Language of Actions Recovering the Syntax and Semantics of Goal-Directed Human Activities Proceedings of IEEE Conference on Computer Vision and Pattern Recognition. Piscataway, NJ: IEEE, 2014. +
    +
    +
    +[8] Karpathy A , Toderici G , Shetty S , et al. Large-Scale Video Classification with Convolutional Neural Networks Proceedings of IEEE Conference on Computer Vision and Pattern Recognition. Piscataway, NJ: IEEE, 2014:1725-1732. +
    +
    +
    +[9] Limin Wang, Yuanjun Xiong, Zhe Wang, Yu Qiao, Dahua Lin, Xiaoo Tang,and Luc Van Gool. Temporal segment networks for action recognition in videos? In Proceedings of the European Conference on Computer Vision,pages 20–36. Springer, 2016. +
    +
    +
    +[10] Lin Ji , Gan Chuang , Han Song . TSM: Temporal Shift Module for Efficient Video Understanding. arXiv:1811.08383,2018. +
    +
    +
    +[11] Feichtenhofer C , Fan Haoqi , Malik J , et al. SlowFast Networks for Video Recognition. arXiv:1812.03982,2018. +
    diff --git a/docs/zh-CN/usage.md b/docs/zh-CN/usage.md new file mode 100644 index 0000000000000000000000000000000000000000..816999993e1edbd64bbb989940b2778ca2695be9 --- /dev/null +++ b/docs/zh-CN/usage.md @@ -0,0 +1,189 @@ +简体中文 | [English](../en/start.md) + +# 使用指南 +--- + +* [1. 模型训练](#1) +* [2. 模型恢复训练](#2) +* [3. 模型微调](#3) +* [4. 模型测试](#4) +* [5. 模型推理](#5) + + +请参考[安装指南](./install.md)配置运行环境,PaddleVideo目前支持Linux下的GPU单卡和多卡运行环境。 + + + + +## 1. 模型训练 + +PaddleVideo支持单机单卡和单机多卡训练,单卡训练和多卡训练的启动方式略有不同。 + +### 1.1 单卡训练 + +启动脚本示例: + +```bash +export CUDA_VISIBLE_DEVICES=0 #指定使用的GPU显卡id +python3.7 main.py --validate -c configs_path/your_config.yaml +``` +- `-c` 必选参数,指定运行的配置文件路径,具体配置参数含义参考[配置文档](./contribute/config.md#config-yaml-details) +- `--validate` 可选参数,指定训练时是否评估 +- `-o`: 可选参数,指定重写参数,例如: `-o DATASET.batch_size=16` 用于重写train时batch size大小 + +### 1.2 多卡训练 + +通过`paddle.distributed.launch`启动,启动脚本示例: +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=your_log_dir main.py --validate -c configs_path/your_config.yaml +``` +- `--gpus`参数指定使用的GPU显卡id +- `--log_dir`参数指定日志保存目录 +多卡训练详细说明可以参考[单机多卡训练](https://www.paddlepaddle.org.cn/documentation/docs/zh/2.1/guides/02_paddle2.0_develop/06_device_cn.html#danjiduokaxunlian) + + +我们将所有标准的启动命令都放在了```run.sh```中,直接运行(./run.sh)可以方便地启动多卡训练与测试,注意选择想要运行的脚本 +```shell +sh run.sh +``` + +### 1.3 输出日志 + +运行训练命令,将会输出运行日志,并默认保存在./log目录下,如:`worker.0` , `worker.1` ... , worker日志文件对应每张卡上的输出 + +【train阶段】打印当前时间,当前epoch/epoch总数,当前batch id,评估指标,耗时,ips等信息: +```txt +[09/24 14:13:00] epoch:[ 1/1 ] train step:100 loss: 5.31382 lr: 0.000250 top1: 0.00000 top5: 0.00000 batch_cost: 0.73082 sec, reader_cost: 0.38075 sec, ips: 5.47330 instance/sec. +``` + +【eval阶段】打印当前时间,当前epoch/epoch总数,当前batch id,评估指标,耗时,ips等信息: +```txt +[09/24 14:16:55] epoch:[ 1/1 ] val step:0 loss: 4.42741 top1: 0.00000 top5: 0.00000 batch_cost: 1.37882 sec, reader_cost: 0.00000 sec, ips: 2.90104 instance/sec. +``` + +【epoch结束】打印当前时间,评估指标,耗时,ips等信息: +```txt +[09/24 14:18:46] END epoch:1 val loss_avg: 5.21620 top1_avg: 0.02215 top5_avg: 0.08808 avg_batch_cost: 0.04321 sec, avg_reader_cost: 0.00000 sec, batch_cost_sum: 112.69575 sec, avg_ips: 8.41203 instance/sec. +``` + +当前为评估结果最好的epoch时,打印最优精度: +```txt +[09/24 14:18:47] Already save the best model (top1 acc)0.0221 +``` + +### 1.4 输出存储路径 + +- PaddleVideo各文件夹的默认存储路径如下: + +``` +PaddleVideo + ├── paddlevideo + ├── ... #other source codes + ├── output #ouput 权重,优化器参数等存储路径 + | ├── example + | | ├── example_best.pdparams #path_to_weights + | | └── ... + | └── ... + ├── log #log存储路径 + | ├── worker.0 + | ├── worker.1 + | └── ... + └── inference #预测文件存储路径 + ├── example.pdiparams file + ├── example.pdimodel file + └── example.pdiparmas.info file +``` + +- 训练Epoch默认从1开始计数,参数文件的保存格式为`ModelName_epoch_00001.pdparams`,命名中的数字对应Epoch编号。 + + + + +## 2. 模型恢复训练 + +如果训练任务终止,可以加载断点权重文件(优化器-学习率参数,断点文件)继续训练。 +需要指定`-o resume_epoch`参数,该参数表示从```resume_epoch```轮开始继续训练. + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/example.yaml \ + --validate \ + -o resume_epoch=5 +``` + + + +## 3. 模型微调 + +进行模型微调(Finetune),对自定义数据集进行模型微调,需要指定 `--weights` 参数来加载预训练模型。 + +```bash +export CUDA_VISIBLE_DEVICES=0,1,2,3 + +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/example.yaml \ + --validate \ + --weights=./output/example/path_to_weights +``` + +PaddleVideo会自动**不加载**shape不匹配的参数 + + + + +## 4. 模型测试 + +需要指定 `--test`来启动测试模式,并指定`--weights`来加载预训练模型。 + +```bash +python3 -m paddle.distributed.launch \ + --gpus="0,1,2,3" \ + main.py \ + -c ./configs/example.yaml \ + --test \ + --weights=./output/example/path_to_weights +``` + + + +## 5. 模型推理 + +通过导出inference模型,PaddlePaddle支持使用预测引擎进行预测推理。接下来介绍如何用预测引擎进行推理: +首先,对训练好的模型进行转换 +指定`-c`参数加载配置文件,指定`-p`参数加载模型权重,指定`-o`用于指定转换后模型的存储路径。 + +```bash +python tools/export_model.py \ + -c ./configs/example.yaml \ + -p ./output/example/path_to_weights \ + -o ./inference +``` + + +上述命令将生成模型结构文件(`model_name.pdmodel`)和模型权重文件(`model_name.pdiparams`),然后可以使用预测引擎进行推理: + +```bash +python tools/predict.py \ + --input_file "data/example.avi" \ + --model_file "./inference/TSN.pdmodel" \ + --params_file "./inference/TSN.pdiparams" \ + --use_gpu=True \ + --use_tensorrt=False +``` + +其中: + ++ `input_file`:待预测的文件路径或文件夹路径,如 `./test.avi` ++ `model_file`:模型结构文件路径,如 `./inference/TSN.pdmodel` ++ `params_file`:模型权重文件路径,如 `./inference/TSN.pdiparams` ++ `use_tensorrt`:是否使用 TesorRT 预测引擎,默认值:`False` ++ `use_gpu`:是否使用 GPU 预测,默认值:`True` + +各模型详细的使用文档,可以参考[Models](./model_zoo/README.md) diff --git a/docs/zh-CN/whl_zh.md b/docs/zh-CN/whl_zh.md new file mode 100644 index 0000000000000000000000000000000000000000..e586ffe58a1227b59b33338b98aba3fd7f5189cc --- /dev/null +++ b/docs/zh-CN/whl_zh.md @@ -0,0 +1,181 @@ +简体中文 | [English](../en/whl_en.md) +# paddlevideo包使用教程 + +## 快速开始 + +### 安装 + +使用pypi安装 +```bash +python3.7 -m pip install paddlevideo==0.0.1 +``` +**注意:** 在下载opecv-python的过程中你可能遇到困难,你可以尝试使用其他源进行安装,试一试: +``` +python3.7 -m pip install opencv-python==4.2.0.32 -i https://pypi.doubanio.com/simple +``` + +本地打包whl文件并安装 +```bash +python3.7 setup.py bdist_wheel +python3.7 -m pip install dist/paddlevideo-0.0.1-py3-none-any.whl +``` + +### 1. 快速开始 + +* 指定 `video_file='data/example.avi'`, 使用飞桨提供的推理模型 `model_name='ppTSM'` + + +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False,use_tensorrt=False) +video_file='data/example.avi' +result=clas.predict(video_file) +print(result) +``` + +``` + >>> result + [{'videoname': 'data/example.avi', 'class_ids': [5], 'scores': [0.9621570706367493], 'label_names': ['archery']}] +``` + +* 使用命令行方式启动程序 +```bash +ppvideo --model_name='ppTSM' --video_file='data/example.avi' +``` + +``` + >>> result + **********data/example.avi********** + [{'videoname': 'data/example.avi', 'class_ids': [5], 'scores': [0.9621570706367493], 'label_names': ['archery']}] +``` + +### 2. 参数介绍 +* model_name(str): 模型的名字. 如果不指定`model_file`和`params_file`你需要指定这个参数来使用飞桨提供的在K400数据集上预训练的模型,默认设置为ppTSM +* video_file(str): 视频文件路径. 支持:本地单一视频文件,包含多个视频文件的文件夹,numpy数组。 +* use_gpu(bool): 是否使用GPU,默认为不使用。 +* num_seg(int): TSN提出的分段采样策略中分段的数量。 +* seg_len(int): 每个分段上采样的帧数。 +* short_size(int): 将帧的短边调整为多少像素,默认为256。 +* target_size(int): 调整帧的尺寸为目标尺寸,默认为224。 +* normalize(bool): 是否对帧进行归一化。默认为True。 +* model_file(str): 推理模型的模型文件(inference.pdmodel)的路径,如果不指定这个参数,你需要指定`model_name`来进行下载。 +* params_file(str): 推理模型的参数文件(inference.pdiparams)的路径,如果不指定这个参数,你需要指定`model_name`来进行下载。 +* batch_size(int): Batch size, 默认为1。 +* use_fp16(bool): 是否使用float16,默认为False。 +* use_tensorrt(bool): 是否使用Tensorrt,默认为False。 +* gpu_mem(int): GPU使用显存大小,默认为8000。 +* top_k(int): 指定返回的top_k,默认为1。 +* enable_mkldnn(bool): 是否使用MKLDNN,默认为False。 + + +### 3. 不同使用方式介绍 + +**我们提供两种不同的使用方式:1.使用python交互式编程 2.使用命令行方式** + +* 查看帮助信息 +```bash +ppvideo -h +``` + +* 使用用户指定的模型,你需要指定模型文件的路径 `model_file` 和参数文件路径 `params_file` + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_file='user-specified model path', + params_file='parmas path', use_gpu=False, use_tensorrt=False) +video_file = '' +result=clas.predict(video_file) +print(result) +``` + +###### bash +```bash +ppvideo --model_file='user-specified model path' --params_file='parmas path' --video_file='video path' +``` + +* 使用飞桨提供的推理模型进行预测,你需要通过指定 `model_name`参数来选择一个模型对ppvideo进行初始化,这时你不需要指定 `model_file`文件,你所选择的model预训练模型会自动下载到 `BASE_INFERENCE_MODEL_DIR`目录中以 `model_name`命名的文件夹下 + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False, use_tensorrt=False) +video_file = '' +result = clas.predict(video_file) +print(result) +``` + +###### bash +```bash +ppvideo --model_name='ppTSM' --video_file='video path' +``` + +* 你可以将 `np.ndarray`形式的数组作为输入,同样以 `--video_file=np.ndarray`方式指定即可 + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False, use_tensorrt=False) +video_file = np.ndarray +result = clas.predict(video_file) +``` + +###### bash +```bash +ppvideo --model_name='ppTSM' --video_file=np.ndarray +``` + +* 你可以将 `video_file`指定为一个包含多个视频文件的路径,同样也可以指定 `top_k`参数 + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False, use_tensorrt=False,top_k=5) +video_file = '' # it can be video_file folder path which contains all of videos you want to predict. +result = clas.predict(video_file) +print(result) +``` + +###### bash +```bash +paddleclas --model_name='ppTSM' --video_file='video path' --top_k=5 +``` + +* 你可以指定 `--label_name_path`为你自己的标签文件,**注意** 格式必须为(类别ID 类别名) + +``` +0 abseiling +1 air_drumming +2 answering_questions +3 applauding +4 applying_cream +5 archery +...... +``` + +* 如果你使用的是飞桨提供的推理模型,你不需要指定`label_name_path`,程序将默认使用`data/k400/Kinetics-400_label_list.txt`;如果你想使用你自己训练的模型,你需要提供你自己的label文件,否则模型只能输出预测的分数而没有类别名称 + +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_file= './inference.pdmodel',params_file = './inference.pdiparams',label_name_path='./data/k400/Kinetics-400_label_list.txt',use_gpu=False) +video_file = '' # it can be video_file folder path which contains all of videos you want to predict. +result = clas.predict(video_file) +print(result) +``` +###### bash +```bash +ppvideo --model_file= './inference.pdmodel' --params_file = './inference.pdiparams' --video_file='video path' --label_name_path='./data/k400/Kinetics-400_label_list.txt' +``` +###### python +```python +from ppvideo import PaddleVideo +clas = PaddleVideo(model_name='ppTSM',use_gpu=False) +video_file = '' # it can be video_file folder path which contains all of videos you want to predict. +result = clas.predict(video_file) +print(result) +``` +###### bash +```bash +ppvideo --model_name='ppTSM' --video_file='video path' +``` diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..feb782a18b3563e0fff20befd21f3e3700f529d0 --- /dev/null +++ b/main.py @@ -0,0 +1,139 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import random + +import numpy as np +import paddle + +from paddlevideo.tasks import (test_model, train_dali, train_model, + train_model_multigrid) +from paddlevideo.utils import get_config, get_dist_info + + +def parse_args(): + parser = argparse.ArgumentParser("PaddleVideo train script") + parser.add_argument('-c', + '--config', + type=str, + default='configs/example.yaml', + help='config file path') + parser.add_argument('-o', + '--override', + action='append', + default=[], + help='config options to be overridden') + parser.add_argument('--test', + action='store_true', + help='whether to test a model') + parser.add_argument('--train_dali', + action='store_true', + help='whether to use dali to speed up training') + parser.add_argument('--multigrid', + action='store_true', + help='whether to use multigrid training') + parser.add_argument('-w', + '--weights', + type=str, + help='weights for finetuning or testing') + parser.add_argument('--fleet', + action='store_true', + help='whether to use fleet run distributed training') + parser.add_argument('--amp', + action='store_true', + help='whether to open amp training.') + parser.add_argument( + '--amp_level', + type=str, + default=None, + help="optimize level when open amp training, can only be 'O1' or 'O2'.") + parser.add_argument( + '--validate', + action='store_true', + help='whether to evaluate the checkpoint during training') + parser.add_argument( + '--seed', + type=int, + default=1234, + help='fixed all random seeds when the program is running') + parser.add_argument( + '--max_iters', + type=int, + default=None, + help='max iterations when training(this arg only used in test_tipc)') + parser.add_argument( + '-p', + '--profiler_options', + type=str, + default=None, + help='The option of profiler, which should be in format ' + '\"key1=value1;key2=value2;key3=value3\".') + parser.add_argument('--use_npu', + type=bool, + default=False, + help='whether use npu.') + + args = parser.parse_args() + return args + + +def main(): + args = parse_args() + cfg = get_config(args.config, overrides=args.override) + + # set seed if specified + seed = args.seed + if seed is not None: + assert isinstance( + seed, int), f"seed must be a integer when specified, but got {seed}" + random.seed(seed) + np.random.seed(seed) + paddle.seed(seed) + + # set amp_level if amp is enabled + if args.amp: + if args.amp_level is None: + args.amp_level = 'O1' # set defaualt amp_level to 'O1' + else: + assert args.amp_level in [ + 'O1', 'O2' + ], f"amp_level must be 'O1' or 'O2' when amp enabled, but got {args.amp_level}." + + _, world_size = get_dist_info() + parallel = world_size != 1 + if parallel: + paddle.distributed.init_parallel_env() + + if args.test: + test_model(cfg, weights=args.weights, parallel=parallel) + elif args.train_dali: + train_dali(cfg, weights=args.weights, parallel=parallel) + elif args.multigrid: + train_model_multigrid(cfg, + world_size=world_size, + validate=args.validate) + else: + train_model(cfg, + weights=args.weights, + parallel=parallel, + validate=args.validate, + use_fleet=args.fleet, + use_amp=args.amp, + amp_level=args.amp_level, + max_iters=args.max_iters, + profiler_options=args.profiler_options) + + +if __name__ == '__main__': + main() diff --git a/paddlevideo/__init__.py b/paddlevideo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b03acf29696e71c21ed2d7bfc3a908b7f7c9c48 --- /dev/null +++ b/paddlevideo/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .version import paddlevideo_version diff --git a/paddlevideo/loader/__init__.py b/paddlevideo/loader/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4ed9b11a7018369a4df0253eb625d4bf88284f15 --- /dev/null +++ b/paddlevideo/loader/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .builder import build_dataset, build_dataloader, build_batch_pipeline +from .dataset import VideoDataset +from .dali_loader import TSN_Dali_loader, get_input_data + +__all__ = [ + 'build_dataset', 'build_dataloader', 'build_batch_pipeline', 'VideoDataset', + 'TSN_Dali_loader', 'get_input_data' +] diff --git a/paddlevideo/loader/builder.py b/paddlevideo/loader/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..23a65c3bf497881f355f85fb518d3f47e03b46aa --- /dev/null +++ b/paddlevideo/loader/builder.py @@ -0,0 +1,132 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import signal +import os +import paddle +from paddle.io import DataLoader, DistributedBatchSampler +from .registry import DATASETS, PIPELINES +from ..utils.build_utils import build +from .pipelines.compose import Compose +from paddlevideo.utils import get_logger +from paddlevideo.utils.multigrid import DistributedShortSampler +import numpy as np + +logger = get_logger("paddlevideo") + + +def build_pipeline(cfg): + """Build pipeline. + Args: + cfg (dict): root config dict. + """ + if cfg == None: + return + return Compose(cfg) + + +def build_dataset(cfg): + """Build dataset. + Args: + cfg (dict): root config dict. + + Returns: + dataset: dataset. + """ + #XXX: ugly code here! + cfg_dataset, cfg_pipeline = cfg + cfg_dataset.pipeline = build_pipeline(cfg_pipeline) + dataset = build(cfg_dataset, DATASETS, key="format") + return dataset + + +def build_batch_pipeline(cfg): + + batch_pipeline = build(cfg, PIPELINES) + return batch_pipeline + + +def build_dataloader(dataset, + batch_size, + num_workers, + places, + shuffle=True, + drop_last=True, + multigrid=False, + collate_fn_cfg=None, + **kwargs): + """Build Paddle Dataloader. + + XXX explain how the dataloader work! + + Args: + dataset (paddle.dataset): A PaddlePaddle dataset object. + batch_size (int): batch size on single card. + num_worker (int): num_worker + shuffle(bool): whether to shuffle the data at every epoch. + """ + if multigrid: + sampler = DistributedShortSampler(dataset, + batch_sizes=batch_size, + shuffle=True, + drop_last=True) + else: + sampler = DistributedBatchSampler(dataset, + batch_size=batch_size, + shuffle=shuffle, + drop_last=drop_last) + + #NOTE(shipping): when switch the mix operator on, such as: mixup, cutmix. + # batch like: [[img, label, attibute, ...], [imgs, label, attribute, ...], ...] will recollate to: + # [[img, img, ...], [label, label, ...], [attribute, attribute, ...], ...] as using numpy.transpose. + + def mix_collate_fn(batch): + pipeline = build_batch_pipeline(collate_fn_cfg) + batch = pipeline(batch) + slots = [] + for items in batch: + for i, item in enumerate(items): + if len(slots) < len(items): + slots.append([item]) + else: + slots[i].append(item) + return [np.stack(slot, axis=0) for slot in slots] + + #if collate_fn_cfg is not None: + #ugly code here. collate_fn is mix op config + # collate_fn = mix_collate_fn(collate_fn_cfg) + + data_loader = DataLoader( + dataset, + batch_sampler=sampler, + places=places, + num_workers=num_workers, + collate_fn=mix_collate_fn if collate_fn_cfg is not None else None, + return_list=True, + **kwargs) + + return data_loader + + +def term_mp(sig_num, frame): + """ kill all child processes + """ + pid = os.getpid() + pgid = os.getpgid(os.getpid()) + logger.info("main proc {} exit, kill process group " "{}".format(pid, pgid)) + os.killpg(pgid, signal.SIGKILL) + return + + +signal.signal(signal.SIGINT, term_mp) +signal.signal(signal.SIGTERM, term_mp) diff --git a/paddlevideo/loader/dali_loader.py b/paddlevideo/loader/dali_loader.py new file mode 100644 index 0000000000000000000000000000000000000000..73fe64fcc5d3a0340b937f6128fdbc8c82cd71c7 --- /dev/null +++ b/paddlevideo/loader/dali_loader.py @@ -0,0 +1,208 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import math + +from paddle.distributed import ParallelEnv +import paddle.distributed as dist +from paddle.fluid.dygraph import to_variable +from paddlevideo.utils import get_logger +logger = get_logger("paddlevideo") + +try: + from nvidia.dali.pipeline import Pipeline + import nvidia.dali.ops as ops + import nvidia.dali.types as types + import tempfile + from nvidia.dali.plugin.paddle import DALIGenericIterator +except: + Pipeline = object + logger.info( + "DALI is not installed, you can improve performance if use DALI") + + +def get_input_data(data): + return to_variable(data[0]['image']), to_variable(data[0]['label']) + + +class TSN_Dali_loader(object): + def __init__(self, cfg): + self.batch_size = cfg.batch_size + self.file_path = cfg.file_path + + self.num_seg = cfg.num_seg + self.seglen = cfg.seglen + self.short_size = cfg.short_size + self.target_size = cfg.target_size + + # set num_shards and shard_id when distributed training is implemented + self.num_shards = dist.get_world_size() + self.shard_id = ParallelEnv().local_rank + self.dali_mean = cfg.mean * (self.num_seg * self.seglen) + self.dali_std = cfg.std * (self.num_seg * self.seglen) + + def build_dali_reader(self): + """ + build dali training reader + """ + def reader_(): + with open(self.file_path) as flist: + full_lines = [line for line in flist] + if (not hasattr(reader_, 'seed')): + reader_.seed = 0 + random.Random(reader_.seed).shuffle(full_lines) + logger.info(f"reader shuffle seed: {reader_.seed}.") + if reader_.seed is not None: + reader_.seed += 1 + + per_node_lines = int( + math.ceil(len(full_lines) * 1.0 / self.num_shards)) + total_lines = per_node_lines * self.num_shards + + # aligned full_lines so that it can evenly divisible + full_lines += full_lines[:(total_lines - len(full_lines))] + assert len(full_lines) == total_lines + + # trainer get own sample + lines = full_lines[self.shard_id:total_lines:self.num_shards] + assert len(lines) == per_node_lines + + logger.info( + f"shard_id: {self.shard_id}, trainer_count: {self.num_shards}" + ) + logger.info( + f"read videos from {self.shard_id * per_node_lines}, " + f"length: {per_node_lines}, " + f"lines length: {len(lines)}, " + f"total: {len(full_lines)}") + + video_files = '' + for item in lines: + video_files += item + tf = tempfile.NamedTemporaryFile() + tf.write(str.encode(video_files)) + tf.flush() + video_files = tf.name + + device_id = ParallelEnv().local_rank + logger.info(f'---------- device_id: {device_id} -----------') + + pipe = VideoPipe(batch_size=self.batch_size, + num_threads=1, + device_id=device_id, + file_list=video_files, + sequence_length=self.num_seg * self.seglen, + num_seg=self.num_seg, + seg_length=self.seglen, + resize_shorter_scale=self.short_size, + crop_target_size=self.target_size, + is_training=True, + num_shards=self.num_shards, + shard_id=self.shard_id, + dali_mean=self.dali_mean, + dali_std=self.dali_std) + + logger.info( + 'initializing dataset, it will take several minutes if it is too large .... ' + ) + video_loader = DALIGenericIterator([pipe], ['image', 'label'], + len(lines), + dynamic_shape=True, + auto_reset=True) + + return video_loader + + dali_reader = reader_() + return dali_reader + + +class VideoPipe(Pipeline): + def __init__(self, + batch_size, + num_threads, + device_id, + file_list, + sequence_length, + num_seg, + seg_length, + resize_shorter_scale, + crop_target_size, + is_training=False, + initial_prefetch_size=20, + num_shards=1, + shard_id=0, + dali_mean=0., + dali_std=1.0): + super(VideoPipe, self).__init__(batch_size, num_threads, device_id) + self.input = ops.VideoReader(device="gpu", + file_list=file_list, + sequence_length=sequence_length, + num_seg=num_seg, + seg_length=seg_length, + is_training=is_training, + num_shards=num_shards, + shard_id=shard_id, + random_shuffle=is_training, + initial_fill=initial_prefetch_size) + # the sequece data read by ops.VideoReader is of shape [F, H, W, C] + # Because the ops.Resize does not support sequence data, + # it will be transposed into [H, W, F, C], + # then reshaped to [H, W, FC], and then resized like a 2-D image. + self.transpose = ops.Transpose(device="gpu", perm=[1, 2, 0, 3]) + self.reshape = ops.Reshape(device="gpu", + rel_shape=[1.0, 1.0, -1], + layout='HWC') + self.resize = ops.Resize(device="gpu", + resize_shorter=resize_shorter_scale) + # crops and mirror are applied by ops.CropMirrorNormalize. + # Normalization will be implemented in paddle due to the difficulty of dimension broadcast, + # It is not sure whether dimension broadcast can be implemented correctly by dali, just take the Paddle Op instead. + self.pos_rng_x = ops.Uniform(range=(0.0, 1.0)) + self.pos_rng_y = ops.Uniform(range=(0.0, 1.0)) + self.mirror_generator = ops.Uniform(range=(0.0, 1.0)) + self.cast_mirror = ops.Cast(dtype=types.DALIDataType.INT32) + self.crop_mirror_norm = ops.CropMirrorNormalize( + device="gpu", + crop=[crop_target_size, crop_target_size], + mean=dali_mean, + std=dali_std) + self.reshape_back = ops.Reshape( + device="gpu", + shape=[num_seg, seg_length * 3, crop_target_size, crop_target_size], + layout='FCHW') + self.cast_label = ops.Cast(device="gpu", dtype=types.DALIDataType.INT64) + + def define_graph(self): + output, label = self.input(name="Reader") + output = self.transpose(output) + output = self.reshape(output) + + output = self.resize(output) + output = output / 255. + pos_x = self.pos_rng_x() + pos_y = self.pos_rng_y() + mirror_flag = self.mirror_generator() + mirror_flag = (mirror_flag > 0.5) + mirror_flag = self.cast_mirror(mirror_flag) + output = self.crop_mirror_norm(output, + crop_pos_x=pos_x, + crop_pos_y=pos_y, + mirror=mirror_flag) + output = self.reshape_back(output) + label = self.cast_label(label) + return output, label + + def __len__(self): + return self.epoch_size() diff --git a/paddlevideo/loader/dataset/MRI.py b/paddlevideo/loader/dataset/MRI.py new file mode 100644 index 0000000000000000000000000000000000000000..990cb87bd4f164b8b9eafc91250ffe90f0673649 --- /dev/null +++ b/paddlevideo/loader/dataset/MRI.py @@ -0,0 +1,109 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class MRIDataset(BaseDataset): + """Rawframe dataset for action recognition. + The dataset loads raw frames from frame files, and apply specified transform operatation them. + The indecx file is a text file with multiple lines, and each line indicates the directory of frames of a video, toatl frames of the video, and its label, which split with a whitespace. + Example of an index file: + + .. code-block:: txt + + file_path-1 150 1 + file_path-2 160 1 + file_path-3 170 2 + file_path-4 180 2 + + Args: + file_path (str): Path to the index file. + pipeline(XXX): + data_prefix (str): directory path of the data. Default: None. + test_mode (bool): Whether to bulid the test dataset. Default: False. + suffix (str): suffix of file. Default: 'img_{:05}.jpg'. + + """ + def __init__(self, + file_path, + pipeline, + num_retries=5, + data_prefix=None, + test_mode=False, + suffix='img_{:05}.jpg'): + self.num_retries = num_retries + self.suffix = suffix + super().__init__(file_path, pipeline, data_prefix, test_mode) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split() + frame_dir, frames_len, labels = line_split + if self.data_prefix is not None: + frame_dir = osp.join(self.data_prefix, frame_dir) + info.append( + dict( + frame_dir=frame_dir, + #suffix=self.suffix, + frames_len=frames_len, + labels=int(labels))) + return info + + def prepare_train(self, idx): + """Prepare the frames for training/valid gisven index. """ + #Try to catch Exception caused by reading missing frames files + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['frame_dir'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return np.array(results['imgs']), np.array([results['labels']]) + + def prepare_test(self, idx): + """Prepare the frames for test given index. """ + #Try to catch Exception caused by reading missing frames files + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['frame_dir'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return np.array(results['imgs']), np.array([results['labels']]) diff --git a/paddlevideo/loader/dataset/MRI_SlowFast.py b/paddlevideo/loader/dataset/MRI_SlowFast.py new file mode 100644 index 0000000000000000000000000000000000000000..db905e4e4bd6bd527609cc2c52aaf7f6c6b96e3f --- /dev/null +++ b/paddlevideo/loader/dataset/MRI_SlowFast.py @@ -0,0 +1,111 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class SFMRIDataset(BaseDataset): + """Rawframe dataset for action recognition. + The dataset loads raw frames from frame files, and apply specified transform operatation them. + The indecx file is a text file with multiple lines, and each line indicates the directory of frames of a video, toatl frames of the video, and its label, which split with a whitespace. + Example of an index file: + + .. code-block:: txt + + file_path-1 150 1 + file_path-2 160 1 + file_path-3 170 2 + file_path-4 180 2 + + Args: + file_path (str): Path to the index file. + pipeline(XXX): + data_prefix (str): directory path of the data. Default: None. + test_mode (bool): Whether to bulid the test dataset. Default: False. + suffix (str): suffix of file. Default: 'img_{:05}.jpg'. + + """ + def __init__(self, + file_path, + pipeline, + num_retries=5, + data_prefix=None, + test_mode=False, + suffix='img_{:05}.jpg'): + self.num_retries = num_retries + self.suffix = suffix + super().__init__(file_path, pipeline, data_prefix, test_mode) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split() + frame_dir, frames_len, labels = line_split + if self.data_prefix is not None: + frame_dir = osp.join(self.data_prefix, frame_dir) + info.append( + dict( + frame_dir=frame_dir, + #suffix=self.suffix, + frames_len=frames_len, + labels=int(labels))) + return info + + def prepare_train(self, idx): + """Prepare the frames for training/valid gisven index. """ + #Try to catch Exception caused by reading missing frames files + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['frame_dir'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return np.array(results['imgs'][0]), np.array( + results['imgs'][1]), np.array([results['labels']]) + + def prepare_test(self, idx): + """Prepare the frames for test given index. """ + #Try to catch Exception caused by reading missing frames files + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['frame_dir'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return np.array(results['imgs'][0]), np.array( + results['imgs'][1]), np.array([results['labels']]) diff --git a/paddlevideo/loader/dataset/__init__.py b/paddlevideo/loader/dataset/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..78a1f7aa6876ac3d33bb46edb7e0c9118a5858c6 --- /dev/null +++ b/paddlevideo/loader/dataset/__init__.py @@ -0,0 +1,37 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .actbert_dataset import ActBertDataset +from .ava_dataset import AVADataset +from .bmn_dataset import BMNDataset +from .davis_dataset import DavisDataset +from .feature import FeatureDataset +from .frame import FrameDataset, FrameDataset_Sport +from .MRI import MRIDataset +from .MRI_SlowFast import SFMRIDataset +from .msrvtt import MSRVTTDataset +from .actbert_dataset import ActBertDataset +from .asrf_dataset import ASRFDataset +from .ms_tcn_dataset import MSTCNDataset +from .oxford import MonoDataset +from .skeleton import SkeletonDataset +from .slowfast_video import SFVideoDataset +from .video import VideoDataset + +__all__ = [ + 'VideoDataset', 'FrameDataset', 'SFVideoDataset', 'BMNDataset', + 'FeatureDataset', 'SkeletonDataset', 'AVADataset', 'MonoDataset', + 'MSRVTTDataset', 'ActBertDataset', 'DavisDataset', 'MRIDataset', + 'SFMRIDataset', 'FrameDataset_Sport', 'MSTCNDataset', 'ASRFDataset' +] diff --git a/paddlevideo/loader/dataset/actbert_dataset.py b/paddlevideo/loader/dataset/actbert_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..51ceb80953b017fc5dbf6e282f13a0e264fb27f1 --- /dev/null +++ b/paddlevideo/loader/dataset/actbert_dataset.py @@ -0,0 +1,72 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np +try: + import lmdb +except ImportError as e: + print(f"{e}, [lmdb] package and it's dependencies is required for ActBERT.") +import pickle +import json +try: + from paddlenlp.transformers import BertTokenizer +except ImportError as e: + print( + f"{e}, [paddlenlp] package and it's dependencies is required for ActBERT." + ) +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class ActBertDataset(BaseDataset): + """ActBert dataset. + """ + def __init__( + self, + file_path, + pipeline, + bert_model="bert-base-uncased", + data_prefix=None, + test_mode=False, + ): + self.bert_model = bert_model + super().__init__(file_path, pipeline, data_prefix, test_mode) + + def load_file(self): + """Load index file to get video information.""" + feature_data = np.load(self.file_path, allow_pickle=True) + self.tokenizer = BertTokenizer.from_pretrained(self.bert_model, + do_lower_case=True) + self.info = [] + for item in feature_data: + self.info.append(dict(feature=item, tokenizer=self.tokenizer)) + return self.info + + def prepare_train(self, idx): + """Prepare the frames for training/valid given index. """ + results = copy.deepcopy(self.info[idx]) + #print('==results==', results) + results = self.pipeline(results) + return results['features'] + + def prepare_test(self, idx): + """Prepare the frames for test given index. """ + pass diff --git a/paddlevideo/loader/dataset/asrf_dataset.py b/paddlevideo/loader/dataset/asrf_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..15bd35a3c6e3317778d1cef7942f6a8e7a56de6a --- /dev/null +++ b/paddlevideo/loader/dataset/asrf_dataset.py @@ -0,0 +1,104 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import copy + +import os +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class ASRFDataset(BaseDataset): + """Video dataset for action segmentation. + """ + + def __init__( + self, + file_path, + pipeline, + feature_path, + label_path, + boundary_path, + **kwargs, + ): + super().__init__(file_path, pipeline, **kwargs) + self.label_path = label_path + self.boundary_path = boundary_path + self.feature_path = feature_path + + def load_file(self): + """Load index file to get video information.""" + file_ptr = open(self.file_path, 'r') + info = file_ptr.read().split('\n')[:-1] + file_ptr.close() + return info + + def prepare_train(self, idx): + """TRAIN & VALID: Prepare data for training/valid given the index.""" + results = {} + video_name = self.info[idx] + # load video feature + file_name = video_name.split('.')[0] + ".npy" + feat_file_path = os.path.join(self.feature_path, file_name) + #TODO: check path + video_feat = np.load(feat_file_path) + + # load label + file_name = video_name.split('.')[0] + ".npy" + label_file_path = os.path.join(self.label_path, file_name) + label = np.load(label_file_path).astype(np.int64) + + # load boundary + file_name = video_name.split('.')[0] + ".npy" + boundary_file_path = os.path.join(self.boundary_path, file_name) + boundary = np.expand_dims(np.load(boundary_file_path),axis=0).astype(np.float32) + + results['video_feat'] = copy.deepcopy(video_feat) + results['video_label'] = copy.deepcopy(label) + results['video_boundary'] = copy.deepcopy(boundary) + + results = self.pipeline(results) + return results['video_feat'], results['video_label'], results['video_boundary'] + + def prepare_test(self, idx): + """TEST: Prepare the data for test given the index.""" + results = {} + video_name = self.info[idx] + # load video feature + file_name = video_name.split('.')[0] + ".npy" + feat_file_path = os.path.join(self.feature_path, file_name) + #TODO: check path + video_feat = np.load(feat_file_path) + + # load label + file_name = video_name.split('.')[0] + ".npy" + label_file_path = os.path.join(self.label_path, file_name) + label = np.load(label_file_path).astype(np.int64) + + # load boundary + file_name = video_name.split('.')[0] + ".npy" + boundary_file_path = os.path.join(self.boundary_path, file_name) + boundary = np.expand_dims(np.load(boundary_file_path),axis=0).astype(np.float32) + + results['video_feat'] = copy.deepcopy(video_feat) + results['video_label'] = copy.deepcopy(label) + results['video_boundary'] = copy.deepcopy(boundary) + + results = self.pipeline(results) + return results['video_feat'], results['video_label'], results['video_boundary'] diff --git a/paddlevideo/loader/dataset/ava_dataset.py b/paddlevideo/loader/dataset/ava_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..744e15bb6808d7900d2fd9af9d8e3f6c40ed08c4 --- /dev/null +++ b/paddlevideo/loader/dataset/ava_dataset.py @@ -0,0 +1,249 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np +import sys +import os +import pickle +from datetime import datetime +from ...metrics.ava_utils import ava_evaluate_results +from ..registry import DATASETS +from .base import BaseDataset +from collections import defaultdict + + +@DATASETS.register() +class AVADataset(BaseDataset): + """AVA dataset for spatial temporal detection. + the dataset loads raw frames, bounding boxes, proposals and applies + transformations to return the frame tensors and other information. + """ + + _FPS = 30 + + def __init__(self, + pipeline, + file_path=None, + exclude_file=None, + label_file=None, + suffix='{:05}.jpg', + proposal_file=None, + person_det_score_thr=0.9, + num_classes=81, + data_prefix=None, + test_mode=False, + num_max_proposals=1000, + timestamp_start=900, + timestamp_end=1800): + self.custom_classes = None + self.exclude_file = exclude_file + self.label_file = label_file + self.proposal_file = proposal_file + assert 0 <= person_det_score_thr <= 1, ( + 'The value of ' + 'person_det_score_thr should in [0, 1]. ') + self.person_det_score_thr = person_det_score_thr + self.num_classes = num_classes + self.suffix = suffix + self.num_max_proposals = num_max_proposals + self.timestamp_start = timestamp_start + self.timestamp_end = timestamp_end + super().__init__( + file_path, + pipeline, + data_prefix, + test_mode, + ) + if self.proposal_file is not None: + self.proposals = self._load(self.proposal_file) + else: + self.proposals = None + if not test_mode: + valid_indexes = self.filter_exclude_file() + self.info = self.info = [self.info[i] for i in valid_indexes] + + def _load(self, path): + f = open(path, 'rb') + res = pickle.load(f) + f.close() + return res + + def parse_img_record(self, img_records): + bboxes, labels, entity_ids = [], [], [] + while len(img_records) > 0: + img_record = img_records[0] + num_img_records = len(img_records) + selected_records = list( + filter( + lambda x: np.array_equal(x['entity_box'], img_record[ + 'entity_box']), img_records)) + num_selected_records = len(selected_records) + img_records = list( + filter( + lambda x: not np.array_equal(x['entity_box'], img_record[ + 'entity_box']), img_records)) + assert len(img_records) + num_selected_records == num_img_records + + bboxes.append(img_record['entity_box']) + valid_labels = np.array([ + selected_record['label'] for selected_record in selected_records + ]) + + label = np.zeros(self.num_classes, dtype=np.float32) + label[valid_labels] = 1. + + labels.append(label) + entity_ids.append(img_record['entity_id']) + + bboxes = np.stack(bboxes) + labels = np.stack(labels) + entity_ids = np.stack(entity_ids) + return bboxes, labels, entity_ids + + def filter_exclude_file(self): + valid_indexes = [] + if self.exclude_file is None: + valid_indexes = list(range(len(self.info))) + else: + exclude_video_infos = [ + x.strip().split(',') for x in open(self.exclude_file) + ] + for i, video_info in enumerate(self.info): + valid_indexes.append(i) + for video_id, timestamp in exclude_video_infos: + if (video_info['video_id'] == video_id + and video_info['timestamp'] == int(timestamp)): + valid_indexes.pop() + break + return valid_indexes + + def load_file(self): + """Load index file to get video information.""" + info = [] + records_dict_by_img = defaultdict(list) + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split(',') + + video_id = line_split[0] + timestamp = int(line_split[1]) + img_key = f'{video_id},{timestamp:04d}' + + entity_box = np.array(list(map(float, line_split[2:6]))) + label = int(line_split[6]) + entity_id = int(line_split[7]) + shot_info = (0, (self.timestamp_end - self.timestamp_start) * + self._FPS) + + video_info = dict(video_id=video_id, + timestamp=timestamp, + entity_box=entity_box, + label=label, + entity_id=entity_id, + shot_info=shot_info) + records_dict_by_img[img_key].append(video_info) + + for img_key in records_dict_by_img: + video_id, timestamp = img_key.split(',') + bboxes, labels, entity_ids = self.parse_img_record( + records_dict_by_img[img_key]) + ann = dict(gt_bboxes=bboxes, + gt_labels=labels, + entity_ids=entity_ids) + frame_dir = video_id + if self.data_prefix is not None: + frame_dir = osp.join(self.data_prefix, frame_dir) + video_info = dict(frame_dir=frame_dir, + video_id=video_id, + timestamp=int(timestamp), + img_key=img_key, + shot_info=shot_info, + fps=self._FPS, + ann=ann) + info.append(video_info) + + return info + + def prepare_train(self, idx): + results = copy.deepcopy(self.info[idx]) + img_key = results['img_key'] + + results['suffix'] = self.suffix + results['timestamp_start'] = self.timestamp_start + results['timestamp_end'] = self.timestamp_end + + if self.proposals is not None: + if img_key not in self.proposals: + results['proposals'] = np.array([[0, 0, 1, 1]]) + results['scores'] = np.array([1]) + else: + proposals = self.proposals[img_key] + assert proposals.shape[-1] in [4, 5] + if proposals.shape[-1] == 5: + thr = min(self.person_det_score_thr, max(proposals[:, 4])) + positive_inds = (proposals[:, 4] >= thr) + proposals = proposals[positive_inds] + proposals = proposals[:self.num_max_proposals] + results['proposals'] = proposals[:, :4] + results['scores'] = proposals[:, 4] + else: + proposals = proposals[:self.num_max_proposals] + results['proposals'] = proposals + + ann = results.pop('ann') + results['gt_bboxes'] = ann['gt_bboxes'] + results['gt_labels'] = ann['gt_labels'] + results['entity_ids'] = ann['entity_ids'] + + #ret = self.pipeline(results, "") + ret = self.pipeline(results) + #padding for dataloader + len_proposals = ret['proposals'].shape[0] + len_gt_bboxes = ret['gt_bboxes'].shape[0] + len_gt_labels = ret['gt_labels'].shape[0] + len_scores = ret['scores'].shape[0] + len_entity_ids = ret['entity_ids'].shape[0] + padding_len = 128 + ret['proposals'] = self.my_padding_2d(ret['proposals'], padding_len) + ret['gt_bboxes'] = self.my_padding_2d(ret['gt_bboxes'], padding_len) + ret['gt_labels'] = self.my_padding_2d(ret['gt_labels'], padding_len) + ret['scores'] = self.my_padding_1d(ret['scores'], padding_len) + ret['entity_ids'] = self.my_padding_1d(ret['entity_ids'], padding_len) + return ret['imgs'][0], ret['imgs'][1], ret['proposals'], ret[ + 'gt_bboxes'], ret['gt_labels'], ret['scores'], ret[ + 'entity_ids'], np.array( + ret['img_shape'], dtype=int + ), idx, len_proposals, len_gt_bboxes, len_gt_labels, len_scores, len_entity_ids + + def my_padding_2d(self, feat, max_len): + feat_add = np.zeros((max_len - feat.shape[0], feat.shape[1]), + dtype=np.float32) + feat_pad = np.concatenate((feat, feat_add), axis=0) + return feat_pad + + def my_padding_1d(self, feat, max_len): + feat_add = np.zeros((max_len - feat.shape[0]), dtype=np.float32) + feat_pad = np.concatenate((feat, feat_add), axis=0) + return feat_pad + + def prepare_test(self, idx): + return self.prepare_train(idx) + + def evaluate(self, results): + return ava_evaluate_results(self.info, len(self), results, + self.custom_classes, self.label_file, + self.file_path, self.exclude_file) diff --git a/paddlevideo/loader/dataset/base.py b/paddlevideo/loader/dataset/base.py new file mode 100644 index 0000000000000000000000000000000000000000..2549dc4111a3ba78a85f0088d07458d3907e7abd --- /dev/null +++ b/paddlevideo/loader/dataset/base.py @@ -0,0 +1,80 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import numpy as np +from abc import ABC, abstractmethod + +import paddle +from paddle.io import Dataset + + +class BaseDataset(Dataset, ABC): + """Base class for datasets + + All datasets should subclass it. + All subclass should overwrite: + + - Method: `load_file`, load info from index file. + - Method: `prepare_train`, providing train data. + - Method: `prepare_test`, providing test data. + + Args: + file_path (str): index file path. + pipeline (Sequence XXX) + data_prefix (str): directory path of the data. Default: None. + test_mode (bool): whether to build test dataset. Default: False. + + """ + def __init__(self, file_path, pipeline, data_prefix=None, test_mode=False): + super().__init__() + self.file_path = file_path + self.data_prefix = osp.realpath(data_prefix) if \ + data_prefix is not None and osp.isdir(data_prefix) else data_prefix + self.test_mode = test_mode + self.pipeline = pipeline + self.info = self.load_file() + + @abstractmethod + def load_file(self): + """load the video information from the index file path.""" + pass + + def prepare_train(self, idx): + """TRAIN & VALID. Prepare the data for training/valid given the index.""" + #Note: For now, paddle.io.DataLoader cannot support dict type retval, so convert to list here + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + #unsqueeze label to list + return results['imgs'], np.array([results['labels']]) + + def prepare_test(self, idx): + """TEST: Prepare the data for test given the index.""" + #Note: For now, paddle.io.DataLoader cannot support dict type retval, so convert to list here + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + #unsqueeze label to list + return results['imgs'], np.array([results['labels']]) + + def __len__(self): + """get the size of the dataset.""" + return len(self.info) + + def __getitem__(self, idx): + """ Get the sample for either training or testing given index""" + if self.test_mode: + return self.prepare_test(idx) + else: + return self.prepare_train(idx) diff --git a/paddlevideo/loader/dataset/bmn_dataset.py b/paddlevideo/loader/dataset/bmn_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..44c7651914233b7dd659b10d55e24aaafd71f555 --- /dev/null +++ b/paddlevideo/loader/dataset/bmn_dataset.py @@ -0,0 +1,72 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import json + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class BMNDataset(BaseDataset): + """Video dataset for action localization. + """ + def __init__( + self, + file_path, + pipeline, + subset, + **kwargs, + ): + self.subset = subset + super().__init__(file_path, pipeline, **kwargs) + + def load_file(self): + """Load index file to get video information.""" + info = [] + annos = json.load(open(self.file_path)) + for video_name in annos.keys(): + video_subset = annos[video_name]["subset"] + if self.subset in video_subset: + info.append( + dict( + video_name=video_name, + video_info=annos[video_name], + )) + #sort by video_name + sort_f = lambda elem: elem['video_name'] + info.sort(key=sort_f) + #add video_idx to info + for idx, elem in enumerate(info): + info[idx]['video_idx'] = idx + logger.info("{} subset video numbers: {}".format( + self.subset, len(info))) + return info + + def prepare_train(self, idx): + """TRAIN & VALID: Prepare data for training/valid given the index.""" + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + return results['video_feat'], results['gt_iou_map'], results['gt_start'],\ + results['gt_end'] + + def prepare_test(self, idx): + """TEST: Prepare the data for test given the index.""" + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + return results['video_feat'], results['gt_iou_map'], results['gt_start'], \ + results['gt_end'], results['video_idx'] diff --git a/paddlevideo/loader/dataset/davis_dataset.py b/paddlevideo/loader/dataset/davis_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..20a27597149898d39e0a8426ab0d66aaf4fe4137 --- /dev/null +++ b/paddlevideo/loader/dataset/davis_dataset.py @@ -0,0 +1,189 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import os.path as osp +import copy +import random +import numpy as np +import shutil +from PIL import Image +import cv2 +from paddle.io import Dataset + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +class VOS_Test(Dataset): + """process frames in each video + """ + def __init__(self, + image_root, + label_root, + seq_name, + images, + labels, + pipeline=None, + rgb=False, + resolution=None): + self.image_root = image_root + self.label_root = label_root + self.seq_name = seq_name + self.images = images # image file list + self.labels = labels + self.obj_num = 1 + self.num_frame = len(self.images) + self.pipeline = pipeline + self.rgb = rgb + self.resolution = resolution + + self.obj_nums = [] + temp_obj_num = 0 + for img_name in self.images: + self.obj_nums.append(temp_obj_num) + current_label_name = img_name.split('.')[0] + '.png' + if current_label_name in self.labels: + current_label = self.read_label(current_label_name) + if temp_obj_num < np.unique( + current_label)[-1]: #get object number from label_id + temp_obj_num = np.unique(current_label)[-1] + + def __len__(self): + return len(self.images) + + def read_image(self, idx): + img_name = self.images[idx] + img_path = os.path.join(self.image_root, self.seq_name, img_name) + img = cv2.imread(img_path) + img = np.array(img, dtype=np.float32) + if self.rgb: + img = img[:, :, [2, 1, 0]] + return img + + def read_label(self, label_name): + label_path = os.path.join(self.label_root, self.seq_name, label_name) + label = Image.open(label_path) + label = np.array(label, dtype=np.uint8) + return label + + def __getitem__(self, idx): + img_name = self.images[idx] + current_img = self.read_image(idx) + current_img = np.array(current_img) + height, width, channels = current_img.shape + if self.resolution is not None: + width = int(np.ceil(float(width) * self.resolution / float(height))) + height = int(self.resolution) + + current_label_name = img_name.split('.')[0] + '.png' + obj_num = self.obj_nums[idx] + + if current_label_name in self.labels: + current_label = self.read_label(current_label_name) + current_label = np.array(current_label) + sample = { + 'current_img': current_img, + 'current_label': current_label + } + else: + sample = { + 'current_img': current_img + } #only the first frame contains label + + sample['meta'] = { + 'seq_name': self.seq_name, + 'frame_num': self.num_frame, + 'obj_num': obj_num, + 'current_name': img_name, + 'height': height, + 'width': width, + 'flip': False + } + if self.pipeline is not None: + sample = self.pipeline(sample) + for s in sample: + s['current_img'] = np.array(s['current_img']) + if 'current_label' in s.keys(): + s['current_label'] = s['current_label'] + return sample + + +@DATASETS.register() +class DavisDataset(BaseDataset): + """Davis 2017 dataset. + """ + def __init__( + self, + file_path, + result_root, + pipeline, + data_prefix=None, + test_mode=False, + year=2017, + rgb=False, + resolution='480p', + ): + self.rgb = rgb + self.result_root = result_root + self.resolution = resolution + self.year = year + self.spt = 'val' if test_mode else 'train' + super().__init__(file_path, pipeline, data_prefix, test_mode) + + def load_file(self): + self.image_root = os.path.join(self.file_path, 'JPEGImages', + self.resolution) + self.label_root = os.path.join(self.file_path, 'Annotations', + self.resolution) + seq_names = [] + with open( + os.path.join(self.file_path, 'ImageSets', str(self.year), + self.spt + '.txt')) as f: + seqs_tmp = f.readlines() + seqs_tmp = list(map(lambda elem: elem.strip(), seqs_tmp)) + seq_names.extend(seqs_tmp) + self.info = list(np.unique(seq_names)) + return self.info + + def prepare_test(self, idx): + seq_name = self.info[idx] #video name + images = list( + np.sort(os.listdir(os.path.join(self.image_root, seq_name)))) + labels = [images[0].replace('jpg', 'png')] #we have first frame target + + # copy first frame target + if not os.path.isfile( + os.path.join(self.result_root, seq_name, labels[0])): + if not os.path.exists(os.path.join(self.result_root, seq_name)): + os.makedirs(os.path.join(self.result_root, seq_name)) + source_label_path = os.path.join(self.label_root, seq_name, + labels[0]) + result_label_path = os.path.join(self.result_root, seq_name, + labels[0]) + + shutil.copy(source_label_path, result_label_path) + + seq_dataset = VOS_Test(self.image_root, + self.label_root, + seq_name, + images, + labels, + self.pipeline, + rgb=self.rgb, + resolution=480) + return seq_dataset diff --git a/paddlevideo/loader/dataset/feature.py b/paddlevideo/loader/dataset/feature.py new file mode 100644 index 0000000000000000000000000000000000000000..7bf4cd604a801f2b9dc7584ad614fdb1b85356fb --- /dev/null +++ b/paddlevideo/loader/dataset/feature.py @@ -0,0 +1,68 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import copy +import os.path as osp + +from ..registry import DATASETS +from .base import BaseDataset + + +@DATASETS.register() +class FeatureDataset(BaseDataset): + """Feature dataset for action recognition + Example:(TODO) + Args:(TODO) + """ + def __init__( + self, + file_path, + pipeline, + data_prefix=None, + test_mode=False, + suffix=None, + ): + self.suffix = suffix + super().__init__(file_path, pipeline, data_prefix, test_mode) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + filename = line.strip() + if self.data_prefix is not None: + filename = osp.join(self.data_prefix, filename) + if self.suffix is not None: + filename = filename + self.suffix + + info.append(dict(filename=filename)) + return info + + def prepare_train(self, idx): + """TRAIN & VALID. Prepare the data for training/valid given the index.""" + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + + return results['rgb_data'], results['rgb_len'], results[ + 'rgb_mask'], results['audio_data'], results['audio_len'], results[ + 'audio_mask'], results['labels'] + + def prepare_test(self, idx): + """TEST. Prepare the data for testing given the index.""" + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + + return results['rgb_data'], results['rgb_len'], results[ + 'rgb_mask'], results['audio_data'], results['audio_len'], results[ + 'audio_mask'], results['labels'] diff --git a/paddlevideo/loader/dataset/frame.py b/paddlevideo/loader/dataset/frame.py new file mode 100644 index 0000000000000000000000000000000000000000..b02f526595527a101a34e5e1a7fdd2de092111a3 --- /dev/null +++ b/paddlevideo/loader/dataset/frame.py @@ -0,0 +1,177 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class FrameDataset(BaseDataset): + """Rawframe dataset for action recognition. + The dataset loads raw frames from frame files, and apply specified transform operatation them. + The indecx file is a text file with multiple lines, and each line indicates the directory of frames of a video, toatl frames of the video, and its label, which split with a whitespace. + Example of an index file: + + .. code-block:: txt + + file_path-1 150 1 + file_path-2 160 1 + file_path-3 170 2 + file_path-4 180 2 + + Args: + file_path (str): Path to the index file. + pipeline(XXX): + data_prefix (str): directory path of the data. Default: None. + test_mode (bool): Whether to bulid the test dataset. Default: False. + suffix (str): suffix of file. Default: 'img_{:05}.jpg'. + + """ + def __init__(self, + file_path, + pipeline, + num_retries=5, + data_prefix=None, + test_mode=False, + suffix='img_{:05}.jpg'): + self.num_retries = num_retries + self.suffix = suffix + super().__init__(file_path, pipeline, data_prefix, test_mode) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split() + frame_dir, frames_len, labels = line_split + if self.data_prefix is not None: + frame_dir = osp.join(self.data_prefix, frame_dir) + info.append( + dict(frame_dir=frame_dir, + suffix=self.suffix, + frames_len=frames_len, + labels=int(labels))) + return info + + def prepare_train(self, idx): + """Prepare the frames for training/valid given index. """ + #Try to catch Exception caused by reading missing frames files + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['frame_dir'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) + + def prepare_test(self, idx): + """Prepare the frames for test given index. """ + #Try to catch Exception caused by reading missing frames files + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['frame_dir'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) + + +@DATASETS.register() +class FrameDataset_Sport(BaseDataset): + """Video dataset for action recognition + The dataset loads raw videos and apply specified transforms on them. + The index file is a file with multiple lines, and each line indicates + a sample video with the filepath and label, which are split with a whitesapce. + Example of a inde file: + .. code-block:: txt + path/000.mp4 1 + path/001.mp4 1 + path/002.mp4 2 + path/003.mp4 2 + Args: + file_path(str): Path to the index file. + pipeline(XXX): A sequence of data transforms. + **kwargs: Keyword arguments for ```BaseDataset```. + """ + def __init__(self, file_path, pipeline, num_retries=5, suffix='', **kwargs): + self.num_retries = num_retries + self.suffix = suffix + super().__init__(file_path, pipeline, **kwargs) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split() + frame_dir = line_split[0] + if self.data_prefix is not None: + frame_dir = osp.join(self.data_prefix, frame_dir) + info.append(dict(frame_dir=frame_dir, suffix=self.suffix)) + return info + + def prepare_train(self, idx): + """TRAIN & VALID. Prepare the data for training/valid given the index.""" + #Try to catch Exception caused by reading corrupted video file + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['filename'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) + + def prepare_test(self, idx): + """TEST. Prepare the data for test given the index.""" + #Try to catch Exception caused by reading corrupted video file + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['filename'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) diff --git a/paddlevideo/loader/dataset/ms_tcn_dataset.py b/paddlevideo/loader/dataset/ms_tcn_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..56e3b7bbba14fa94f4d029794c68ee3746c022bb --- /dev/null +++ b/paddlevideo/loader/dataset/ms_tcn_dataset.py @@ -0,0 +1,110 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import copy + +import os +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class MSTCNDataset(BaseDataset): + """Video dataset for action segmentation. + """ + + def __init__( + self, + file_path, + pipeline, + feature_path, + gt_path, + actions_map_file_path, + **kwargs, + ): + super().__init__(file_path, pipeline, **kwargs) + self.gt_path = gt_path + self.actions_map_file_path = actions_map_file_path + self.feature_path = feature_path + + # actions dict generate + file_ptr = open(self.actions_map_file_path, 'r') + actions = file_ptr.read().split('\n')[:-1] + file_ptr.close() + self.actions_dict = dict() + for a in actions: + self.actions_dict[a.split()[1]] = int(a.split()[0]) + + self.num_classes = len(self.actions_dict.keys()) + + def load_file(self): + """Load index file to get video information.""" + file_ptr = open(self.file_path, 'r') + info = file_ptr.read().split('\n')[:-1] + file_ptr.close() + return info + + def prepare_train(self, idx): + """TRAIN & VALID: Prepare data for training/valid given the index.""" + results = {} + video_name = self.info[idx] + # load video feature + file_name = video_name.split('.')[0] + ".npy" + feat_file_path = os.path.join(self.feature_path, file_name) + #TODO: check path + video_feat = np.load(feat_file_path) + + # load label + target_file_path = os.path.join(self.gt_path, video_name) + file_ptr = open(target_file_path, 'r') + content = file_ptr.read().split('\n')[:-1] + classes = np.zeros(min(np.shape(video_feat)[1], len(content)), dtype='int64') + for i in range(len(classes)): + classes[i] = self.actions_dict[content[i]] + # classes = classes * (-100) + + results['video_feat'] = copy.deepcopy(video_feat) + results['video_gt'] = copy.deepcopy(classes) + + results = self.pipeline(results) + return results['video_feat'], results['video_gt'] + + def prepare_test(self, idx): + """TEST: Prepare the data for test given the index.""" + results = {} + video_name = self.info[idx] + # load video feature + file_name = video_name.split('.')[0] + ".npy" + feat_file_path = os.path.join(self.feature_path, file_name) + #TODO: check path + video_feat = np.load(feat_file_path) + + # load label + target_file_path = os.path.join(self.gt_path, video_name) + file_ptr = open(target_file_path, 'r') + content = file_ptr.read().split('\n')[:-1] + classes = np.zeros(min(np.shape(video_feat)[1], len(content))) + for i in range(len(classes)): + classes[i] = self.actions_dict[content[i]] + # classes = classes * (-100) + + results['video_feat'] = copy.deepcopy(video_feat) + results['video_gt'] = copy.deepcopy(classes) + + results = self.pipeline(results) + return results['video_feat'], results['video_gt'] diff --git a/paddlevideo/loader/dataset/msrvtt.py b/paddlevideo/loader/dataset/msrvtt.py new file mode 100644 index 0000000000000000000000000000000000000000..accd6c6d154904e266526727d25c8a39d8cac208 --- /dev/null +++ b/paddlevideo/loader/dataset/msrvtt.py @@ -0,0 +1,218 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np +try: + import lmdb +except ImportError as e: + print(f"{e}, [lmdb] package and it's dependencies is required for ActBERT.") +import pickle +try: + from paddlenlp.transformers import BertTokenizer +except ImportError as e: + print( + f"{e}, [paddlenlp] package and it's dependencies is required for ActBERT." + ) +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class MSRVTTDataset(BaseDataset): + """MSR-VTT dataset for text-video clip retrieval. + """ + def __init__( + self, + file_path, + pipeline, + features_path, + bert_model="bert-base-uncased", + padding_index=0, + max_seq_length=36, + max_region_num=36, + max_action_num=5, + vision_feature_dim=2048, + action_feature_dim=2048, + spatials_dim=5, + data_prefix=None, + test_mode=False, + ): + self.features_path = features_path + self.bert_model = bert_model + self.padding_index = padding_index + self.max_seq_length = max_seq_length + self.max_region_num = max_region_num + self._max_action_num = max_action_num + self.vision_feature_dim = vision_feature_dim + self.action_feature_dim = action_feature_dim + self.spatials_dim = spatials_dim + self._tokenizer = BertTokenizer.from_pretrained(bert_model, + do_lower_case=True) + super().__init__(file_path, pipeline, data_prefix, test_mode) + self.tokenize() + self.gen_feature() + + def load_file(self): + """Load index file to get video information.""" + with open(self.file_path) as fin: + self.image_entries = [] + self.caption_entries = [] + for line in fin.readlines(): + line = line.strip() + vid_id = line.split(',')[0] + self.image_entries.append(vid_id) + self.caption_entries.append({ + "caption": line.split(',')[1], + "vid_id": vid_id + }) + self.env = lmdb.open(self.features_path) + + def tokenize(self): + for entry in self.caption_entries: + tokens = [] + tokens.append("[CLS]") + for token in self._tokenizer.tokenize(entry["caption"]): + tokens.append(token) + tokens.append("[SEP]") + tokens = self._tokenizer.convert_tokens_to_ids(tokens) + + segment_ids = [0] * len(tokens) + input_mask = [1] * len(tokens) + + if len(tokens) < self.max_seq_length: + padding = [self.padding_index + ] * (self.max_seq_length - len(tokens)) + tokens = tokens + padding + input_mask += padding + segment_ids += padding + + entry["token"] = np.array(tokens).astype('int64') + entry["input_mask"] = np.array(input_mask) + entry["segment_ids"] = np.array(segment_ids).astype('int64') + + def get_image_feature(self, video_id): + video_id = str(video_id).encode() + with self.env.begin(write=False) as txn: + item = pickle.loads(txn.get(video_id)) + video_id = item["video_id"] + image_h = int(item["image_h"]) + image_w = int(item["image_w"]) + + features = item["features"].reshape(-1, self.vision_feature_dim) + boxes = item["boxes"].reshape(-1, 4) + + num_boxes = features.shape[0] + g_feat = np.sum(features, axis=0) / num_boxes + num_boxes = num_boxes + 1 + features = np.concatenate( + [np.expand_dims(g_feat, axis=0), features], axis=0) + + action_features = item["action_features"].reshape( + -1, self.action_feature_dim) + + image_location = np.zeros((boxes.shape[0], self.spatials_dim), + dtype=np.float32) + image_location[:, :4] = boxes + image_location[:, + 4] = ((image_location[:, 3] - image_location[:, 1]) * + (image_location[:, 2] - image_location[:, 0]) / + (float(image_w) * float(image_h))) + + image_location[:, 0] = image_location[:, 0] / float(image_w) + image_location[:, 1] = image_location[:, 1] / float(image_h) + image_location[:, 2] = image_location[:, 2] / float(image_w) + image_location[:, 3] = image_location[:, 3] / float(image_h) + + g_location = np.array([0, 0, 1, 1, 1]) + image_location = np.concatenate( + [np.expand_dims(g_location, axis=0), image_location], axis=0) + return features, num_boxes, image_location, action_features + + def gen_feature(self): + num_inst = len(self.image_entries) #1000 + self.features_all = np.zeros( + (num_inst, self.max_region_num, self.vision_feature_dim)) + self.action_features_all = np.zeros( + (num_inst, self._max_action_num, self.action_feature_dim)) + self.spatials_all = np.zeros( + (num_inst, self.max_region_num, self.spatials_dim)) + self.image_mask_all = np.zeros((num_inst, self.max_region_num)) + self.action_mask_all = np.zeros((num_inst, self._max_action_num)) + + for i, image_id in enumerate(self.image_entries): + features, num_boxes, boxes, action_features = self.get_image_feature( + image_id) + + mix_num_boxes = min(int(num_boxes), self.max_region_num) + mix_boxes_pad = np.zeros((self.max_region_num, self.spatials_dim)) + mix_features_pad = np.zeros( + (self.max_region_num, self.vision_feature_dim)) + + image_mask = [1] * (int(mix_num_boxes)) + while len(image_mask) < self.max_region_num: + image_mask.append(0) + action_mask = [1] * (self._max_action_num) + while len(action_mask) < self._max_action_num: + action_mask.append(0) + + mix_boxes_pad[:mix_num_boxes] = boxes[:mix_num_boxes] + mix_features_pad[:mix_num_boxes] = features[:mix_num_boxes] + + self.features_all[i] = mix_features_pad + x = action_features.shape[0] + self.action_features_all[i][:x] = action_features[:] + self.image_mask_all[i] = np.array(image_mask) + self.action_mask_all[i] = np.array(action_mask) + self.spatials_all[i] = mix_boxes_pad + + self.features_all = self.features_all.astype("float32") + self.action_features_all = self.action_features_all.astype("float32") + self.image_mask_all = self.image_mask_all.astype("int64") + self.action_mask_all = self.action_mask_all.astype("int64") + self.spatials_all = self.spatials_all.astype("float32") + + def prepare_train(self, idx): + pass + + def prepare_test(self, idx): + entry = self.caption_entries[idx] + caption = entry["token"] + input_mask = entry["input_mask"] + segment_ids = entry["segment_ids"] + + target_all = np.zeros(1000) + for i, image_id in enumerate(self.image_entries): + if image_id == entry["vid_id"]: + target_all[i] = 1 + + return ( + caption, + self.action_features_all, + self.features_all, + self.spatials_all, + segment_ids, + input_mask, + self.image_mask_all, + self.action_mask_all, + target_all, + ) + + def __len__(self): + return len(self.caption_entries) diff --git a/paddlevideo/loader/dataset/oxford.py b/paddlevideo/loader/dataset/oxford.py new file mode 100644 index 0000000000000000000000000000000000000000..a9e65c6981d59b69408ebea268cb235e4dfd37b0 --- /dev/null +++ b/paddlevideo/loader/dataset/oxford.py @@ -0,0 +1,62 @@ +# Copyright Niantic 2019. Patent Pending. All rights reserved. +# +# This software is licensed under the terms of the Monodepth2 licence +# which allows for non-commercial use only, the full terms of which are made +# available in the LICENSE file. + +from __future__ import absolute_import, division, print_function + +import copy +from os import path as osp + +from PIL import Image + +from ..registry import DATASETS +from .base import BaseDataset + + +def pil_loader(path): + # open path as file to avoid ResourceWarning + # (https://github.com/python-pillow/Pillow/issues/835) + with open(path, 'rb') as f: + with Image.open(f) as img: + return img.convert('RGB') + + +@DATASETS.register() +class MonoDataset(BaseDataset): + def __init__(self, + file_path, + data_prefix, + pipeline, + num_retries=0, + suffix='.png', + **kwargs): + self.num_retries = num_retries + self.suffix = suffix + super().__init__(file_path, pipeline, data_prefix, **kwargs) + + def load_file(self): + info = [] + with open(self.file_path, 'r') as f: + for line in f: + filename = line.strip() + self.suffix + folder = osp.dirname(filename) + frame_index = line.strip().split('/')[1] + info.append( + dict(data_path=self.data_prefix, + filename=filename, + folder=folder, + frame_index=int(frame_index))) + return info + + def prepare_train(self, idx): + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + results['imgs']['idx'] = idx + return results['imgs'], results['day_or_night'] + + def prepare_test(self, idx): + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + return results['imgs'], results['day_or_night'] diff --git a/paddlevideo/loader/dataset/skeleton.py b/paddlevideo/loader/dataset/skeleton.py new file mode 100644 index 0000000000000000000000000000000000000000..30a3f3e70fa2869d663b0d089a8521be9bfaaf1d --- /dev/null +++ b/paddlevideo/loader/dataset/skeleton.py @@ -0,0 +1,78 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np +import pickle + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class SkeletonDataset(BaseDataset): + """ + Skeleton dataset for action recognition. + The dataset loads skeleton feature, and apply norm operatations. + Args: + file_path (str): Path to the index file. + pipeline(obj): Define the pipeline of data preprocessing. + data_prefix (str): directory path of the data. Default: None. + test_mode (bool): Whether to bulid the test dataset. Default: False. + """ + def __init__(self, file_path, pipeline, label_path=None, test_mode=False): + self.label_path = label_path + super().__init__(file_path, pipeline, test_mode=test_mode) + + def load_file(self): + """Load feature file to get skeleton information.""" + logger.info("Loading data, it will take some moment...") + self.data = np.load(self.file_path) + if self.label_path: + if self.label_path.endswith('npy'): + self.label = np.load(self.label_path) + elif self.label_path.endswith('pkl'): + with open(self.label_path, 'rb') as f: + sample_name, self.label = pickle.load(f) + else: + logger.info( + "Label path not provided when test_mode={}, here just output predictions." + .format(self.test_mode)) + logger.info("Data Loaded!") + return self.data # used for __len__ + + def prepare_train(self, idx): + """Prepare the feature for training/valid given index. """ + results = dict() + results['data'] = copy.deepcopy(self.data[idx]) + results['label'] = copy.deepcopy(self.label[idx]) + results = self.pipeline(results) + return results['data'], results['label'] + + def prepare_test(self, idx): + """Prepare the feature for test given index. """ + results = dict() + results['data'] = copy.deepcopy(self.data[idx]) + if self.label_path: + results['label'] = copy.deepcopy(self.label[idx]) + results = self.pipeline(results) + return results['data'], results['label'] + else: + results = self.pipeline(results) + return [results['data']] diff --git a/paddlevideo/loader/dataset/slowfast_video.py b/paddlevideo/loader/dataset/slowfast_video.py new file mode 100644 index 0000000000000000000000000000000000000000..1adf89c5404acd864fa9b9392e8fcae2598894af --- /dev/null +++ b/paddlevideo/loader/dataset/slowfast_video.py @@ -0,0 +1,143 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + +@DATASETS.register() +class SFVideoDataset(BaseDataset): + """Video dataset for action recognition + The dataset loads raw videos and apply specified transforms on them. + + The index file is a file with multiple lines, and each line indicates + a sample video with the filepath and label, which are split with a whitesapce. + Example of a inde file: + + .. code-block:: txt + + path/000.mp4 1 + path/001.mp4 1 + path/002.mp4 2 + path/003.mp4 2 + + Args: + file_path(str): Path to the index file. + pipeline(XXX): A sequence of data transforms. + num_ensemble_views(int): temporal segment when multi-crop test + num_spatial_crops(int): spatial crop number when multi-crop test + **kwargs: Keyword arguments for ```BaseDataset```. + + """ + def __init__( + self, + file_path, + pipeline, + num_ensemble_views=1, + num_spatial_crops=1, + num_retries=5, + num_samples_precise_bn=None, + **kwargs, + ): + self.num_ensemble_views = num_ensemble_views + self.num_spatial_crops = num_spatial_crops + self.num_retries = num_retries + self.num_samples_precise_bn = num_samples_precise_bn + super().__init__(file_path, pipeline, **kwargs) + #set random seed + random.seed(0) + np.random.seed(0) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split() + filename, labels = line_split + if self.data_prefix is not None: + filename = osp.join(self.data_prefix, filename) + for tidx in range(self.num_ensemble_views): + for sidx in range(self.num_spatial_crops): + info.append( + dict( + filename=filename, + labels=int(labels), + temporal_sample_index=tidx, + spatial_sample_index=sidx, + temporal_num_clips=self.num_ensemble_views, + spatial_num_clips=self.num_spatial_crops, + )) + return info + + def prepare_train(self, idx): + """TRAIN & VALID. Prepare the data for training given the index.""" + #Try to catch Exception caused by reading corrupted video file + short_cycle = False + if isinstance(idx, tuple): + idx, short_cycle_idx = idx + short_cycle = True + for ir in range(self.num_retries): + try: + #Multi-grid short cycle + if short_cycle: + results = copy.deepcopy(self.info[idx]) + results['short_cycle_idx'] = short_cycle_idx + else: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['filename'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + + return results['imgs'][0], results['imgs'][1], np.array( + [results['labels']]) + + def prepare_test(self, idx): + """TEST. Prepare the data for test given the index.""" + #Try to catch Exception caused by reading corrupted video file + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['filename'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'][0], results['imgs'][1], np.array( + [results['labels']]), np.array([idx]) + + def __len__(self): + """get the size of the dataset.""" + if self.num_samples_precise_bn is None: + return len(self.info) + else: + random.shuffle(self.info) + return min(self.num_samples_precise_bn, len(self.info)) diff --git a/paddlevideo/loader/dataset/video.py b/paddlevideo/loader/dataset/video.py new file mode 100644 index 0000000000000000000000000000000000000000..f2d8f897a2ce00796f85354161c026205fe6001e --- /dev/null +++ b/paddlevideo/loader/dataset/video.py @@ -0,0 +1,95 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import copy +import random +import numpy as np + +from ..registry import DATASETS +from .base import BaseDataset +from ...utils import get_logger + +logger = get_logger("paddlevideo") + + +@DATASETS.register() +class VideoDataset(BaseDataset): + """Video dataset for action recognition + The dataset loads raw videos and apply specified transforms on them. + The index file is a file with multiple lines, and each line indicates + a sample video with the filepath and label, which are split with a whitesapce. + Example of a inde file: + .. code-block:: txt + path/000.mp4 1 + path/001.mp4 1 + path/002.mp4 2 + path/003.mp4 2 + Args: + file_path(str): Path to the index file. + pipeline(XXX): A sequence of data transforms. + **kwargs: Keyword arguments for ```BaseDataset```. + """ + def __init__(self, file_path, pipeline, num_retries=5, suffix='', **kwargs): + self.num_retries = num_retries + self.suffix = suffix + super().__init__(file_path, pipeline, **kwargs) + + def load_file(self): + """Load index file to get video information.""" + info = [] + with open(self.file_path, 'r') as fin: + for line in fin: + line_split = line.strip().split() + filename, labels = line_split + #TODO(hj): Required suffix format: may mp4/avi/wmv + filename = filename + self.suffix + if self.data_prefix is not None: + filename = osp.join(self.data_prefix, filename) + info.append(dict(filename=filename, labels=int(labels))) + return info + + def prepare_train(self, idx): + """TRAIN & VALID. Prepare the data for training/valid given the index.""" + #Try to catch Exception caused by reading corrupted video file + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['filename'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) + + def prepare_test(self, idx): + """TEST. Prepare the data for test given the index.""" + #Try to catch Exception caused by reading corrupted video file + for ir in range(self.num_retries): + try: + results = copy.deepcopy(self.info[idx]) + results = self.pipeline(results) + except Exception as e: + #logger.info(e) + if ir < self.num_retries - 1: + logger.info( + "Error when loading {}, have {} trys, will try again". + format(results['filename'], ir)) + idx = random.randint(0, len(self.info) - 1) + continue + return results['imgs'], np.array([results['labels']]) diff --git a/paddlevideo/loader/pipelines/__init__.py b/paddlevideo/loader/pipelines/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bfd808f060d94cca3e7ced2c81bd4653cf8c26b9 --- /dev/null +++ b/paddlevideo/loader/pipelines/__init__.py @@ -0,0 +1,47 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .anet_pipeline import GetMatchMap, GetVideoLabel, LoadFeat +from .augmentations import (CenterCrop, ColorJitter, GroupRandomFlip, + GroupResize, Image2Array, JitterScale, MultiCrop, + Normalization, PackOutput, RandomCrop, RandomFlip, + RandomResizedCrop, Scale, TenCrop, ToArray, + UniformCrop) +from .augmentations_ava import * +from .compose import Compose +from .decode import FeatureDecoder, FrameDecoder, VideoDecoder +from .decode_image import ImageDecoder +from .decode_sampler import DecodeSampler +from .mix import Cutmix, Mixup, VideoMix +from .multimodal import FeaturePadding, RandomCap, RandomMask, Tokenize +from .sample import Sampler, SamplerPkl +from .sample_ava import * +from .segmentation import MultiNorm, MultiRestrictSize +from .skeleton_pipeline import AutoPadding, Iden, SkeletonNorm +from .skeleton_pipeline import SketeonCropSample, SketeonModalityTransform, RandomRotation +from .decode_sampler_MRI import SFMRI_DecodeSampler +from .segmentation_pipline import SegmentationSampler + +__all__ = [ + 'ImageDecoder', 'RandomMask', 'UniformCrop', 'SkeletonNorm', 'Tokenize', + 'Sampler', 'FeatureDecoder', 'DecodeSampler', 'TenCrop', 'Compose', + 'AutoPadding', 'Normalization', 'Mixup', 'Image2Array', 'Scale', + 'GroupResize', 'VideoDecoder', 'FrameDecoder', 'PackOutput', + 'GetVideoLabel', 'Cutmix', 'CenterCrop', 'RandomCrop', 'LoadFeat', + 'RandomCap', 'JitterScale', 'Iden', 'VideoMix', 'ColorJitter', 'RandomFlip', + 'ToArray', 'FeaturePadding', 'GetMatchMap', 'GroupRandomFlip', 'MultiCrop', + 'SFMRI_DecodeSampler', 'MultiRestrictSize', 'MultiNorm', + 'RandomResizedCrop', 'SamplerPkl', 'SegmentationSampler', + 'SketeonCropSample', 'SketeonModalityTransform', 'RandomRotation' +] diff --git a/paddlevideo/loader/pipelines/anet_pipeline.py b/paddlevideo/loader/pipelines/anet_pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..210d733b7e91eb580e151de68b550d7b9b2ae5f6 --- /dev/null +++ b/paddlevideo/loader/pipelines/anet_pipeline.py @@ -0,0 +1,150 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import numpy as np +from ..registry import PIPELINES +"""pipeline ops for Activity Net. +""" + + +@PIPELINES.register() +class LoadFeat(object): + def __init__(self, feat_path): + self.feat_path = feat_path + + def __call__(self, results): + video_name = results['video_name'] + file_name = video_name + ".npy" + file_path = os.path.join(self.feat_path, file_name) + #TODO: check path + video_feat = np.load(file_path) + video_feat = video_feat.T + video_feat = video_feat.astype("float32") + results['video_feat'] = video_feat + return results + + +@PIPELINES.register() +class GetMatchMap(object): + def __init__(self, tscale): + self.tscale = tscale + self.tgap = 1. / self.tscale + + def __call__(self, results): + match_map = [] + for idx in range(self.tscale): + tmp_match_window = [] + xmin = self.tgap * idx + for jdx in range(1, self.tscale + 1): + xmax = xmin + self.tgap * jdx + tmp_match_window.append([xmin, xmax]) + match_map.append(tmp_match_window) + match_map = np.array(match_map) + match_map = np.transpose(match_map, [1, 0, 2]) + match_map = np.reshape(match_map, [-1, 2]) + + anchor_xmin = [self.tgap * i for i in range(self.tscale)] + anchor_xmax = [self.tgap * i for i in range(1, self.tscale + 1)] + + results['match_map'] = match_map + results['anchor_xmin'] = anchor_xmin + results['anchor_xmax'] = anchor_xmax + return results + + +@PIPELINES.register() +class GetVideoLabel(object): + def __init__(self, tscale, dscale, datatype="float32"): + self.tscale = tscale + self.dscale = dscale + self.tgap = 1. / self.tscale + self.datatype = datatype + + def iou_with_anchors(self, anchors_min, anchors_max, box_min, box_max): + """Compute jaccard score between a box and the anchors. + """ + len_anchors = anchors_max - anchors_min + int_xmin = np.maximum(anchors_min, box_min) + int_xmax = np.minimum(anchors_max, box_max) + inter_len = np.maximum(int_xmax - int_xmin, 0.) + union_len = len_anchors - inter_len + box_max - box_min + jaccard = np.divide(inter_len, union_len) + return jaccard + + def ioa_with_anchors(self, anchors_min, anchors_max, box_min, box_max): + """Compute intersection between score a box and the anchors. + """ + len_anchors = anchors_max - anchors_min + int_xmin = np.maximum(anchors_min, box_min) + int_xmax = np.minimum(anchors_max, box_max) + inter_len = np.maximum(int_xmax - int_xmin, 0.) + scores = np.divide(inter_len, len_anchors) + return scores + + def __call__(self, results): + video_info = results['video_info'] + match_map = results['match_map'] + anchor_xmin = results['anchor_xmin'] + anchor_xmax = results['anchor_xmax'] + + video_second = video_info['duration_second'] + video_labels = video_info['annotations'] + + gt_bbox = [] + gt_iou_map = [] + for gt in video_labels: + tmp_start = max(min(1, gt["segment"][0] / video_second), 0) + tmp_end = max(min(1, gt["segment"][1] / video_second), 0) + gt_bbox.append([tmp_start, tmp_end]) + tmp_gt_iou_map = self.iou_with_anchors(match_map[:, 0], + match_map[:, 1], tmp_start, + tmp_end) + tmp_gt_iou_map = np.reshape(tmp_gt_iou_map, + [self.dscale, self.tscale]) + gt_iou_map.append(tmp_gt_iou_map) + gt_iou_map = np.array(gt_iou_map) + gt_iou_map = np.max(gt_iou_map, axis=0) + + gt_bbox = np.array(gt_bbox) + gt_xmins = gt_bbox[:, 0] + gt_xmaxs = gt_bbox[:, 1] + gt_len_small = 3 * self.tgap + gt_start_bboxs = np.stack( + (gt_xmins - gt_len_small / 2, gt_xmins + gt_len_small / 2), axis=1) + gt_end_bboxs = np.stack( + (gt_xmaxs - gt_len_small / 2, gt_xmaxs + gt_len_small / 2), axis=1) + + match_score_start = [] + for jdx in range(len(anchor_xmin)): + match_score_start.append( + np.max( + self.ioa_with_anchors(anchor_xmin[jdx], anchor_xmax[jdx], + gt_start_bboxs[:, 0], + gt_start_bboxs[:, 1]))) + match_score_end = [] + for jdx in range(len(anchor_xmin)): + match_score_end.append( + np.max( + self.ioa_with_anchors(anchor_xmin[jdx], anchor_xmax[jdx], + gt_end_bboxs[:, 0], gt_end_bboxs[:, + 1]))) + + gt_start = np.array(match_score_start) + gt_end = np.array(match_score_end) + + results['gt_iou_map'] = gt_iou_map.astype(self.datatype) + results['gt_start'] = gt_start.astype(self.datatype) + results['gt_end'] = gt_end.astype(self.datatype) + return results diff --git a/paddlevideo/loader/pipelines/augmentations.py b/paddlevideo/loader/pipelines/augmentations.py new file mode 100644 index 0000000000000000000000000000000000000000..0a9b3cefbe3563c9cb5754da3aa8eac7b738f3dd --- /dev/null +++ b/paddlevideo/loader/pipelines/augmentations.py @@ -0,0 +1,1026 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import random +from collections.abc import Sequence + +import cv2 +import numpy as np +import paddle +import paddle.nn.functional as F +from PIL import Image + +from ..registry import PIPELINES + + +@PIPELINES.register() +class Scale(object): + """ + Scale images. + Args: + short_size(float | int): Short size of an image will be scaled to the short_size. + fixed_ratio(bool): Set whether to zoom according to a fixed ratio. default: True + do_round(bool): Whether to round up when calculating the zoom ratio. default: False + backend(str): Choose pillow or cv2 as the graphics processing backend. default: 'pillow' + """ + def __init__(self, + short_size, + fixed_ratio=True, + keep_ratio=None, + do_round=False, + backend='pillow'): + self.short_size = short_size + assert (fixed_ratio and not keep_ratio) or (not fixed_ratio), \ + f"fixed_ratio and keep_ratio cannot be true at the same time" + self.fixed_ratio = fixed_ratio + self.keep_ratio = keep_ratio + self.do_round = do_round + + assert backend in [ + 'pillow', 'cv2' + ], f"Scale's backend must be pillow or cv2, but get {backend}" + self.backend = backend + + def __call__(self, results): + """ + Performs resize operations. + Args: + imgs (Sequence[PIL.Image]): List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + resized_imgs: List where each item is a PIL.Image after scaling. + """ + imgs = results['imgs'] + resized_imgs = [] + for i in range(len(imgs)): + img = imgs[i] + if isinstance(img, np.ndarray): + h, w, _ = img.shape + elif isinstance(img, Image.Image): + w, h = img.size + else: + raise NotImplementedError + + if w <= h: + ow = self.short_size + if self.fixed_ratio: + oh = int(self.short_size * 4.0 / 3.0) + elif self.keep_ratio is False: + oh = self.short_size + else: + scale_factor = self.short_size / w + oh = int(h * float(scale_factor) + + 0.5) if self.do_round else int(h * + self.short_size / w) + ow = int(w * float(scale_factor) + + 0.5) if self.do_round else self.short_size + else: + oh = self.short_size + if self.fixed_ratio: + ow = int(self.short_size * 4.0 / 3.0) + elif self.keep_ratio is False: + ow = self.short_size + else: + scale_factor = self.short_size / h + oh = int(h * float(scale_factor) + + 0.5) if self.do_round else self.short_size + ow = int(w * float(scale_factor) + + 0.5) if self.do_round else int(w * + self.short_size / h) + if self.backend == 'pillow': + resized_imgs.append(img.resize((ow, oh), Image.BILINEAR)) + elif self.backend == 'cv2' and (self.keep_ratio is not None): + resized_imgs.append( + cv2.resize(img, (ow, oh), interpolation=cv2.INTER_LINEAR)) + else: + resized_imgs.append( + Image.fromarray( + cv2.resize(np.asarray(img), (ow, oh), + interpolation=cv2.INTER_LINEAR))) + results['imgs'] = resized_imgs + return results + + +@PIPELINES.register() +class RandomCrop(object): + """ + Random crop images. + Args: + target_size(int): Random crop a square with the target_size from an image. + """ + def __init__(self, target_size): + self.target_size = target_size + + def __call__(self, results): + """ + Performs random crop operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + crop_imgs: List where each item is a PIL.Image after random crop. + """ + imgs = results['imgs'] + if 'backend' in results and results['backend'] == 'pyav': # [c,t,h,w] + h, w = imgs.shape[2:] + else: + w, h = imgs[0].size + th, tw = self.target_size, self.target_size + + assert (w >= self.target_size) and (h >= self.target_size), \ + "image width({}) and height({}) should be larger than crop size".format( + w, h, self.target_size) + + crop_images = [] + if 'backend' in results and results['backend'] == 'pyav': + x1 = np.random.randint(0, w - tw) + y1 = np.random.randint(0, h - th) + crop_images = imgs[:, :, y1:y1 + th, x1:x1 + tw] # [C, T, th, tw] + else: + x1 = random.randint(0, w - tw) + y1 = random.randint(0, h - th) + for img in imgs: + if w == tw and h == th: + crop_images.append(img) + else: + crop_images.append(img.crop((x1, y1, x1 + tw, y1 + th))) + results['imgs'] = crop_images + return results + + +@PIPELINES.register() +class RandomResizedCrop(RandomCrop): + def __init__(self, + area_range=(0.08, 1.0), + aspect_ratio_range=(3 / 4, 4 / 3), + target_size=224, + backend='cv2'): + + self.area_range = area_range + self.aspect_ratio_range = aspect_ratio_range + self.target_size = target_size + self.backend = backend + + @staticmethod + def get_crop_bbox(img_shape, + area_range, + aspect_ratio_range, + max_attempts=10): + + assert 0 < area_range[0] <= area_range[1] <= 1 + assert 0 < aspect_ratio_range[0] <= aspect_ratio_range[1] + + img_h, img_w = img_shape + area = img_h * img_w + + min_ar, max_ar = aspect_ratio_range + aspect_ratios = np.exp( + np.random.uniform(np.log(min_ar), np.log(max_ar), + size=max_attempts)) + target_areas = np.random.uniform(*area_range, size=max_attempts) * area + candidate_crop_w = np.round(np.sqrt(target_areas * + aspect_ratios)).astype(np.int32) + candidate_crop_h = np.round(np.sqrt(target_areas / + aspect_ratios)).astype(np.int32) + + for i in range(max_attempts): + crop_w = candidate_crop_w[i] + crop_h = candidate_crop_h[i] + if crop_h <= img_h and crop_w <= img_w: + x_offset = random.randint(0, img_w - crop_w) + y_offset = random.randint(0, img_h - crop_h) + return x_offset, y_offset, x_offset + crop_w, y_offset + crop_h + + # Fallback + crop_size = min(img_h, img_w) + x_offset = (img_w - crop_size) // 2 + y_offset = (img_h - crop_size) // 2 + return x_offset, y_offset, x_offset + crop_size, y_offset + crop_size + + def __call__(self, results): + imgs = results['imgs'] + if self.backend == 'pillow': + img_w, img_h = imgs[0].size + elif self.backend == 'cv2': + img_h, img_w, _ = imgs[0].shape + elif self.backend == 'pyav': + img_h, img_w = imgs.shape[2:] # [cthw] + else: + raise NotImplementedError + + left, top, right, bottom = self.get_crop_bbox( + (img_h, img_w), self.area_range, self.aspect_ratio_range) + + if self.backend == 'pillow': + img_w, img_h = imgs[0].size + imgs = [img.crop(left, top, right, bottom) for img in imgs] + elif self.backend == 'cv2': + img_h, img_w, _ = imgs[0].shape + imgs = [img[top:bottom, left:right] for img in imgs] + elif self.backend == 'pyav': + img_h, img_w = imgs.shape[2:] # [cthw] + imgs = imgs[:, :, top:bottom, left:right] + else: + raise NotImplementedError + results['imgs'] = imgs + return results + + +@PIPELINES.register() +class CenterCrop(object): + """ + Center crop images. + Args: + target_size(int): Center crop a square with the target_size from an image. + do_round(bool): Whether to round up the coordinates of the upper left corner of the cropping area. default: True + """ + def __init__(self, target_size, do_round=True, backend='pillow'): + self.target_size = target_size + self.do_round = do_round + self.backend = backend + + def __call__(self, results): + """ + Performs Center crop operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + ccrop_imgs: List where each item is a PIL.Image after Center crop. + """ + imgs = results['imgs'] + ccrop_imgs = [] + th, tw = self.target_size, self.target_size + if isinstance(imgs, paddle.Tensor): + h, w = imgs.shape[-2:] + x1 = int(round((w - tw) / 2.0)) if self.do_round else (w - tw) // 2 + y1 = int(round((h - th) / 2.0)) if self.do_round else (h - th) // 2 + ccrop_imgs = imgs[:, :, y1:y1 + th, x1:x1 + tw] + else: + for img in imgs: + if self.backend == 'pillow': + w, h = img.size + elif self.backend == 'cv2': + h, w, _ = img.shape + else: + raise NotImplementedError + assert (w >= self.target_size) and (h >= self.target_size), \ + "image width({}) and height({}) should be larger than crop size".format( + w, h, self.target_size) + x1 = int(round( + (w - tw) / 2.0)) if self.do_round else (w - tw) // 2 + y1 = int(round( + (h - th) / 2.0)) if self.do_round else (h - th) // 2 + if self.backend == 'cv2': + ccrop_imgs.append(img[y1:y1 + th, x1:x1 + tw]) + elif self.backend == 'pillow': + ccrop_imgs.append(img.crop((x1, y1, x1 + tw, y1 + th))) + results['imgs'] = ccrop_imgs + return results + + +@PIPELINES.register() +class MultiScaleCrop(object): + """ + Random crop images in with multiscale sizes + Args: + target_size(int): Random crop a square with the target_size from an image. + scales(int): List of candidate cropping scales. + max_distort(int): Maximum allowable deformation combination distance. + fix_crop(int): Whether to fix the cutting start point. + allow_duplication(int): Whether to allow duplicate candidate crop starting points. + more_fix_crop(int): Whether to allow more cutting starting points. + """ + def __init__( + self, + target_size, # NOTE: named target size now, but still pass short size in it! + scales=None, + max_distort=1, + fix_crop=True, + allow_duplication=False, + more_fix_crop=True, + backend='pillow'): + + self.target_size = target_size + self.scales = scales if scales else [1, .875, .75, .66] + self.max_distort = max_distort + self.fix_crop = fix_crop + self.allow_duplication = allow_duplication + self.more_fix_crop = more_fix_crop + assert backend in [ + 'pillow', 'cv2' + ], f"MultiScaleCrop's backend must be pillow or cv2, but get {backend}" + self.backend = backend + + def __call__(self, results): + """ + Performs MultiScaleCrop operations. + Args: + imgs: List where wach item is a PIL.Image. + XXX: + results: + + """ + imgs = results['imgs'] + + input_size = [self.target_size, self.target_size] + + im_size = imgs[0].size + + # get random crop offset + def _sample_crop_size(im_size): + image_w, image_h = im_size[0], im_size[1] + + base_size = min(image_w, image_h) + crop_sizes = [int(base_size * x) for x in self.scales] + crop_h = [ + input_size[1] if abs(x - input_size[1]) < 3 else x + for x in crop_sizes + ] + crop_w = [ + input_size[0] if abs(x - input_size[0]) < 3 else x + for x in crop_sizes + ] + + pairs = [] + for i, h in enumerate(crop_h): + for j, w in enumerate(crop_w): + if abs(i - j) <= self.max_distort: + pairs.append((w, h)) + crop_pair = random.choice(pairs) + if not self.fix_crop: + w_offset = random.randint(0, image_w - crop_pair[0]) + h_offset = random.randint(0, image_h - crop_pair[1]) + else: + w_step = (image_w - crop_pair[0]) / 4 + h_step = (image_h - crop_pair[1]) / 4 + + ret = list() + ret.append((0, 0)) # upper left + if self.allow_duplication or w_step != 0: + ret.append((4 * w_step, 0)) # upper right + if self.allow_duplication or h_step != 0: + ret.append((0, 4 * h_step)) # lower left + if self.allow_duplication or (h_step != 0 and w_step != 0): + ret.append((4 * w_step, 4 * h_step)) # lower right + if self.allow_duplication or (h_step != 0 or w_step != 0): + ret.append((2 * w_step, 2 * h_step)) # center + + if self.more_fix_crop: + ret.append((0, 2 * h_step)) # center left + ret.append((4 * w_step, 2 * h_step)) # center right + ret.append((2 * w_step, 4 * h_step)) # lower center + ret.append((2 * w_step, 0 * h_step)) # upper center + + ret.append((1 * w_step, 1 * h_step)) # upper left quarter + ret.append((3 * w_step, 1 * h_step)) # upper right quarter + ret.append((1 * w_step, 3 * h_step)) # lower left quarter + ret.append((3 * w_step, 3 * h_step)) # lower righ quarter + + w_offset, h_offset = random.choice(ret) + + return crop_pair[0], crop_pair[1], w_offset, h_offset + + crop_w, crop_h, offset_w, offset_h = _sample_crop_size(im_size) + crop_img_group = [ + img.crop((offset_w, offset_h, offset_w + crop_w, offset_h + crop_h)) + for img in imgs + ] + if self.backend == 'pillow': + ret_img_group = [ + img.resize((input_size[0], input_size[1]), Image.BILINEAR) + for img in crop_img_group + ] + else: + ret_img_group = [ + Image.fromarray( + cv2.resize(np.asarray(img), + dsize=(input_size[0], input_size[1]), + interpolation=cv2.INTER_LINEAR)) + for img in crop_img_group + ] + results['imgs'] = ret_img_group + return results + + +@PIPELINES.register() +class RandomFlip(object): + """ + Random Flip images. + Args: + p(float): Random flip images with the probability p. + """ + def __init__(self, p=0.5): + self.p = p + + def __call__(self, results): + """ + Performs random flip operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + flip_imgs: List where each item is a PIL.Image after random flip. + """ + imgs = results['imgs'] + v = random.random() + if v < self.p: + if isinstance(imgs, paddle.Tensor): + results['imgs'] = paddle.flip(imgs, axis=[3]) + elif isinstance(imgs[0], np.ndarray): + results['imgs'] = [cv2.flip(img, 1, img) for img in imgs + ] # [[h,w,c], [h,w,c], ..., [h,w,c]] + else: + results['imgs'] = [ + img.transpose(Image.FLIP_LEFT_RIGHT) for img in imgs + ] + else: + results['imgs'] = imgs + return results + + +@PIPELINES.register() +class Image2Array(object): + """ + transfer PIL.Image to Numpy array and transpose dimensions from 'dhwc' to 'dchw'. + Args: + transpose: whether to transpose or not, default True, False for slowfast. + """ + def __init__(self, transpose=True, data_format='tchw'): + assert data_format in [ + 'tchw', 'cthw' + ], f"Target format must in ['tchw', 'cthw'], but got {data_format}" + self.transpose = transpose + self.data_format = data_format + + def __call__(self, results): + """ + Performs Image to NumpyArray operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + np_imgs: Numpy array. + """ + imgs = results['imgs'] + if 'backend' in results and results[ + 'backend'] == 'pyav': # [T,H,W,C] in [0, 1] + if self.transpose: + if self.data_format == 'tchw': + t_imgs = imgs.transpose((0, 3, 1, 2)) # tchw + else: + t_imgs = imgs.transpose((3, 0, 1, 2)) # cthw + results['imgs'] = t_imgs + else: + t_imgs = np.stack(imgs).astype('float32') + if self.transpose: + if self.data_format == 'tchw': + t_imgs = t_imgs.transpose(0, 3, 1, 2) # tchw + else: + t_imgs = t_imgs.transpose(3, 0, 1, 2) # cthw + results['imgs'] = t_imgs + return results + + +@PIPELINES.register() +class Normalization(object): + """ + Normalization. + Args: + mean(Sequence[float]): mean values of different channels. + std(Sequence[float]): std values of different channels. + tensor_shape(list): size of mean, default [3,1,1]. For slowfast, [1,1,1,3] + """ + def __init__(self, mean, std, tensor_shape=[3, 1, 1], inplace=False): + if not isinstance(mean, Sequence): + raise TypeError( + f'Mean must be list, tuple or np.ndarray, but got {type(mean)}') + if not isinstance(std, Sequence): + raise TypeError( + f'Std must be list, tuple or np.ndarray, but got {type(std)}') + + self.inplace = inplace + if not inplace: + self.mean = np.array(mean).reshape(tensor_shape).astype(np.float32) + self.std = np.array(std).reshape(tensor_shape).astype(np.float32) + else: + self.mean = np.array(mean, dtype=np.float32) + self.std = np.array(std, dtype=np.float32) + + def __call__(self, results): + """ + Performs normalization operations. + Args: + imgs: Numpy array. + return: + np_imgs: Numpy array after normalization. + """ + if self.inplace: + n = len(results['imgs']) + h, w, c = results['imgs'][0].shape + norm_imgs = np.empty((n, h, w, c), dtype=np.float32) + for i, img in enumerate(results['imgs']): + norm_imgs[i] = img + + for img in norm_imgs: # [n,h,w,c] + mean = np.float64(self.mean.reshape(1, -1)) # [1, 3] + stdinv = 1 / np.float64(self.std.reshape(1, -1)) # [1, 3] + cv2.subtract(img, mean, img) + cv2.multiply(img, stdinv, img) + else: + imgs = results['imgs'] + norm_imgs = imgs / 255.0 + norm_imgs -= self.mean + norm_imgs /= self.std + if 'backend' in results and results['backend'] == 'pyav': + norm_imgs = paddle.to_tensor(norm_imgs, dtype=paddle.float32) + results['imgs'] = norm_imgs + return results + + +@PIPELINES.register() +class JitterScale(object): + """ + Scale image, while the target short size is randomly select between min_size and max_size. + Args: + min_size: Lower bound for random sampler. + max_size: Higher bound for random sampler. + """ + def __init__(self, + min_size, + max_size, + short_cycle_factors=[0.5, 0.7071], + default_min_size=256): + self.default_min_size = default_min_size + self.orig_min_size = self.min_size = min_size + self.max_size = max_size + self.short_cycle_factors = short_cycle_factors + + def __call__(self, results): + """ + Performs jitter resize operations. + Args: + imgs (Sequence[PIL.Image]): List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + resized_imgs: List where each item is a PIL.Image after scaling. + """ + short_cycle_idx = results.get('short_cycle_idx') + if short_cycle_idx in [0, 1]: + self.min_size = int( + round(self.short_cycle_factors[short_cycle_idx] * + self.default_min_size)) + else: + self.min_size = self.orig_min_size + + imgs = results['imgs'] + size = int(round(np.random.uniform(self.min_size, self.max_size))) + assert (len(imgs) >= 1), \ + "len(imgs):{} should be larger than 1".format(len(imgs)) + + if 'backend' in results and results['backend'] == 'pyav': + height, width = imgs.shape[2:] + else: + width, height = imgs[0].size + if (width <= height and width == size) or (height <= width + and height == size): + return results + + new_width = size + new_height = size + if width < height: + new_height = int(math.floor((float(height) / width) * size)) + else: + new_width = int(math.floor((float(width) / height) * size)) + + if 'backend' in results and results['backend'] == 'pyav': + frames_resize = F.interpolate(imgs, + size=(new_height, new_width), + mode="bilinear", + align_corners=False) # [c,t,h,w] + else: + frames_resize = [] + for j in range(len(imgs)): + img = imgs[j] + scale_img = img.resize((new_width, new_height), Image.BILINEAR) + frames_resize.append(scale_img) + + results['imgs'] = frames_resize + return results + + +@PIPELINES.register() +class MultiCrop(object): + """ + Random crop image. + This operation can perform multi-crop during multi-clip test, as in slowfast model. + Args: + target_size(int): Random crop a square with the target_size from an image. + """ + def __init__(self, + target_size, + default_crop_size=224, + short_cycle_factors=[0.5, 0.7071], + test_mode=False): + self.orig_target_size = self.target_size = target_size + self.short_cycle_factors = short_cycle_factors + self.default_crop_size = default_crop_size + self.test_mode = test_mode + + def __call__(self, results): + """ + Performs random crop operations. + Args: + imgs: List where each item is a PIL.Image. + For example, [PIL.Image0, PIL.Image1, PIL.Image2, ...] + return: + crop_imgs: List where each item is a PIL.Image after random crop. + """ + imgs = results['imgs'] + spatial_sample_index = results['spatial_sample_index'] + spatial_num_clips = results['spatial_num_clips'] + + short_cycle_idx = results.get('short_cycle_idx') + if short_cycle_idx in [0, 1]: + self.target_size = int( + round(self.short_cycle_factors[short_cycle_idx] * + self.default_crop_size)) + else: + self.target_size = self.orig_target_size # use saved value before call + + w, h = imgs[0].size + if w == self.target_size and h == self.target_size: + return results + + assert (w >= self.target_size) and (h >= self.target_size), \ + "image width({}) and height({}) should be larger than crop size({},{})".format(w, h, self.target_size, self.target_size) + frames_crop = [] + if not self.test_mode: + x_offset = random.randint(0, w - self.target_size) + y_offset = random.randint(0, h - self.target_size) + else: # multi-crop + x_gap = int( + math.ceil((w - self.target_size) / (spatial_num_clips - 1))) + y_gap = int( + math.ceil((h - self.target_size) / (spatial_num_clips - 1))) + if h > w: + x_offset = int(math.ceil((w - self.target_size) / 2)) + if spatial_sample_index == 0: + y_offset = 0 + elif spatial_sample_index == spatial_num_clips - 1: + y_offset = h - self.target_size + else: + y_offset = y_gap * spatial_sample_index + else: + y_offset = int(math.ceil((h - self.target_size) / 2)) + if spatial_sample_index == 0: + x_offset = 0 + elif spatial_sample_index == spatial_num_clips - 1: + x_offset = w - self.target_size + else: + x_offset = x_gap * spatial_sample_index + + for img in imgs: + nimg = img.crop((x_offset, y_offset, x_offset + self.target_size, + y_offset + self.target_size)) + frames_crop.append(nimg) + results['imgs'] = frames_crop + return results + + +@PIPELINES.register() +class PackOutput(object): + """ + In slowfast model, we want to get slow pathway from fast pathway based on + alpha factor. + Args: + alpha(int): temporal length of fast/slow + """ + def __init__(self, alpha): + self.alpha = alpha + + def __call__(self, results): + fast_pathway = results['imgs'] + + # sample num points between start and end + slow_idx_start = 0 + slow_idx_end = fast_pathway.shape[0] - 1 + slow_idx_num = fast_pathway.shape[0] // self.alpha + slow_idxs_select = np.linspace(slow_idx_start, slow_idx_end, + slow_idx_num).astype("int64") + slow_pathway = fast_pathway[slow_idxs_select] + + # T H W C -> C T H W. + slow_pathway = slow_pathway.transpose(3, 0, 1, 2) + fast_pathway = fast_pathway.transpose(3, 0, 1, 2) + + # slow + fast + frames_list = [slow_pathway, fast_pathway] + results['imgs'] = frames_list + return results + + +@PIPELINES.register() +class GroupFullResSample(object): + def __init__(self, crop_size, flip=False): + self.crop_size = crop_size if not isinstance(crop_size, int) else ( + crop_size, crop_size) + self.flip = flip + + def __call__(self, results): + img_group = results['imgs'] + + image_w, image_h = img_group[0].size + crop_w, crop_h = self.crop_size + + w_step = (image_w - crop_w) // 4 + h_step = (image_h - crop_h) // 4 + + offsets = list() + offsets.append((0 * w_step, 2 * h_step)) # left + offsets.append((4 * w_step, 2 * h_step)) # right + offsets.append((2 * w_step, 2 * h_step)) # center + + oversample_group = list() + for o_w, o_h in offsets: + normal_group = list() + flip_group = list() + for i, img in enumerate(img_group): + crop = img.crop((o_w, o_h, o_w + crop_w, o_h + crop_h)) + normal_group.append(crop) + if self.flip: + flip_crop = crop.copy().transpose(Image.FLIP_LEFT_RIGHT) + flip_group.append(flip_crop) + + oversample_group.extend(normal_group) + if self.flip: + oversample_group.extend(flip_group) + + results['imgs'] = oversample_group + return results + + +@PIPELINES.register() +class TenCrop: + """ + Crop out 5 regions (4 corner points + 1 center point) from the picture, + and then flip the cropping result to get 10 cropped images, which can make the prediction result more robust. + Args: + target_size(int | tuple[int]): (w, h) of target size for crop. + """ + def __init__(self, target_size): + self.target_size = (target_size, target_size) + + def __call__(self, results): + imgs = results['imgs'] + img_w, img_h = imgs[0].size + crop_w, crop_h = self.target_size + w_step = (img_w - crop_w) // 4 + h_step = (img_h - crop_h) // 4 + offsets = [ + (0, 0), + (4 * w_step, 0), + (0, 4 * h_step), + (4 * w_step, 4 * h_step), + (2 * w_step, 2 * h_step), + ] + img_crops = list() + for x_offset, y_offset in offsets: + crop = [ + img.crop( + (x_offset, y_offset, x_offset + crop_w, y_offset + crop_h)) + for img in imgs + ] + crop_fliped = [ + timg.transpose(Image.FLIP_LEFT_RIGHT) for timg in crop + ] + img_crops.extend(crop) + img_crops.extend(crop_fliped) + + results['imgs'] = img_crops + return results + + +@PIPELINES.register() +class UniformCrop: + """ + Perform uniform spatial sampling on the images, + select the two ends of the long side and the middle position (left middle right or top middle bottom) 3 regions. + Args: + target_size(int | tuple[int]): (w, h) of target size for crop. + """ + def __init__(self, target_size, backend='cv2'): + if isinstance(target_size, tuple): + self.target_size = target_size + elif isinstance(target_size, int): + self.target_size = (target_size, target_size) + else: + raise TypeError( + f'target_size must be int or tuple[int], but got {type(target_size)}' + ) + self.backend = backend + + def __call__(self, results): + + imgs = results['imgs'] + if 'backend' in results and results['backend'] == 'pyav': # [c,t,h,w] + img_h, img_w = imgs.shape[2:] + elif self.backend == 'pillow': + img_w, img_h = imgs[0].size + else: + img_h, img_w = imgs[0].shape[:2] + + crop_w, crop_h = self.target_size + if crop_h == img_h: + w_step = (img_w - crop_w) // 2 + offsets = [ + (0, 0), + (w_step * 2, 0), + (w_step, 0), + ] + elif crop_w == img_w: + h_step = (img_h - crop_h) // 2 + offsets = [ + (0, 0), + (0, h_step * 2), + (0, h_step), + ] + else: + raise ValueError( + f"img_w({img_w}) == crop_w({crop_w}) or img_h({img_h}) == crop_h({crop_h})" + ) + img_crops = [] + if 'backend' in results and results['backend'] == 'pyav': # [c,t,h,w] + for x_offset, y_offset in offsets: + crop = imgs[:, :, y_offset:y_offset + crop_h, + x_offset:x_offset + crop_w] + img_crops.append(crop) + img_crops = paddle.concat(img_crops, axis=1) + else: + if self.backend == 'pillow': + for x_offset, y_offset in offsets: + crop = [ + img.crop((x_offset, y_offset, x_offset + crop_w, + y_offset + crop_h)) for img in imgs + ] + img_crops.extend(crop) + else: + for x_offset, y_offset in offsets: + crop = [ + img[y_offset:y_offset + crop_h, + x_offset:x_offset + crop_w] for img in imgs + ] + img_crops.extend(crop) + results['imgs'] = img_crops + return results + + +@PIPELINES.register() +class GroupResize(object): + def __init__(self, height, width, scale, K, mode='train'): + self.height = height + self.width = width + self.scale = scale + self.resize = {} + self.K = np.array(K, dtype=np.float32) + self.mode = mode + for i in range(self.scale): + s = 2**i + self.resize[i] = paddle.vision.transforms.Resize( + (self.height // s, self.width // s), interpolation='lanczos') + + def __call__(self, results): + if self.mode == 'infer': + imgs = results['imgs'] + for k in list(imgs): # ("color", 0, -1) + if "color" in k or "color_n" in k: + n, im, _ = k + for i in range(self.scale): + imgs[(n, im, i)] = self.resize[i](imgs[(n, im, i - 1)]) + else: + imgs = results['imgs'] + for scale in range(self.scale): + K = self.K.copy() + + K[0, :] *= self.width // (2**scale) + K[1, :] *= self.height // (2**scale) + + inv_K = np.linalg.pinv(K) + imgs[("K", scale)] = K + imgs[("inv_K", scale)] = inv_K + + for k in list(imgs): + if "color" in k or "color_n" in k: + n, im, i = k + for i in range(self.scale): + imgs[(n, im, i)] = self.resize[i](imgs[(n, im, i - 1)]) + + results['imgs'] = imgs + return results + + +@PIPELINES.register() +class ColorJitter(object): + """Randomly change the brightness, contrast, saturation and hue of an image. + """ + def __init__(self, + brightness=0, + contrast=0, + saturation=0, + hue=0, + mode='train', + p=0.5, + keys=None): + self.mode = mode + self.colorjitter = paddle.vision.transforms.ColorJitter( + brightness, contrast, saturation, hue) + self.p = p + + def __call__(self, results): + """ + Args: + results (PIL Image): Input image. + + Returns: + PIL Image: Color jittered image. + """ + + do_color_aug = random.random() > self.p + imgs = results['imgs'] + for k in list(imgs): + f = imgs[k] + if "color" in k or "color_n" in k: + n, im, i = k + imgs[(n, im, i)] = f + if do_color_aug: + imgs[(n + "_aug", im, i)] = self.colorjitter(f) + else: + imgs[(n + "_aug", im, i)] = f + if self.mode == "train": + for i in results['frame_idxs']: + del imgs[("color", i, -1)] + del imgs[("color_aug", i, -1)] + del imgs[("color_n", i, -1)] + del imgs[("color_n_aug", i, -1)] + else: + for i in results['frame_idxs']: + del imgs[("color", i, -1)] + del imgs[("color_aug", i, -1)] + + results['img'] = imgs + return results + + +@PIPELINES.register() +class GroupRandomFlip(object): + def __init__(self, p=0.5): + self.p = p + + def __call__(self, results): + + imgs = results['imgs'] + do_flip = random.random() > self.p + if do_flip: + for k in list(imgs): + if "color" in k or "color_n" in k: + n, im, i = k + imgs[(n, im, + i)] = imgs[(n, im, + i)].transpose(Image.FLIP_LEFT_RIGHT) + if "depth_gt" in imgs: + imgs['depth_gt'] = np.array(np.fliplr(imgs['depth_gt'])) + + results['imgs'] = imgs + return results + + +@PIPELINES.register() +class ToArray(object): + def __init__(self): + pass + + def __call__(self, results): + imgs = results['imgs'] + for k in list(imgs): + if "color" in k or "color_n" in k or "color_aug" in k or "color_n_aug" in k: + n, im, i = k + imgs[(n, im, + i)] = np.array(imgs[(n, im, i)]).astype('float32') / 255.0 + imgs[(n, im, i)] = imgs[(n, im, i)].transpose((2, 0, 1)) + if "depth_gt" in imgs: + imgs['depth_gt'] = np.array(imgs['depth_gt']).astype('float32') + + results['imgs'] = imgs + return results diff --git a/paddlevideo/loader/pipelines/augmentations_ava.py b/paddlevideo/loader/pipelines/augmentations_ava.py new file mode 100644 index 0000000000000000000000000000000000000000..e7cbe3c39a4463f9ace01d8dd611b8447e48a472 --- /dev/null +++ b/paddlevideo/loader/pipelines/augmentations_ava.py @@ -0,0 +1,730 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import numpy as np +import math +from PIL import Image +from ..registry import PIPELINES +from collections.abc import Sequence +import cv2 + +pillow_interp_codes = { + 'nearest': Image.NEAREST, + 'bilinear': Image.BILINEAR, + 'bicubic': Image.BICUBIC, + 'box': Image.BOX, + 'lanczos': Image.LANCZOS, + 'hamming': Image.HAMMING +} + +cv2_interp_codes = { + 'nearest': cv2.INTER_NEAREST, + 'bilinear': cv2.INTER_LINEAR, + 'bicubic': cv2.INTER_CUBIC, + 'area': cv2.INTER_AREA, + 'lanczos': cv2.INTER_LANCZOS4 +} + +def _init_lazy_if_proper(results, lazy): + """Initialize lazy operation properly. + + Make sure that a lazy operation is properly initialized, + and avoid a non-lazy operation accidentally getting mixed in. + + Required keys in results are "imgs" if "img_shape" not in results, + otherwise, Required keys in results are "img_shape", add or modified keys + are "img_shape", "lazy". + Add or modified keys in "lazy" are "original_shape", "crop_bbox", "flip", + "flip_direction", "interpolation". + + Args: + results (dict): A dict stores data pipeline result. + lazy (bool): Determine whether to apply lazy operation. Default: False. + """ + + if 'img_shape' not in results: + results['img_shape'] = results['imgs'][0].shape[:2] + if lazy: + if 'lazy' not in results: + img_h, img_w = results['img_shape'] + lazyop = dict() + lazyop['original_shape'] = results['img_shape'] + lazyop['crop_bbox'] = np.array([0, 0, img_w, img_h], + dtype=np.float32) + lazyop['flip'] = False + lazyop['flip_direction'] = None + lazyop['interpolation'] = None + results['lazy'] = lazyop + else: + assert 'lazy' not in results, 'Use Fuse after lazy operations' + +def _scale_size(size, scale): + """Rescale a size by a ratio. + + Args: + size (tuple[int]): (w, h). + scale (float): Scaling factor. + + Returns: + tuple[int]: scaled size. + """ + w, h = size + return int(w * float(scale) + 0.5), int(h * float(scale) + 0.5) + +def rescale_size(old_size, scale, return_scale=False): + """Calculate the new size to be rescaled to. + + Args: + old_size (tuple[int]): The old size (w, h) of image. + scale (float | tuple[int]): The scaling factor or maximum size. + If it is a float number, then the image will be rescaled by this + factor, else if it is a tuple of 2 integers, then the image will + be rescaled as large as possible within the scale. + return_scale (bool): Whether to return the scaling factor besides the + rescaled image size. + + Returns: + tuple[int]: The new rescaled image size. + """ + w, h = old_size + if isinstance(scale, (float, int)): + if scale <= 0: + raise ValueError(f'Invalid scale {scale}, must be positive.') + scale_factor = scale + elif isinstance(scale, tuple): + max_long_edge = max(scale) + max_short_edge = min(scale) + scale_factor = min(max_long_edge / max(h, w), + max_short_edge / min(h, w)) + else: + raise TypeError( + f'Scale must be a number or tuple of int, but got {type(scale)}') + + new_size = _scale_size((w, h), scale_factor) + + if return_scale: + return new_size, scale_factor + else: + return new_size + +def imresize(img, + size, + return_scale=False, + interpolation='bilinear', + out=None, + backend=None): + """Resize image to a given size. """ + h, w = img.shape[:2] + if backend is None: + backend = 'cv2' + if backend not in ['cv2', 'pillow']: + raise ValueError(f'backend: {backend} is not supported for resize.' + f"Supported backends are 'cv2', 'pillow'") + + if backend == 'pillow': + assert img.dtype == np.uint8, 'Pillow backend only support uint8 type' + pil_image = Image.fromarray(img) + pil_image = pil_image.resize(size, pillow_interp_codes[interpolation]) + resized_img = np.array(pil_image) + else: + resized_img = cv2.resize( + img, size, dst=out, interpolation=cv2_interp_codes[interpolation]) + if not return_scale: + return resized_img + else: + w_scale = size[0] / w + h_scale = size[1] / h + return resized_img, w_scale, h_scale + +@PIPELINES.register() +class EntityBoxRescale: + """Rescale the entity box and proposals according to the image shape. + + Required keys are "proposals", "gt_bboxes", added or modified keys are + "gt_bboxes". If original "proposals" is not None, "proposals" and + will be added or modified. + + Args: + scale_factor (np.ndarray): The scale factor used entity_box rescaling. + """ + + def __init__(self, scale_factor): + self.scale_factor = scale_factor + + def __call__(self, results): + scale_factor = np.concatenate([self.scale_factor, self.scale_factor]) + + if 'gt_bboxes' in results: + gt_bboxes = results['gt_bboxes'] + results['gt_bboxes'] = gt_bboxes * scale_factor + + if 'proposals' in results: + proposals = results['proposals'] + if proposals is not None: + assert proposals.shape[1] == 4, ( + 'proposals shape should be in ' + f'(n, 4), but got {proposals.shape}') + results['proposals'] = proposals * scale_factor + + return results + + def __repr__(self): + return f'{self.__class__.__name__}(scale_factor={self.scale_factor})' + +@PIPELINES.register() +class EntityBoxCrop: + """Crop the entity boxes and proposals according to the cropped images. + + Required keys are "proposals", "gt_bboxes", added or modified keys are + "gt_bboxes". If original "proposals" is not None, "proposals" will be + modified. + + Args: + crop_bbox(np.ndarray | None): The bbox used to crop the original image. + """ + + def __init__(self, crop_bbox): + self.crop_bbox = crop_bbox + + def __call__(self, results): + proposals = results['proposals'] + gt_bboxes = results['gt_bboxes'] + + if self.crop_bbox is None: + return results + + x1, y1, x2, y2 = self.crop_bbox + img_w, img_h = x2 - x1, y2 - y1 + + assert gt_bboxes.shape[-1] == 4 + gt_bboxes_ = gt_bboxes.copy() + gt_bboxes_[..., 0::2] = np.clip(gt_bboxes[..., 0::2] - x1, 0, img_w - 1) + gt_bboxes_[..., 1::2] = np.clip(gt_bboxes[..., 1::2] - y1, 0, img_h - 1) + results['gt_bboxes'] = gt_bboxes_ + + if proposals is not None: + assert proposals.shape[-1] == 4 + proposals_ = proposals.copy() + proposals_[..., 0::2] = np.clip(proposals[..., 0::2] - x1, 0, img_w - 1) + proposals_[..., 1::2] = np.clip(proposals[..., 1::2] - y1, 0, img_h - 1) + results['proposals'] = proposals_ + return results + + def __repr__(self): + return f'{self.__class__.__name__}(crop_bbox={self.crop_bbox})' + +@PIPELINES.register() +class EntityBoxFlip: + """Flip the entity boxes and proposals with a probability. + + Reverse the order of elements in the given bounding boxes and proposals + with a specific direction. The shape of them are preserved, but the + elements are reordered. Only the horizontal flip is supported (seems + vertical flipping makes no sense). Required keys are "proposals", + "gt_bboxes", added or modified keys are "gt_bboxes". If "proposals" + is not None, it will also be modified. + + Args: + img_shape (tuple[int]): The img shape. + """ + + def __init__(self, img_shape): + self.img_shape = img_shape + + def __call__(self, results): + proposals = results['proposals'] + gt_bboxes = results['gt_bboxes'] + img_h, img_w = self.img_shape + + assert gt_bboxes.shape[-1] == 4 + gt_bboxes_ = gt_bboxes.copy() + gt_bboxes_[..., 0::4] = img_w - gt_bboxes[..., 2::4] - 1 + gt_bboxes_[..., 2::4] = img_w - gt_bboxes[..., 0::4] - 1 + if proposals is not None: + assert proposals.shape[-1] == 4 + proposals_ = proposals.copy() + proposals_[..., 0::4] = img_w - proposals[..., 2::4] - 1 + proposals_[..., 2::4] = img_w - proposals[..., 0::4] - 1 + else: + proposals_ = None + + results['proposals'] = proposals_ + results['gt_bboxes'] = gt_bboxes_ + + return results + + def __repr__(self): + repr_str = f'{self.__class__.__name__}(img_shape={self.img_shape})' + return repr_str + + +@PIPELINES.register() +class Resize: + """Resize images to a specific size. + + Required keys are "imgs", "img_shape", "modality", added or modified + keys are "imgs", "img_shape", "keep_ratio", "scale_factor", "lazy", + "resize_size". Required keys in "lazy" is None, added or modified key is + "interpolation". + + Args: + scale (float | Tuple[int]): If keep_ratio is True, it serves as scaling + factor or maximum size: + If it is a float number, the image will be rescaled by this + factor, else if it is a tuple of 2 integers, the image will + be rescaled as large as possible within the scale. + Otherwise, it serves as (w, h) of output size. + keep_ratio (bool): If set to True, Images will be resized without + changing the aspect ratio. Otherwise, it will resize images to a + given size. Default: True. + interpolation (str): Algorithm used for interpolation: + "nearest" | "bilinear". Default: "bilinear". + lazy (bool): Determine whether to apply lazy operation. Default: False. + """ + + def __init__(self, + scale, + keep_ratio=True, + interpolation='bilinear', + lazy=False): + if isinstance(scale, float): + if scale <= 0: + raise ValueError(f'Invalid scale {scale}, must be positive.') + elif isinstance(scale, tuple): + max_long_edge = max(scale) + max_short_edge = min(scale) + if max_short_edge == -1: + # assign np.inf to long edge for rescaling short edge later. + scale = (np.inf, max_long_edge) + else: + raise TypeError( + f'Scale must be float or tuple of int, but got {type(scale)}') + self.scale = scale + self.keep_ratio = keep_ratio + self.interpolation = interpolation + self.lazy = lazy + + def __call__(self, results): + """Performs the Resize augmentation. + + Args: + results (dict): The resulting dict to be modified and passed + to the next transform in pipeline. + """ + + _init_lazy_if_proper(results, self.lazy) + + if 'scale_factor' not in results: + results['scale_factor'] = np.array([1, 1], dtype=np.float32) + img_h, img_w = results['img_shape'] + + if self.keep_ratio: + new_w, new_h = rescale_size((img_w, img_h), self.scale) + else: + new_w, new_h = self.scale + + self.scale_factor = np.array([new_w / img_w, new_h / img_h], + dtype=np.float32) + results['img_shape'] = (new_h, new_w) + results['keep_ratio'] = self.keep_ratio + results['scale_factor'] = results['scale_factor'] * self.scale_factor + + + if not self.lazy: + results['imgs'] = [ + imresize( + img, (new_w, new_h), interpolation=self.interpolation) + for img in results['imgs'] + ] + else: + lazyop = results['lazy'] + if lazyop['flip']: + raise NotImplementedError('Put Flip at last for now') + lazyop['interpolation'] = self.interpolation + + #if 'gt_bboxes' in results: + assert not self.lazy + entity_box_rescale = EntityBoxRescale(self.scale_factor) + results = entity_box_rescale(results) + + return results + + def __repr__(self): + repr_str = (f'{self.__class__.__name__}(' + f'scale={self.scale}, keep_ratio={self.keep_ratio}, ' + f'interpolation={self.interpolation}, ' + f'lazy={self.lazy})') + return repr_str + +@PIPELINES.register() +class RandomRescale: + """Randomly resize images so that the short_edge is resized to a specific + size in a given range. The scale ratio is unchanged after resizing. + """ + + def __init__(self, scale_range, interpolation='bilinear'): + scale_range = eval(scale_range) + self.scale_range = scale_range + + assert len(scale_range) == 2 + assert scale_range[0] < scale_range[1] + assert np.all([x > 0 for x in scale_range]) + + self.keep_ratio = True + self.interpolation = interpolation + + def __call__(self, results): + """Performs the Resize augmentation. + + Args: + results (dict): The resulting dict to be modified and passed + to the next transform in pipeline. + """ + short_edge = np.random.randint(self.scale_range[0], + self.scale_range[1] + 1) + resize = Resize((-1, short_edge), + keep_ratio=True, + interpolation=self.interpolation, + lazy=False) + results = resize(results) + + results['short_edge'] = short_edge + return results + + def __repr__(self): + scale_range = self.scale_range + repr_str = (f'{self.__class__.__name__}(' + f'scale_range=({scale_range[0]}, {scale_range[1]}), ' + f'interpolation={self.interpolation})') + return repr_str + +@PIPELINES.register() +class Rescale: + """resize images so that the short_edge is resized to a specific + size in a given range. The scale ratio is unchanged after resizing. + + Required keys are "imgs", "img_shape", "modality", added or modified + keys are "imgs", "img_shape", "keep_ratio", "scale_factor", "resize_size", + "short_edge". + + Args: + scale_range (tuple[int]): The range of short edge length. A closed + interval. + interpolation (str): Algorithm used for interpolation: + "nearest" | "bilinear". Default: "bilinear". + """ + + def __init__(self, scale_range, interpolation='bilinear'): + scale_range = eval(scale_range) + self.scale_range = scale_range + + self.keep_ratio = True + self.interpolation = interpolation + + def __call__(self, results): + """Performs the Resize augmentation. + + Args: + results (dict): The resulting dict to be modified and passed + to the next transform in pipeline. + """ + resize = Resize(self.scale_range, + keep_ratio=True, + interpolation=self.interpolation, + lazy=False) + results = resize(results) + return results + + def __repr__(self): + scale_range = self.scale_range + repr_str = (f'{self.__class__.__name__}(' + f'scale_range=({scale_range[0]}, {scale_range[1]}), ' + f'interpolation={self.interpolation})') + return repr_str + + +@PIPELINES.register() +class RandomCrop_v2: + """Vanilla square random crop that specifics the output size. + + Required keys in results are "imgs" and "img_shape", added or + modified keys are "imgs", "lazy"; Required keys in "lazy" are "flip", + "crop_bbox", added or modified key is "crop_bbox". + + Args: + size (int): The output size of the images. + lazy (bool): Determine whether to apply lazy operation. Default: False. + """ + + def __init__(self, size, lazy=False): + if not isinstance(size, int): + raise TypeError(f'Size must be an int, but got {type(size)}') + self.size = size + self.lazy = lazy + + def __call__(self, results): + """Performs the RandomCrop augmentation. + + Args: + results (dict): The resulting dict to be modified and passed + to the next transform in pipeline. + """ + _init_lazy_if_proper(results, self.lazy) + + img_h, img_w = results['img_shape'] + assert self.size <= img_h and self.size <= img_w + + y_offset = 0 + x_offset = 0 + if img_h > self.size: + y_offset = int(np.random.randint(0, img_h - self.size)) + if img_w > self.size: + x_offset = int(np.random.randint(0, img_w - self.size)) + if 'crop_quadruple' not in results: + results['crop_quadruple'] = np.array( + [0, 0, 1, 1], # x, y, w, h + dtype=np.float32) + + x_ratio, y_ratio = x_offset / img_w, y_offset / img_h + w_ratio, h_ratio = self.size / img_w, self.size / img_h + + old_crop_quadruple = results['crop_quadruple'] + old_x_ratio, old_y_ratio = old_crop_quadruple[0], old_crop_quadruple[1] + old_w_ratio, old_h_ratio = old_crop_quadruple[2], old_crop_quadruple[3] + new_crop_quadruple = [ + old_x_ratio + x_ratio * old_w_ratio, + old_y_ratio + y_ratio * old_h_ratio, w_ratio * old_w_ratio, + h_ratio * old_x_ratio + ] + results['crop_quadruple'] = np.array( new_crop_quadruple, dtype=np.float32) + + new_h, new_w = self.size, self.size + + results['crop_bbox'] = np.array( [x_offset, y_offset, x_offset + new_w, y_offset + new_h]) + results['img_shape'] = (new_h, new_w) + + if not self.lazy: + results['imgs'] = [ + img[y_offset:y_offset + new_h, x_offset:x_offset + new_w] + for img in results['imgs'] + ] + else: + lazyop = results['lazy'] + if lazyop['flip']: + raise NotImplementedError('Put Flip at last for now') + + # record crop_bbox in lazyop dict to ensure only crop once in Fuse + lazy_left, lazy_top, lazy_right, lazy_bottom = lazyop['crop_bbox'] + left = x_offset * (lazy_right - lazy_left) / img_w + right = (x_offset + new_w) * (lazy_right - lazy_left) / img_w + top = y_offset * (lazy_bottom - lazy_top) / img_h + bottom = (y_offset + new_h) * (lazy_bottom - lazy_top) / img_h + lazyop['crop_bbox'] = np.array([(lazy_left + left), + (lazy_top + top), + (lazy_left + right), + (lazy_top + bottom)], + dtype=np.float32) + + # Process entity boxes + if 'gt_bboxes' in results: + assert not self.lazy + entity_box_crop = EntityBoxCrop(results['crop_bbox']) + results = entity_box_crop(results) + + return results + + def __repr__(self): + repr_str = (f'{self.__class__.__name__}(size={self.size}, ' + f'lazy={self.lazy})') + return repr_str + +def imflip_(img, direction='horizontal'): + """Inplace flip an image horizontally or vertically. + + Args: + img (ndarray): Image to be flipped. + direction (str): The flip direction, either "horizontal" or + "vertical" or "diagonal". + + Returns: + ndarray: The flipped image (inplace). + """ + assert direction in ['horizontal', 'vertical', 'diagonal'] + if direction == 'horizontal': + return cv2.flip(img, 1, img) + elif direction == 'vertical': + return cv2.flip(img, 0, img) + else: + return cv2.flip(img, -1, img) + +def iminvert(img): + """Invert (negate) an image. + + Args: + img (ndarray): Image to be inverted. + + Returns: + ndarray: The inverted image. + """ + return np.full_like(img, 255) - img + +@PIPELINES.register() +class Flip: + """Flip the input images with a probability. + + Reverse the order of elements in the given imgs with a specific direction. + The shape of the imgs is preserved, but the elements are reordered. + Required keys are "imgs", "img_shape", "modality", added or modified + keys are "imgs", "lazy" and "flip_direction". Required keys in "lazy" is + None, added or modified key are "flip" and "flip_direction". The Flip + augmentation should be placed after any cropping / reshaping augmentations, + to make sure crop_quadruple is calculated properly. + + Args: + flip_ratio (float): Probability of implementing flip. Default: 0.5. + direction (str): Flip imgs horizontally or vertically. Options are + "horizontal" | "vertical". Default: "horizontal". + lazy (bool): Determine whether to apply lazy operation. Default: False. + """ + _directions = ['horizontal', 'vertical'] + + def __init__(self, flip_ratio=0.5, direction='horizontal', lazy=False): + if direction not in self._directions: + raise ValueError(f'Direction {direction} is not supported. ' + f'Currently support ones are {self._directions}') + self.flip_ratio = flip_ratio + self.direction = direction + self.lazy = lazy + + def __call__(self, results): + """Performs the Flip augmentation. + + Args: + results (dict): The resulting dict to be modified and passed + to the next transform in pipeline. + """ + _init_lazy_if_proper(results, self.lazy) + flip = np.random.rand() < self.flip_ratio + + results['flip'] = flip + results['flip_direction'] = self.direction + + if not self.lazy: + if flip: + for i, img in enumerate(results['imgs']): + imflip_(img, self.direction) + lt = len(results['imgs']) + else: + results['imgs'] = list(results['imgs']) + else: + lazyop = results['lazy'] + if lazyop['flip']: + raise NotImplementedError('Use one Flip please') + lazyop['flip'] = flip + lazyop['flip_direction'] = self.direction + + if 'gt_bboxes' in results and flip: + assert not self.lazy and self.direction == 'horizontal' + entity_box_flip = EntityBoxFlip(results['img_shape']) + results = entity_box_flip(results) + + return results + + def __repr__(self): + repr_str = ( + f'{self.__class__.__name__}(' + f'flip_ratio={self.flip_ratio}, direction={self.direction}, ' + f'lazy={self.lazy})') + return repr_str + +def imnormalize_(img, mean, std, to_rgb=True): + """Inplace normalize an image with mean and std. + + Args: + img (ndarray): Image to be normalized. + mean (ndarray): The mean to be used for normalize. + std (ndarray): The std to be used for normalize. + to_rgb (bool): Whether to convert to rgb. + + Returns: + ndarray: The normalized image. + """ + # cv2 inplace normalization does not accept uint8 + assert img.dtype != np.uint8 + mean = np.float64(mean.reshape(1, -1)) + stdinv = 1 / np.float64(std.reshape(1, -1)) + if to_rgb: + cv2.cvtColor(img, cv2.COLOR_BGR2RGB, img) # inplace + cv2.subtract(img, mean, img) # inplace + cv2.multiply(img, stdinv, img) # inplace + return img + +@PIPELINES.register() +class Normalize: + """Normalize images with the given mean and std value. + + Required keys are "imgs", "img_shape", "modality", added or modified + keys are "imgs" and "img_norm_cfg". If modality is 'Flow', additional + keys "scale_factor" is required + + Args: + mean (Sequence[float]): Mean values of different channels. + std (Sequence[float]): Std values of different channels. + to_bgr (bool): Whether to convert channels from RGB to BGR. + Default: False. + adjust_magnitude (bool): Indicate whether to adjust the flow magnitude + on 'scale_factor' when modality is 'Flow'. Default: False. + """ + + def __init__(self, mean, std, to_bgr=False, adjust_magnitude=False): + if not isinstance(mean, Sequence): + raise TypeError( + f'Mean must be list, tuple or np.ndarray, but got {type(mean)}' + ) + + if not isinstance(std, Sequence): + raise TypeError( + f'Std must be list, tuple or np.ndarray, but got {type(std)}') + + self.mean = np.array(mean, dtype=np.float32) + self.std = np.array(std, dtype=np.float32) + self.to_bgr = to_bgr + self.adjust_magnitude = adjust_magnitude + + def __call__(self, results): + n = len(results['imgs']) + h, w, c = results['imgs'][0].shape + imgs = np.empty((n, h, w, c), dtype=np.float32) + for i, img in enumerate(results['imgs']): + imgs[i] = img + + for img in imgs: + imnormalize_(img, self.mean, self.std, self.to_bgr) + + results['imgs'] = imgs + results['img_norm_cfg'] = dict( + mean=self.mean, std=self.std, to_bgr=self.to_bgr) + + return results + + def __repr__(self): + repr_str = (f'{self.__class__.__name__}(' + f'mean={self.mean}, ' + f'std={self.std}, ' + f'to_bgr={self.to_bgr}, ' + f'adjust_magnitude={self.adjust_magnitude})') + return repr_str + + diff --git a/paddlevideo/loader/pipelines/compose.py b/paddlevideo/loader/pipelines/compose.py new file mode 100644 index 0000000000000000000000000000000000000000..76eb4ed4d436f692a25081dbe8efe9a9b9a11102 --- /dev/null +++ b/paddlevideo/loader/pipelines/compose.py @@ -0,0 +1,76 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Sequence +from ..registry import PIPELINES +import traceback +from ...utils import build +from ...utils import get_logger + + +@PIPELINES.register() +class Compose(object): + """ + Composes several pipelines(include decode func, sample func, and transforms) together. + + Note: To deal with ```list``` type cfg temporaray, like: + + transform: + - Crop: # A list + attribute: 10 + - Resize: # A list + attribute: 20 + + every key of list will pass as the key name to build a module. + XXX: will be improved in the future. + + Args: + pipelines (list): List of transforms to compose. + Returns: + A compose object which is callable, __call__ for this Compose + object will call each given :attr:`transforms` sequencely. + """ + def __init__(self, pipelines): + #assert isinstance(pipelines, Sequence) + self.pipelines = [] + for p in pipelines.values(): + if isinstance(p, dict): + p = build(p, PIPELINES) + self.pipelines.append(p) + elif isinstance(p, list): + for t in p: + #XXX: to deal with old format cfg, ugly code here! + temp_dict = dict(name=list(t.keys())[0]) + for all_sub_t in t.values(): + if all_sub_t is not None: + temp_dict.update(all_sub_t) + + t = build(temp_dict, PIPELINES) + self.pipelines.append(t) + elif callable(p): + self.pipelines.append(p) + else: + raise TypeError(f'pipelines must be callable or a dict,' + f'but got {type(p)}') + def __call__(self, data): + for p in self.pipelines: + try: + data = p(data) + except Exception as e: + stack_info = traceback.format_exc() + logger = get_logger("paddlevideo") + logger.info("fail to perform transform [{}] with error: " + "{} and stack:\n{}".format(p, e, str(stack_info))) + raise e + return data diff --git a/paddlevideo/loader/pipelines/decode.py b/paddlevideo/loader/pipelines/decode.py new file mode 100644 index 0000000000000000000000000000000000000000..478ea04e513859c939ba16a3b93292f9895bf08a --- /dev/null +++ b/paddlevideo/loader/pipelines/decode.py @@ -0,0 +1,285 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import av +import cv2 +import pickle +import decord as de +import math +import random +from ..registry import PIPELINES + + +def get_start_end_idx(video_size, clip_size, clip_idx, num_clips): + delta = max(video_size - clip_size, 0) + if clip_idx == -1: # here + # Random temporal sampling. + start_idx = random.uniform(0, delta) + else: # ignore + # Uniformly sample the clip with the given index. + start_idx = delta * clip_idx / num_clips + end_idx = start_idx + clip_size - 1 + return start_idx, end_idx + + +@PIPELINES.register() +class VideoDecoder(object): + """ + Decode mp4 file to frames. + Args: + filepath: the file path of mp4 file + """ + def __init__(self, + backend='cv2', + mode='train', + sampling_rate=32, + num_seg=8, + num_clips=1, + target_fps=30): + + self.backend = backend + # params below only for TimeSformer + self.mode = mode + self.sampling_rate = sampling_rate + self.num_seg = num_seg + self.num_clips = num_clips + self.target_fps = target_fps + + def __call__(self, results): + """ + Perform mp4 decode operations. + return: + List where each item is a numpy array after decoder. + """ + file_path = results['filename'] + results['format'] = 'video' + results['backend'] = self.backend + + if self.backend == 'cv2': + cap = cv2.VideoCapture(file_path) + videolen = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + sampledFrames = [] + for i in range(videolen): + ret, frame = cap.read() + # maybe first frame is empty + if ret == False: + continue + img = frame[:, :, ::-1] + sampledFrames.append(img) + results['frames'] = sampledFrames + results['frames_len'] = len(sampledFrames) + + elif self.backend == 'decord': + container = de.VideoReader(file_path) + frames_len = len(container) + results['frames'] = container + results['frames_len'] = frames_len + + elif self.backend == 'pyav': # for TimeSformer + if self.mode in ["train", "valid"]: + clip_idx = -1 + elif self.mode in ["test"]: + clip_idx = 0 + else: + raise NotImplementedError + + container = av.open(file_path) + + num_clips = 1 # always be 1 + + # decode process + fps = float(container.streams.video[0].average_rate) + + frames_length = container.streams.video[0].frames + duration = container.streams.video[0].duration + + if duration is None: + # If failed to fetch the decoding information, decode the entire video. + decode_all_video = True + video_start_pts, video_end_pts = 0, math.inf + else: + decode_all_video = False + start_idx, end_idx = get_start_end_idx( + frames_length, + self.sampling_rate * self.num_seg / self.target_fps * fps, + clip_idx, num_clips) + timebase = duration / frames_length + video_start_pts = int(start_idx * timebase) + video_end_pts = int(end_idx * timebase) + + frames = None + # If video stream was found, fetch video frames from the video. + if container.streams.video: + margin = 1024 + seek_offset = max(video_start_pts - margin, 0) + + container.seek(seek_offset, + any_frame=False, + backward=True, + stream=container.streams.video[0]) + tmp_frames = {} + buffer_count = 0 + max_pts = 0 + for frame in container.decode(**{"video": 0}): + max_pts = max(max_pts, frame.pts) + if frame.pts < video_start_pts: + continue + if frame.pts <= video_end_pts: + tmp_frames[frame.pts] = frame + else: + buffer_count += 1 + tmp_frames[frame.pts] = frame + if buffer_count >= 0: + break + video_frames = [tmp_frames[pts] for pts in sorted(tmp_frames)] + + container.close() + + frames = [frame.to_rgb().to_ndarray() for frame in video_frames] + clip_sz = self.sampling_rate * self.num_seg / self.target_fps * fps + + start_idx, end_idx = get_start_end_idx( + len(frames), # frame_len + clip_sz, + clip_idx if decode_all_video else + 0, # If decode all video, -1 in train and valid, 0 in test; + # else, always 0 in train, valid and test, as we has selected clip size frames when decode. + 1) + results['frames'] = frames + results['frames_len'] = len(frames) + results['start_idx'] = start_idx + results['end_idx'] = end_idx + else: + raise NotImplementedError + return results + + +@PIPELINES.register() +class FrameDecoder(object): + """just parse results + """ + def __init__(self): + pass + + def __call__(self, results): + results['format'] = 'frame' + return results + + +@PIPELINES.register() +class MRIDecoder(object): + """just parse results + """ + def __init__(self): + pass + + def __call__(self, results): + results['format'] = 'MRI' + return results + + +@PIPELINES.register() +class FeatureDecoder(object): + """ + Perform feature decode operations.e.g.youtube8m + """ + def __init__(self, num_classes, max_len=512, has_label=True): + self.max_len = max_len + self.num_classes = num_classes + self.has_label = has_label + + def __call__(self, results): + """ + Perform feature decode operations. + return: + List where each item is a numpy array after decoder. + """ + #1. load pkl + #2. parse to rgb/audio/ + #3. padding + + filepath = results['filename'] + data = pickle.load(open(filepath, 'rb'), encoding='bytes') + + record = data + nframes = record['nframes'] if 'nframes' in record else record[ + b'nframes'] + rgb = record['feature'].astype( + float) if 'feature' in record else record[b'feature'].astype(float) + audio = record['audio'].astype( + float) if 'audio' in record else record[b'audio'].astype(float) + if self.has_label: + label = record['label'] if 'label' in record else record[b'label'] + one_hot_label = self.make_one_hot(label, self.num_classes) + + rgb = rgb[0:nframes, :] + audio = audio[0:nframes, :] + + rgb = self.dequantize(rgb, + max_quantized_value=2., + min_quantized_value=-2.) + audio = self.dequantize(audio, + max_quantized_value=2, + min_quantized_value=-2) + + if self.has_label: + results['labels'] = one_hot_label.astype("float32") + + feat_pad_list = [] + feat_len_list = [] + mask_list = [] + vitem = [rgb, audio] + for vi in range(2): #rgb and audio + if vi == 0: + prefix = "rgb_" + else: + prefix = "audio_" + feat = vitem[vi] + results[prefix + 'len'] = feat.shape[0] + #feat pad step 1. padding + feat_add = np.zeros((self.max_len - feat.shape[0], feat.shape[1]), + dtype=np.float32) + feat_pad = np.concatenate((feat, feat_add), axis=0) + results[prefix + 'data'] = feat_pad.astype("float32") + #feat pad step 2. mask + feat_mask_origin = np.ones(feat.shape, dtype=np.float32) + feat_mask_add = feat_add + feat_mask = np.concatenate((feat_mask_origin, feat_mask_add), + axis=0) + results[prefix + 'mask'] = feat_mask.astype("float32") + + return results + + def dequantize(self, + feat_vector, + max_quantized_value=2., + min_quantized_value=-2.): + """ + Dequantize the feature from the byte format to the float format + """ + + assert max_quantized_value > min_quantized_value + quantized_range = max_quantized_value - min_quantized_value + scalar = quantized_range / 255.0 + bias = (quantized_range / 512.0) + min_quantized_value + + return feat_vector * scalar + bias + + def make_one_hot(self, label, dim=3862): + one_hot_label = np.zeros(dim) + one_hot_label = one_hot_label.astype(float) + for ind in label: + one_hot_label[int(ind)] = 1 + return one_hot_label diff --git a/paddlevideo/loader/pipelines/decode_image.py b/paddlevideo/loader/pipelines/decode_image.py new file mode 100644 index 0000000000000000000000000000000000000000..6cc01e9039c24f5fd89419669c0ea5dc8375fa36 --- /dev/null +++ b/paddlevideo/loader/pipelines/decode_image.py @@ -0,0 +1,206 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import numpy as np +import PIL.Image as pil + +try: + import skimage.transform +except ImportError as e: + print( + f"{e}, [scikit-image] package and it's dependencies is required for ADDS." + ) +from PIL import Image + +from ..registry import PIPELINES + + +@PIPELINES.register() +class ImageDecoder(object): + """Decode Image + """ + def __init__(self, + dataset, + frame_idxs, + num_scales, + side_map, + full_res_shape, + img_ext, + backend='cv2'): + self.backend = backend + self.dataset = dataset + self.frame_idxs = frame_idxs + self.num_scales = num_scales + self.side_map = side_map + self.full_res_shape = full_res_shape + self.img_ext = img_ext + + def _pil_loader(self, path): + with open(path, 'rb') as f: + with Image.open(f) as img: + return img.convert('RGB') + + def get_color(self, folder, frame_index, side): + color = self._pil_loader( + self.get_image_path(self.dataset, folder, frame_index, side)) + return color + + def get_image_path(self, dataset, folder, frame_index, side): + if dataset == "kitti": + f_str = "{:010d}{}".format(frame_index, self.img_ext) + image_path = os.path.join(self.data_path, folder, f_str) + elif dataset == "kitti_odom": + f_str = "{:06d}{}".format(frame_index, self.img_ext) + image_path = os.path.join(self.data_path, + "sequences/{:02d}".format(int(folder)), + "image_{}".format(self.side_map[side]), + f_str) + elif dataset == "kitti_depth": + f_str = "{:010d}{}".format(frame_index, self.img_ext) + image_path = os.path.join( + self.data_path, folder, + "image_0{}/data".format(self.side_map[side]), f_str) + + return image_path + + def get_depth(self, dataset, folder, frame_index, side): + if dataset == "kitii_depth": + f_str = "{:010d}.png".format(frame_index) + depth_path = os.path.join( + self.data_path, folder, + "proj_depth/groundtruth/image_0{}".format(self.side_map[side]), + f_str) + + depth_gt = pil.open(depth_path) + depth_gt = depth_gt.resize(self.full_res_shape, pil.NEAREST) + depth_gt = np.array(depth_gt).astype(np.float32) / 256 + + else: + f_str = "{:010d}{}".format(frame_index, self.img_ext) + depth_path = os.path.join(self.data_path, folder + '_gt', f_str) + + img_file = Image.open(depth_path) + depth_png = np.array(img_file, dtype=int) + img_file.close() + # make sure we have a proper 16bit depth map here.. not 8bit! + assert np.max(depth_png) > 255, \ + "np.max(depth_png)={}, path={}".format(np.max(depth_png), depth_path) + + depth_gt = depth_png.astype(np.float) / 256. + + depth_gt = depth_gt[160:960 - 160, :] + + depth_gt = skimage.transform.resize(depth_gt, + self.full_res_shape[::-1], + order=0, + preserve_range=True, + mode='constant') + + return depth_gt + + def __call__(self, results): + """ + Perform mp4 decode operations. + return: + List where each item is a numpy array after decoder. + """ + if results.get('mode', None) == 'infer': + imgs = {} + imgs[("color", 0, + -1)] = Image.open(results["filename"]).convert("RGB") + results['imgs'] = imgs + return results + + self.data_path = results['data_path'] + results['backend'] = self.backend + + imgs = {} + + results['frame_idxs'] = self.frame_idxs + results['num_scales'] = self.num_scales + + file_name = results['filename'] + folder = results['folder'] + frame_index = results['frame_index'] + line = file_name.split('/') + istrain = folder.split('_')[1] + if 'mode' not in results: + results['mode'] = istrain + results['day_or_night'] = folder.split('_')[0] + + if istrain == "train": + if folder[0] == 'd': + folder2 = folder + '_fake_night' + flag = 0 + else: + folder2 = folder + '_fake_day' + tmp = folder + folder = folder2 + folder2 = tmp + flag = 1 + + if len(line) == 3: + side = line[2] + else: + side = None + + results['side'] = side + + for i in self.frame_idxs: + + if i == "s": + other_side = {"r": "l", "l": "r"}[side] + imgs[("color", i, + -1)] = self.get_color(folder, frame_index, other_side) + imgs[("color_n", i, + -1)] = self.get_color(folder2, frame_index, + other_side) + else: + imgs[("color", i, + -1)] = self.get_color(folder, frame_index + i, side) + imgs[("color_n", i, + -1)] = self.get_color(folder2, frame_index + i, side) + + istrain = folder.split('_')[1] + if istrain != 'train': + if flag: + depth_gt = self.get_depth(folder2, frame_index, side) + else: + depth_gt = self.get_depth(folder, frame_index, side) + imgs["depth_gt"] = np.expand_dims(depth_gt, 0) + elif istrain == 'val': + if len(line) == 3: + side = line[2] + else: + side = None + + for i in self.frame_idxs: + if i == "s": + other_side = {"r": "l", "l": "r"}[side] + imgs[("color", i, + -1)] = self.get_color(folder, frame_index, other_side) + else: + + imgs[("color", i, + -1)] = self.get_color(folder, frame_index + i, side) + + # adjusting intrinsics to match each scale in the pyramid + + depth_gt = self.get_depth(self.dataset, folder, frame_index, side) + imgs["depth_gt"] = np.expand_dims(depth_gt, 0) + results['imgs'] = imgs + + return results diff --git a/paddlevideo/loader/pipelines/decode_sampler.py b/paddlevideo/loader/pipelines/decode_sampler.py new file mode 100644 index 0000000000000000000000000000000000000000..2f8f8743d1a06213e37210a5703b4d0a6b501c75 --- /dev/null +++ b/paddlevideo/loader/pipelines/decode_sampler.py @@ -0,0 +1,93 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import numpy as np +from PIL import Image +import decord as de +from ..registry import PIPELINES + + +@PIPELINES.register() +class DecodeSampler(object): + """ + We use 'decord' for decode and sampling, which is faster than opencv. + This is used in slowfast model. + Args: + num_frames(int): the number of frames we want to sample. + sampling_rate(int): sampling rate for video data. + target_fps(int): desired fps, default 30 + test_mode(bool): whether test or train/valid. In slowfast, we use multicrop when test. + """ + def __init__(self, + num_frames, + sampling_rate, + default_sampling_rate=2, + target_fps=30, + test_mode=False): + self.num_frames = num_frames + self.orig_sampling_rate = self.sampling_rate = sampling_rate + self.default_sampling_rate = default_sampling_rate + self.target_fps = target_fps + self.test_mode = test_mode + + def get_start_end_idx(self, video_size, clip_size, clip_idx, + temporal_num_clips): + delta = max(video_size - clip_size, 0) + if not self.test_mode: + # Random temporal sampling. + start_idx = random.uniform(0, delta) + else: + # Uniformly sample the clip with the given index. + start_idx = delta * clip_idx / temporal_num_clips + end_idx = start_idx + clip_size - 1 + return start_idx, end_idx + + def __call__(self, results): + """ + Perform mp4 decode operations. + return: + List where each item is a numpy array after decoder. + """ + short_cycle_idx = results.get('short_cycle_idx') + if short_cycle_idx: + self.sampling_rate = random.randint(self.default_sampling_rate, + self.orig_sampling_rate) + + filepath = results['filename'] + temporal_sample_index = results['temporal_sample_index'] + temporal_num_clips = results['temporal_num_clips'] + + vr = de.VideoReader(filepath) + videolen = len(vr) + + fps = vr.get_avg_fps() + clip_size = self.num_frames * self.sampling_rate * fps / self.target_fps + + start_idx, end_idx = self.get_start_end_idx(videolen, clip_size, + temporal_sample_index, + temporal_num_clips) + index = np.linspace(start_idx, end_idx, self.num_frames).astype("int64") + index = np.clip(index, 0, videolen) + + frames_select = vr.get_batch(index) #1 for buffer + + # dearray_to_img + np_frames = frames_select.asnumpy() + frames_select_list = [] + for i in range(np_frames.shape[0]): + imgbuf = np_frames[i] + frames_select_list.append(Image.fromarray(imgbuf, mode='RGB')) + results['imgs'] = frames_select_list + return results diff --git a/paddlevideo/loader/pipelines/decode_sampler_MRI.py b/paddlevideo/loader/pipelines/decode_sampler_MRI.py new file mode 100644 index 0000000000000000000000000000000000000000..16f64514e559bb45cf0241e243acb21865845b18 --- /dev/null +++ b/paddlevideo/loader/pipelines/decode_sampler_MRI.py @@ -0,0 +1,224 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import random + +import numpy as np +from PIL import Image +try: + import SimpleITK as sitk +except ImportError as e: + print( + f"{e}, [SimpleITK] package and it's dependencies is required for PP-Care." + ) +import cv2 + +from ..registry import PIPELINES + + +@PIPELINES.register() +class SFMRI_DecodeSampler(object): + """ + Sample frames id. + NOTE: Use PIL to read image here, has diff with CV2 + Args: + num_seg(int): number of segments. + seg_len(int): number of sampled frames in each segment. + valid_mode(bool): True or False. + select_left: Whether to select the frame to the left in the middle when the sampling interval is even in the test mode. + Returns: + frames_idx: the index of sampled #frames. + """ + def __init__(self, + num_seg, + seg_len, + valid_mode=False, + select_left=False, + dense_sample=False, + linspace_sample=False): + self.num_seg = num_seg + self.seg_len = seg_len + self.valid_mode = valid_mode + self.select_left = select_left + self.dense_sample = dense_sample + self.linspace_sample = linspace_sample + + def _get(self, frames_idx_s, frames_idx_f, results): + + frame_dir = results['frame_dir'] + imgs_s = [] + imgs_f = [] + MRI = sitk.GetArrayFromImage(sitk.ReadImage(frame_dir)) + for idx in frames_idx_s: + item = MRI[idx] + item = cv2.resize(item, (224, 224)) + imgs_s.append(item) + + for idx in frames_idx_f: + item = MRI[idx] + item = cv2.resize(item, (224, 224)) + imgs_f.append(item) + + results['imgs'] = [imgs_s, imgs_f] + return results + + def __call__(self, results): + """ + Args: + frames_len: length of frames. + return: + sampling id. + """ + frames_len = int(results['frames_len']) + average_dur1 = int(frames_len / self.num_seg[0]) + average_dur2 = int(frames_len / self.num_seg[1]) + frames_idx_s = [] + frames_idx_f = [] + if self.linspace_sample: + if 'start_idx' in results and 'end_idx' in results: + offsets_s = np.linspace(results['start_idx'], + results['end_idx'], self.num_seg[0]) + offsets_f = np.linspace(results['start_idx'], + results['end_idx'], self.num_seg[1]) + else: + offsets_s = np.linspace(0, frames_len - 1, self.num_seg[0]) + offsets_f = np.linspace(0, frames_len - 1, self.num_seg[1]) + offsets_s = np.clip(offsets_s, 0, frames_len - 1).astype(np.int64) + offsets_f = np.clip(offsets_f, 0, frames_len - 1).astype(np.int64) + + frames_idx_s = list(offsets_s) + frames_idx_f = list(offsets_f) + + return self._get(frames_idx_s, frames_idx_f, results) + + if not self.select_left: + if self.dense_sample: # For ppTSM + if not self.valid_mode: # train + sample_pos = max(1, 1 + frames_len - 64) + t_stride1 = 64 // self.num_seg[0] + t_stride2 = 64 // self.num_seg[1] + start_idx = 0 if sample_pos == 1 else np.random.randint( + 0, sample_pos - 1) + offsets_s = [(idx * t_stride1 + start_idx) % frames_len + 1 + for idx in range(self.num_seg[0])] + offsets_f = [(idx * t_stride2 + start_idx) % frames_len + 1 + for idx in range(self.num_seg[1])] + frames_idx_s = offsets_s + frames_idx_f = offsets_f + else: + sample_pos = max(1, 1 + frames_len - 64) + t_stride1 = 64 // self.num_seg[0] + t_stride2 = 64 // self.num_seg[1] + start_list = np.linspace(0, + sample_pos - 1, + num=10, + dtype=int) + offsets_s = [] + offsets_f = [] + for start_idx in start_list.tolist(): + offsets_s += [ + (idx * t_stride1 + start_idx) % frames_len + 1 + for idx in range(self.num_seg[0]) + ] + for start_idx in start_list.tolist(): + offsets_f += [ + (idx * t_stride2 + start_idx) % frames_len + 1 + for idx in range(self.num_seg[1]) + ] + frames_idx_s = offsets_s + frames_idx_f = offsets_f + else: + for i in range(self.num_seg[0]): + idx = 0 + if not self.valid_mode: + if average_dur1 >= self.seg_len: + idx = random.randint(0, average_dur1 - self.seg_len) + idx += i * average_dur1 + elif average_dur1 >= 1: + idx += i * average_dur1 + else: + idx = i + else: + if average_dur1 >= self.seg_len: + idx = (average_dur1 - 1) // 2 + idx += i * average_dur1 + elif average_dur1 >= 1: + idx += i * average_dur1 + else: + idx = i + for jj in range(idx, idx + self.seg_len): + frames_idx_s.append(jj) + + for i in range(self.num_seg[1]): + idx = 0 + if not self.valid_mode: + if average_dur2 >= self.seg_len: + idx = random.randint(0, average_dur2 - self.seg_len) + idx += i * average_dur2 + elif average_dur2 >= 1: + idx += i * average_dur2 + else: + idx = i + else: + if average_dur2 >= self.seg_len: + idx = (average_dur2 - 1) // 2 + idx += i * average_dur2 + elif average_dur2 >= 1: + idx += i * average_dur2 + else: + idx = i + for jj in range(idx, idx + self.seg_len): + frames_idx_f.append(jj) + + return self._get(frames_idx_s, frames_idx_f, results) + + else: # for TSM + if not self.valid_mode: + if average_dur2 > 0: + offsets_s = np.multiply(list(range( + self.num_seg[0])), average_dur1) + np.random.randint( + average_dur1, size=self.num_seg[0]) + + offsets_f = np.multiply(list(range( + self.num_seg[1])), average_dur2) + np.random.randint( + average_dur2, size=self.num_seg[1]) + elif frames_len > self.num_seg[1]: + offsets_s = np.sort( + np.random.randint(frames_len, size=self.num_seg[0])) + offsets_f = np.sort( + np.random.randint(frames_len, size=self.num_seg[1])) + else: + offsets_s = np.zeros(shape=(self.num_seg[0], )) + offsets_f = np.zeros(shape=(self.num_seg[1], )) + else: + if frames_len > self.num_seg[1]: + average_dur_float_s = frames_len / self.num_seg[0] + offsets_s = np.array([ + int(average_dur_float_s / 2.0 + average_dur_float_s * x) + for x in range(self.num_seg[0]) + ]) + average_dur_float_f = frames_len / self.num_seg[1] + offsets_f = np.array([ + int(average_dur_float_f / 2.0 + average_dur_float_f * x) + for x in range(self.num_seg[1]) + ]) + else: + offsets_s = np.zeros(shape=(self.num_seg[0], )) + offsets_f = np.zeros(shape=(self.num_seg[1], )) + + frames_idx_s = list(offsets_s) + frames_idx_f = list(offsets_f) + + return self._get(frames_idx_s, frames_idx_f, results) diff --git a/paddlevideo/loader/pipelines/mix.py b/paddlevideo/loader/pipelines/mix.py new file mode 100644 index 0000000000000000000000000000000000000000..ccc5f98cf0e6e3ead3848a47ea76c8ceb478f2f0 --- /dev/null +++ b/paddlevideo/loader/pipelines/mix.py @@ -0,0 +1,116 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np + +from ..registry import PIPELINES + + +@PIPELINES.register() +class Mixup(object): + """ + Mixup operator. + Args: + alpha(float): alpha value. + """ + def __init__(self, alpha=0.2): + assert alpha > 0., \ + 'parameter alpha[%f] should > 0.0' % (alpha) + self.alpha = alpha + + def __call__(self, batch): + imgs, labels = list(zip(*batch)) + imgs = np.array(imgs) + labels = np.array(labels) + bs = len(batch) + idx = np.random.permutation(bs) + lam = np.random.beta(self.alpha, self.alpha) + lams = np.array([lam] * bs, dtype=np.float32) + imgs = lam * imgs + (1 - lam) * imgs[idx] + return list(zip(imgs, labels, labels[idx], lams)) + + +@PIPELINES.register() +class Cutmix(object): + """ Cutmix operator + Args: + alpha(float): alpha value. + """ + def __init__(self, alpha=0.2): + assert alpha > 0., \ + 'parameter alpha[%f] should > 0.0' % (alpha) + self.alpha = alpha + + def rand_bbox(self, size, lam): + """ rand_bbox """ + w = size[2] + h = size[3] + cut_rat = np.sqrt(1. - lam) + cut_w = np.int(w * cut_rat) + cut_h = np.int(h * cut_rat) + + # uniform + cx = np.random.randint(w) + cy = np.random.randint(h) + + bbx1 = np.clip(cx - cut_w // 2, 0, w) + bby1 = np.clip(cy - cut_h // 2, 0, h) + bbx2 = np.clip(cx + cut_w // 2, 0, w) + bby2 = np.clip(cy + cut_h // 2, 0, h) + + return bbx1, bby1, bbx2, bby2 + + def __call__(self, batch): + imgs, labels = list(zip(*batch)) + imgs = np.array(imgs) + labels = np.array(labels) + + bs = len(batch) + idx = np.random.permutation(bs) + lam = np.random.beta(self.alpha, self.alpha) + + bbx1, bby1, bbx2, bby2 = self.rand_bbox(imgs.shape, lam) + imgs[:, :, bbx1:bbx2, bby1:bby2] = imgs[idx, :, bbx1:bbx2, bby1:bby2] + lam = 1 - (float(bbx2 - bbx1) * (bby2 - bby1) / + (imgs.shape[-2] * imgs.shape[-1])) + lams = np.array([lam] * bs, dtype=np.float32) + + return list(zip(imgs, labels, labels[idx], lams)) + + +@PIPELINES.register() +class VideoMix(object): + """ + VideoMix operator. + Args: + cutmix_prob(float): prob choose cutmix + mixup_alpha(float): alpha for mixup aug + cutmix_alpha(float): alpha for cutmix aug + """ + def __init__(self, cutmix_prob=0.5, mixup_alpha=0.2, cutmix_alpha=1.0): + assert cutmix_prob > 0., \ + 'parameter cutmix_prob[%f] should > 0.0' % (cutmix_prob) + assert mixup_alpha > 0., \ + 'parameter mixup_alpha[%f] should > 0.0' % (mixup_alpha) + assert cutmix_alpha > 0., \ + 'parameter cutmix_alpha[%f] should > 0.0' % (cutmix_alpha) + self.cutmix_prob = cutmix_prob + self.mixup = Mixup(mixup_alpha) + self.cutmix = Cutmix(cutmix_alpha) + + def __call__(self, batch): + if np.random.random() < self.cutmix_prob: + return self.cutmix(batch) + else: + return self.mixup(batch) diff --git a/paddlevideo/loader/pipelines/multimodal.py b/paddlevideo/loader/pipelines/multimodal.py new file mode 100644 index 0000000000000000000000000000000000000000..f00f68bea297f5bfe49b4d2b5baaaae039dbf79f --- /dev/null +++ b/paddlevideo/loader/pipelines/multimodal.py @@ -0,0 +1,380 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import numpy as np +from PIL import Image +import decord as de +import copy +import json +from ..registry import PIPELINES + +try: + from paddlenlp.transformers import BertTokenizer +except ImportError as e: + print( + f"{e}, [paddlenlp] package and it's dependencies is required for ActBERT." + ) + + +@PIPELINES.register() +class FeaturePadding(object): + """ + Padding feature to target shape. + """ + def __init__(self, max_region_num=36, max_action_num=5): + self.max_region_num = max_region_num + self.max_action_num = max_action_num + + def __call__(self, results): + """ + Padding feature. + """ + pack_feature = results['feature'] + tokenizer = results['tokenizer'] + image_feature_wp, image_target_wp, image_location_wp, \ + num_boxes, image_h, image_w, image_id, caption, \ + action_feature_wp, action_target_wp, num_actions = pack_feature + + image_feature = np.zeros((self.max_region_num, 2048), dtype=np.float32) + image_target = np.zeros((self.max_region_num, 1601), dtype=np.float32) + image_location = np.zeros((self.max_region_num, 5), dtype=np.float32) + + action_feature = np.zeros((self.max_action_num, 2048), dtype=np.float32) + action_target = np.zeros((self.max_action_num, ), dtype=np.int64) + + num_boxes = int(num_boxes) + image_feature[:num_boxes] = image_feature_wp + image_target[:num_boxes] = image_target_wp + image_location[:num_boxes, :4] = image_location_wp + + image_location[:, 4] = (image_location[:, 3] - image_location[:, 1]) * ( + image_location[:, 2] - image_location[:, 0]) / (float(image_w) * + float(image_h)) + + image_location[:, 0] = image_location[:, 0] / float(image_w) + image_location[:, 1] = image_location[:, 1] / float(image_h) + image_location[:, 2] = image_location[:, 2] / float(image_w) + image_location[:, 3] = image_location[:, 3] / float(image_h) + + image_feature = copy.deepcopy(image_feature) + image_target = copy.deepcopy(image_target) + + num_actions = int(num_actions) + action_feature[:num_actions] = action_feature_wp + action_target[:num_actions] = action_target_wp + action_feature = copy.deepcopy(action_feature) + action_target = copy.deepcopy(action_target) + + results = dict(image_feat=image_feature, + image_target=image_target, + caption=caption, + image_loc=image_location, + num_boxes=int(num_boxes), + action_feat=action_feature, + action_target=action_target, + num_actions=int(num_actions), + tokenizer=tokenizer) + return results + + +@PIPELINES.register() +class RandomCap(object): + def __init__(self, caption_path): + """ + Random Caption for NSP task + """ + self.caption_path = caption_path + + def select_caption(self, caption): + captions = caption.split('!') + rind = random.randint(0, len(captions) - 1) + caption = captions[rind] + return caption + + def get_random_caption(self, all_captions): + num_caps = len(all_captions) + rand_doc_idx = random.randint(0, num_caps - 1) + caption = all_captions[rand_doc_idx] + caption = self.select_caption(caption) + return caption + + def random_cap(self, caption, all_captions): + if random.random() > 0.5: + label = 0 + else: + caption = self.get_random_caption(all_captions) + label = 1 + return caption, label + + def __call__(self, results): + caption = results['caption'] + all_captions = list(json.load(open(self.caption_path, 'r'))) + caption = self.select_caption(caption) + caption, label = self.random_cap(caption, all_captions) + results['caption'] = caption + results['is_next'] = label + return results + + +@PIPELINES.register() +class Tokenize(object): + def __init__(self, ): + """ + Tokenize caption + """ + pass + + def __call__(self, results): + caption = results['caption'] + tokenizer = results['tokenizer'] + tokens_caption = tokenizer.tokenize(caption) + results['caption'] = tokens_caption + return results + + +@PIPELINES.register() +class RandomMask(object): + def __init__(self, + max_seq_length=36, + max_action_length=5, + max_region_length=36): + self.max_seq_length = max_seq_length + self.max_action_length = max_action_length + self.max_region_length = max_region_length + + def get_image_global_feature(self, image_feat, image_loc, image_mask): + g_image_feat = np.sum(image_feat, axis=0) / np.sum( + image_mask, axis=0, keepdims=True) + image_feat = np.concatenate( + [np.expand_dims(g_image_feat, axis=0), image_feat], + axis=0).astype("float32") + + g_image_loc = np.array([0, 0, 1, 1, 1]).astype("float32") + image_loc = np.concatenate( + [np.expand_dims(g_image_loc, axis=0), image_loc], axis=0) + + g_image_mask = np.array([1]) + image_mask = np.concatenate([g_image_mask, image_mask], axis=0) + + return image_feat, image_loc, image_mask + + def _truncate_seq_pair(self, tokens_b, max_length): + """Truncates a sequence pair in place to the maximum length. + This is a simple heuristic which will always truncate the longer sequence + one token at a time. This makes more sense than truncating an equal percent + of tokens from each, since if one sequence is very short then each token + that's truncated likely contains more information than a longer sequence. + """ + while True: + total_length = len(tokens_b) + if total_length <= max_length: + break + tokens_b.pop() + + def random_word(self, tokens, tokenizer): + """ + Masking some random tokens for Language Model task with probabilities as in the original BERT paper. + Args: + tokens: list of str, tokenized sentence. + tokenizer: Tokenizer, object used for tokenization (we need it's vocab here) + Return: + (list of str, list of int), masked tokens and related labels for LM prediction + """ + output_label = [] + + for i, token in enumerate(tokens): + prob = random.random() + # mask token with 15% probability + + if prob < 0.15: + prob /= 0.15 + + # 80% randomly change token to mask token + if prob < 0.8: + tokens[i] = "[MASK]" + + # 10% randomly change token to random token + elif prob < 0.9: + #tok = random.choice(list(tokenizer.vocab.items()))[0] + tok = tokenizer.vocab.idx_to_token[random.randint( + 0, + tokenizer.vocab_size, + )] + tokens[i] = tok + + # rest 10% randomly keep current token + # append current token to output (we will predict these later) + try: + output_label.append(tokenizer.vocab[token]) + except KeyError: + # For unknown words (should not occur with BPE vocab) + output_label.append(tokenizer.vocab["[UNK]"]) + print( + "Cannot find token '{}' in vocab. Using [UNK] insetad". + format(token)) + else: + # no masking token (will be ignored by loss function later) + output_label.append(-1) + + return tokens, output_label + + def random_region(self, image_feat, image_loc, num_boxes): + output_label = [] + + for i in range(num_boxes): + prob = random.random() + # mask token with 15% probability + if prob < 0.15: + prob /= 0.15 + + # 80% randomly change token to mask token + if prob < 0.9: + image_feat[i] = 0 + + # rest 20% randomly keep current token + # append current token to output (we will predict these later) + output_label.append(1) + else: + # no masking token (will be ignored by loss function later) + output_label.append(-1) + + return image_feat, image_loc, output_label + + def random_action(self, action_feat, action_target, num_actions): + output_label = [] + + for i in range(num_actions): + prob = random.random() + # mask token with 15% probability + if prob < 0.15: + prob /= 0.15 + + # 90% randomly change token to mask token + if prob < 0.9: + action_feat[i] = 0 + + # rest 10% randomly keep current token + # append current token to output (we will predict these later) + output_label.append(action_target[i]) + else: + # no masking token (will be ignored by loss function later) + output_label.append(-1) + + return action_feat, output_label + + def __call__(self, results): + caption = results['caption'] + tokenizer = results['tokenizer'] + image_feat = results['image_feat'] + image_loc = results['image_loc'] + num_boxes = results['num_boxes'] + action_feat = results['action_feat'] + action_target = results['action_target'] + num_actions = results['num_actions'] + is_next = results['is_next'] + image_target = results['image_target'] + + self._truncate_seq_pair(caption, self.max_seq_length - 2) + caption, caption_label = self.random_word(caption, tokenizer) + + image_feat, image_loc, image_label = self.random_region( + image_feat, image_loc, num_boxes) + action_feat, action_label = self.random_action(action_feat, + action_target, + num_actions) + + # concatenate lm labels and account for CLS, SEP, SEP + lm_label_ids = [-1] + caption_label + [-1] + + # The convention in BERT is: + # (a) For sequence pairs: + # tokens: [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP] + # type_ids: 0 0 0 0 0 0 0 0 1 1 1 1 1 1 + # (b) For single sequences: + # tokens: [CLS] the dog is hairy . [SEP] + # type_ids: 0 0 0 0 0 0 0 + # + # Where "type_ids" are used to indicate whether this is the first + # sequence or the second sequence. The embedding vectors for `type=0` and + # `type=1` were learned during pre-training and are added to the wordpiece + # embedding vector (and position vector). This is not *strictly* necessary + # since the [SEP] token unambigiously separates the sequences, but it makes + # it easier for the model to learn the concept of sequences. + # + # For classification tasks, the first vector (corresponding to [CLS]) is + # used as as the "sentence vector". Note that this only makes sense because + # the entire model is fine-tuned. + + tokens = [] + segment_ids = [] + + tokens.append("[CLS]") + segment_ids.append(0) + + for token in caption: + tokens.append(token) + segment_ids.append(0) + tokens.append("[SEP]") + segment_ids.append(0) + input_ids = tokenizer.convert_tokens_to_ids(tokens) + + # The mask has 1 for real tokens and 0 for padding tokens. Only real tokens are attended to. + input_mask = [1] * (len(input_ids)) + image_mask = [1] * (num_boxes) + action_mask = [1] * (num_actions) + + # Zero-pad up to the visual sequence length. + while len(image_mask) < self.max_region_length: + image_mask.append(0) + image_label.append(-1) + while len(action_mask) < self.max_action_length: + action_mask.append(0) + action_label.append(-1) + + # Zero-pad up to the sequence length. + while len(input_ids) < self.max_seq_length: + input_ids.append(0) + input_mask.append(0) + segment_ids.append(0) + lm_label_ids.append(-1) + + assert len(input_ids) == self.max_seq_length + assert len(input_mask) == self.max_seq_length + assert len(segment_ids) == self.max_seq_length + assert len(lm_label_ids) == self.max_seq_length + assert len(image_mask) == self.max_region_length + assert len(image_label) == self.max_region_length + assert len(action_mask) == self.max_action_length + assert len(action_label) == self.max_action_length + + image_feat, image_loc, image_mask = self.get_image_global_feature( + image_feat, image_loc, np.array(image_mask)) + features = [ + np.array(input_ids), + action_feat, + image_feat, + image_loc, + np.array(segment_ids), + np.array(input_mask), + image_mask, + np.array(action_mask), + np.array(lm_label_ids), + np.array(action_label), + np.array(is_next), + np.array(image_label), + image_target, + ] + results['features'] = features + return results diff --git a/paddlevideo/loader/pipelines/sample.py b/paddlevideo/loader/pipelines/sample.py new file mode 100644 index 0000000000000000000000000000000000000000..9780ae333760d16830c7a8675826d32a6baaef02 --- /dev/null +++ b/paddlevideo/loader/pipelines/sample.py @@ -0,0 +1,380 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import random + +import numpy as np +from PIL import Image +try: + import SimpleITK as sitk +except ImportError as e: + print( + f"{e}, [SimpleITK] package and it's dependencies is required for PP-Care." + ) +import cv2 + +from ..registry import PIPELINES + +try: + import cPickle as pickle + from cStringIO import StringIO +except ImportError: + import pickle + from io import BytesIO + + +@PIPELINES.register() +class Sampler(object): + """ + Sample frames id. + NOTE: Use PIL to read image here, has diff with CV2 + Args: + num_seg(int): number of segments. + seg_len(int): number of sampled frames in each segment. + valid_mode(bool): True or False. + select_left: Whether to select the frame to the left in the middle when the sampling interval is even in the test mode. + Returns: + frames_idx: the index of sampled #frames. + """ + def __init__(self, + num_seg, + seg_len, + frame_interval=None, + valid_mode=False, + select_left=False, + dense_sample=False, + linspace_sample=False, + use_pil=True): + self.num_seg = num_seg + self.seg_len = seg_len + self.frame_interval = frame_interval + self.valid_mode = valid_mode + self.select_left = select_left + self.dense_sample = dense_sample + self.linspace_sample = linspace_sample + self.use_pil = use_pil + + def _get(self, frames_idx, results): + data_format = results['format'] + + if data_format == "frame": + frame_dir = results['frame_dir'] + imgs = [] + for idx in frames_idx: + img = Image.open( + os.path.join(frame_dir, + results['suffix'].format(idx))).convert('RGB') + imgs.append(img) + + elif data_format == "MRI": + frame_dir = results['frame_dir'] + imgs = [] + MRI = sitk.GetArrayFromImage(sitk.ReadImage(frame_dir)) + for idx in frames_idx: + item = MRI[idx] + item = cv2.resize(item, (224, 224)) + imgs.append(item) + + elif data_format == "video": + if results['backend'] == 'cv2': + frames = np.array(results['frames']) + imgs = [] + for idx in frames_idx: + imgbuf = frames[idx] + img = Image.fromarray(imgbuf, mode='RGB') + imgs.append(img) + elif results['backend'] == 'decord': + container = results['frames'] + if self.use_pil: + frames_select = container.get_batch(frames_idx) + # dearray_to_img + np_frames = frames_select.asnumpy() + imgs = [] + for i in range(np_frames.shape[0]): + imgbuf = np_frames[i] + imgs.append(Image.fromarray(imgbuf, mode='RGB')) + else: + if frames_idx.ndim != 1: + frames_idx = np.squeeze(frames_idx) + frame_dict = { + idx: container[idx].asnumpy() + for idx in np.unique(frames_idx) + } + imgs = [frame_dict[idx] for idx in frames_idx] + elif results['backend'] == 'pyav': + imgs = [] + frames = np.array(results['frames']) + for idx in frames_idx: + imgbuf = frames[idx] + imgs.append(imgbuf) + imgs = np.stack(imgs) # thwc + else: + raise NotImplementedError + else: + raise NotImplementedError + results['imgs'] = imgs + return results + + def _get_train_clips(self, num_frames): + ori_seg_len = self.seg_len * self.frame_interval + avg_interval = (num_frames - ori_seg_len + 1) // self.num_seg + + if avg_interval > 0: + base_offsets = np.arange(self.num_seg) * avg_interval + clip_offsets = base_offsets + np.random.randint(avg_interval, + size=self.num_seg) + elif num_frames > max(self.num_seg, ori_seg_len): + clip_offsets = np.sort( + np.random.randint(num_frames - ori_seg_len + 1, + size=self.num_seg)) + elif avg_interval == 0: + ratio = (num_frames - ori_seg_len + 1.0) / self.num_seg + clip_offsets = np.around(np.arange(self.num_seg) * ratio) + else: + clip_offsets = np.zeros((self.num_seg, ), dtype=np.int) + return clip_offsets + + def _get_test_clips(self, num_frames): + ori_seg_len = self.seg_len * self.frame_interval + avg_interval = (num_frames - ori_seg_len + 1) / float(self.num_seg) + if num_frames > ori_seg_len - 1: + base_offsets = np.arange(self.num_seg) * avg_interval + clip_offsets = (base_offsets + avg_interval / 2.0).astype(np.int) + else: + clip_offsets = np.zeros((self.num_seg, ), dtype=np.int) + return clip_offsets + + def __call__(self, results): + """ + Args: + frames_len: length of frames. + return: + sampling id. + """ + frames_len = int(results['frames_len']) + frames_idx = [] + if self.frame_interval is not None: + assert isinstance(self.frame_interval, int) + if not self.valid_mode: + offsets = self._get_train_clips(frames_len) + else: + offsets = self._get_test_clips(frames_len) + + offsets = offsets[:, None] + np.arange( + self.seg_len)[None, :] * self.frame_interval + offsets = np.concatenate(offsets) + + offsets = offsets.reshape((-1, self.seg_len)) + offsets = np.mod(offsets, frames_len) + offsets = np.concatenate(offsets) + + if results['format'] == 'video': + frames_idx = offsets + elif results['format'] == 'frame': + frames_idx = list(offsets + 1) + else: + raise NotImplementedError + + return self._get(frames_idx, results) + + if self.linspace_sample: + if 'start_idx' in results and 'end_idx' in results: + offsets = np.linspace(results['start_idx'], results['end_idx'], + self.num_seg) + else: + offsets = np.linspace(0, frames_len - 1, self.num_seg) + offsets = np.clip(offsets, 0, frames_len - 1).astype(np.int64) + if results['format'] == 'video': + frames_idx = list(offsets) + frames_idx = [x % frames_len for x in frames_idx] + elif results['format'] == 'frame': + frames_idx = list(offsets + 1) + + elif results['format'] == 'MRI': + frames_idx = list(offsets) + + else: + raise NotImplementedError + return self._get(frames_idx, results) + + average_dur = int(frames_len / self.num_seg) + if not self.select_left: + if self.dense_sample: # For ppTSM + if not self.valid_mode: # train + sample_pos = max(1, 1 + frames_len - 64) + t_stride = 64 // self.num_seg + start_idx = 0 if sample_pos == 1 else np.random.randint( + 0, sample_pos - 1) + offsets = [(idx * t_stride + start_idx) % frames_len + 1 + for idx in range(self.num_seg)] + frames_idx = offsets + else: + sample_pos = max(1, 1 + frames_len - 64) + t_stride = 64 // self.num_seg + start_list = np.linspace(0, + sample_pos - 1, + num=10, + dtype=int) + offsets = [] + for start_idx in start_list.tolist(): + offsets += [ + (idx * t_stride + start_idx) % frames_len + 1 + for idx in range(self.num_seg) + ] + frames_idx = offsets + else: + for i in range(self.num_seg): + idx = 0 + if not self.valid_mode: + if average_dur >= self.seg_len: + idx = random.randint(0, average_dur - self.seg_len) + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + else: + if average_dur >= self.seg_len: + idx = (average_dur - 1) // 2 + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + for jj in range(idx, idx + self.seg_len): + if results['format'] == 'video': + frames_idx.append(int(jj % frames_len)) + elif results['format'] == 'frame': + frames_idx.append(jj + 1) + + elif results['format'] == 'MRI': + frames_idx.append(jj) + else: + raise NotImplementedError + return self._get(frames_idx, results) + + else: # for TSM + if not self.valid_mode: + if average_dur > 0: + offsets = np.multiply(list(range(self.num_seg)), + average_dur) + np.random.randint( + average_dur, size=self.num_seg) + elif frames_len > self.num_seg: + offsets = np.sort( + np.random.randint(frames_len, size=self.num_seg)) + else: + offsets = np.zeros(shape=(self.num_seg, )) + else: + if frames_len > self.num_seg: + average_dur_float = frames_len / self.num_seg + offsets = np.array([ + int(average_dur_float / 2.0 + average_dur_float * x) + for x in range(self.num_seg) + ]) + else: + offsets = np.zeros(shape=(self.num_seg, )) + + if results['format'] == 'video': + frames_idx = list(offsets) + frames_idx = [x % frames_len for x in frames_idx] + elif results['format'] == 'frame': + frames_idx = list(offsets + 1) + + elif results['format'] == 'MRI': + frames_idx = list(offsets) + + else: + raise NotImplementedError + + return self._get(frames_idx, results) + + +@PIPELINES.register() +class SamplerPkl(object): + """ + Sample frames id. + NOTE: Use PIL to read image here, has diff with CV2 + Args: + num_seg(int): number of segments. + seg_len(int): number of sampled frames in each segment. + mode(str): 'train', 'valid' + Returns: + frames_idx: the index of sampled #frames. + """ + def __init__(self, num_seg, seg_len, backend='pillow', valid_mode=False): + self.num_seg = num_seg + self.seg_len = seg_len + self.valid_mode = valid_mode + self.backend = backend + + def _get(self, buf): + if isinstance(buf, str): + img = Image.open(StringIO(buf)) + else: + img = Image.open(BytesIO(buf)) + img = img.convert('RGB') + if self.backend != 'pillow': + img = np.array(img) + return img + + def __call__(self, results): + """ + Args: + frames_len: length of frames. + return: + sampling id. + """ + filename = results['frame_dir'] + data_loaded = pickle.load(open(filename, 'rb'), encoding='bytes') + video_name, label, frames = data_loaded + if isinstance(label, dict): + label = label['动作类型'] + results['labels'] = label + elif len(label) == 1: + results['labels'] = int(label[0]) + else: + results['labels'] = int(label[0]) if random.random() < 0.5 else int( + label[1]) + results['frames_len'] = len(frames) + frames_len = results['frames_len'] + average_dur = int(int(frames_len) / self.num_seg) + imgs = [] + for i in range(self.num_seg): + idx = 0 + if not self.valid_mode: + if average_dur >= self.seg_len: + idx = random.randint(0, average_dur - self.seg_len) + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + else: + if average_dur >= self.seg_len: + idx = (average_dur - 1) // 2 + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + + for jj in range(idx, idx + self.seg_len): + imgbuf = frames[int(jj % results['frames_len'])] + img = self._get(imgbuf) + imgs.append(img) + results['backend'] = self.backend + results['imgs'] = imgs + + return results diff --git a/paddlevideo/loader/pipelines/sample_ava.py b/paddlevideo/loader/pipelines/sample_ava.py new file mode 100644 index 0000000000000000000000000000000000000000..39e90a2166531a6db2a62c5bb33bd3d4ab1b3914 --- /dev/null +++ b/paddlevideo/loader/pipelines/sample_ava.py @@ -0,0 +1,375 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import random +from PIL import Image +from ..registry import PIPELINES +import os +import numpy as np +import io +import os.path as osp +from abc import ABCMeta, abstractmethod +import cv2 +from cv2 import IMREAD_COLOR, IMREAD_GRAYSCALE, IMREAD_UNCHANGED +import inspect + +imread_backend = 'cv2' +imread_flags = { + 'color': IMREAD_COLOR, + 'grayscale': IMREAD_GRAYSCALE, + 'unchanged': IMREAD_UNCHANGED +} + + +@PIPELINES.register() +class SampleFrames: + """Sample frames from the video. """ + + def __init__(self, + clip_len, + frame_interval=1, + num_clips=1, + temporal_jitter=False, + twice_sample=False, + out_of_bound_opt='loop', + test_mode=False): + self.clip_len = clip_len + self.frame_interval = frame_interval + self.num_clips = num_clips + self.temporal_jitter = temporal_jitter + self.twice_sample = twice_sample + self.out_of_bound_opt = out_of_bound_opt + self.test_mode = test_mode + assert self.out_of_bound_opt in ['loop', 'repeat_last'] + + def _get_train_clips(self, num_frames): + """Get clip offsets in train mode. """ + ori_clip_len = self.clip_len * self.frame_interval + avg_interval = (num_frames - ori_clip_len + 1) // self.num_clips + if avg_interval > 0: + base_offsets = np.arange(self.num_clips) * avg_interval + clip_offsets = base_offsets + np.random.randint( + avg_interval, size=self.num_clips) + elif num_frames > max(self.num_clips, ori_clip_len): + clip_offsets = np.sort( + np.random.randint( + num_frames - ori_clip_len + 1, size=self.num_clips)) + elif avg_interval == 0: + ratio = (num_frames - ori_clip_len + 1.0) / self.num_clips + clip_offsets = np.around(np.arange(self.num_clips) * ratio) + else: + clip_offsets = np.zeros((self.num_clips, ), dtype=np.int) + return clip_offsets + + def _get_test_clips(self, num_frames): + """Get clip offsets in test mode. """ + ori_clip_len = self.clip_len * self.frame_interval + avg_interval = (num_frames - ori_clip_len + 1) / float(self.num_clips) + if num_frames > ori_clip_len - 1: + base_offsets = np.arange(self.num_clips) * avg_interval + clip_offsets = (base_offsets + avg_interval / 2.0).astype(np.int) + if self.twice_sample: + clip_offsets = np.concatenate([clip_offsets, base_offsets]) + else: + clip_offsets = np.zeros((self.num_clips, ), dtype=np.int) + return clip_offsets + + def _sample_clips(self, num_frames): + """Choose clip offsets for the video in a given mode. """ + if self.test_mode: + clip_offsets = self._get_test_clips(num_frames) + else: + clip_offsets = self._get_train_clips(num_frames) + return clip_offsets + + def __call__(self, results): + """Perform the SampleFrames loading. """ + total_frames = results['total_frames'] + clip_offsets = self._sample_clips(total_frames) + frame_inds = clip_offsets[:, None] + np.arange( + self.clip_len)[None, :] * self.frame_interval + frame_inds = np.concatenate(frame_inds) + if self.temporal_jitter: + perframe_offsets = np.random.randint( + self.frame_interval, size=len(frame_inds)) + frame_inds += perframe_offsets + frame_inds = frame_inds.reshape((-1, self.clip_len)) + if self.out_of_bound_opt == 'loop': + frame_inds = np.mod(frame_inds, total_frames) + elif self.out_of_bound_opt == 'repeat_last': + safe_inds = frame_inds < total_frames + unsafe_inds = 1 - safe_inds + last_ind = np.max(safe_inds * frame_inds, axis=1) + new_inds = (safe_inds * frame_inds + (unsafe_inds.T * last_ind).T) + frame_inds = new_inds + else: + raise ValueError('Illegal out_of_bound option.') + start_index = results['start_index'] + frame_inds = np.concatenate(frame_inds) + start_index + results['frame_inds'] = frame_inds.astype(np.int) + results['clip_len'] = self.clip_len + results['frame_interval'] = self.frame_interval + results['num_clips'] = self.num_clips + return results + + def __repr__(self): + repr_str = (f'{self.__class__.__name__}(' + f'clip_len={self.clip_len}, ' + f'frame_interval={self.frame_interval}, ' + f'num_clips={self.num_clips}, ' + f'temporal_jitter={self.temporal_jitter}, ' + f'twice_sample={self.twice_sample}, ' + f'out_of_bound_opt={self.out_of_bound_opt}, ' + f'test_mode={self.test_mode})') + return repr_str + +class BaseStorageBackend(metaclass=ABCMeta): + """Abstract class of storage backends. """ + + @abstractmethod + def get(self, filepath): + pass + + @abstractmethod + def get_text(self, filepath): + pass + +class HardDiskBackend(BaseStorageBackend): + """Raw hard disks storage backend.""" + + def get(self, filepath): + filepath = str(filepath) + with open(filepath, 'rb') as f: + value_buf = f.read() + return value_buf + + def get_text(self, filepath): + filepath = str(filepath) + with open(filepath, 'r') as f: + value_buf = f.read() + return value_buf + +class FileClient: + """A general file client to access files in different backend. """ + + _backends = { + 'disk': HardDiskBackend, + } + + def __init__(self, backend='disk', **kwargs): + if backend not in self._backends: + raise ValueError( + f'Backend {backend} is not supported. Currently supported ones' + f' are {list(self._backends.keys())}') + self.backend = backend + self.client = self._backends[backend](**kwargs) + + @classmethod + def _register_backend(cls, name, backend, force=False): + if not isinstance(name, str): + raise TypeError('the backend name should be a string, ' + f'but got {type(name)}') + if not inspect.isclass(backend): + raise TypeError( + f'backend should be a class but got {type(backend)}') + if not issubclass(backend, BaseStorageBackend): + raise TypeError( + f'backend {backend} is not a subclass of BaseStorageBackend') + if not force and name in cls._backends: + raise KeyError( + f'{name} is already registered as a storage backend, ' + 'add "force=True" if you want to override it') + + cls._backends[name] = backend + + @classmethod + def register_backend(cls, name, backend=None, force=False): + """Register a backend to FileClient. """ + + if backend is not None: + cls._register_backend(name, backend, force=force) + return + + def _register(backend_cls): + cls._register_backend(name, backend_cls, force=force) + return backend_cls + + return _register + + def get(self, filepath): + return self.client.get(filepath) + + def get_text(self, filepath): + return self.client.get_text(filepath) + +@PIPELINES.register() +class RawFrameDecode: + """Load and decode frames with given indices. """ + + def __init__(self, io_backend='disk', decoding_backend='cv2', **kwargs): + self.io_backend = io_backend + self.decoding_backend = decoding_backend + self.kwargs = kwargs + self.file_client = None + + def _pillow2array(self,img, flag='color', channel_order='bgr'): + """Convert a pillow image to numpy array. """ + + channel_order = channel_order.lower() + if channel_order not in ['rgb', 'bgr']: + raise ValueError('channel order must be either "rgb" or "bgr"') + + if flag == 'unchanged': + array = np.array(img) + if array.ndim >= 3 and array.shape[2] >= 3: # color image + array[:, :, :3] = array[:, :, (2, 1, 0)] # RGB to BGR + else: + # If the image mode is not 'RGB', convert it to 'RGB' first. + if img.mode != 'RGB': + if img.mode != 'LA': + # Most formats except 'LA' can be directly converted to RGB + img = img.convert('RGB') + else: + # When the mode is 'LA', the default conversion will fill in + # the canvas with black, which sometimes shadows black objects + # in the foreground. + # + # Therefore, a random color (124, 117, 104) is used for canvas + img_rgba = img.convert('RGBA') + img = Image.new('RGB', img_rgba.size, (124, 117, 104)) + img.paste(img_rgba, mask=img_rgba.split()[3]) # 3 is alpha + if flag == 'color': + array = np.array(img) + if channel_order != 'rgb': + array = array[:, :, ::-1] # RGB to BGR + elif flag == 'grayscale': + img = img.convert('L') + array = np.array(img) + else: + raise ValueError( + 'flag must be "color", "grayscale" or "unchanged", ' + f'but got {flag}') + return array + + def _imfrombytes(self,content, flag='color', channel_order='bgr'):#, backend=None): + """Read an image from bytes. """ + + img_np = np.frombuffer(content, np.uint8) + flag = imread_flags[flag] if isinstance(flag, str) else flag + img = cv2.imdecode(img_np, flag) + if flag == IMREAD_COLOR and channel_order == 'rgb': + cv2.cvtColor(img, cv2.COLOR_BGR2RGB, img) + return img + + def __call__(self, results): + """Perform the ``RawFrameDecode`` to pick frames given indices. + + Args: + results (dict): The resulting dict to be modified and passed + to the next transform in pipeline. + """ + # mmcv.use_backend(self.decoding_backend) + + directory = results['frame_dir'] + suffix = results['suffix'] + #modality = results['modality'] + + if self.file_client is None: + self.file_client = FileClient(self.io_backend, **self.kwargs) + + imgs = list() + + if results['frame_inds'].ndim != 1: + results['frame_inds'] = np.squeeze(results['frame_inds']) + + offset = results.get('offset', 0) + + for frame_idx in results['frame_inds']: + frame_idx += offset + filepath = osp.join(directory, suffix.format(frame_idx)) + img_bytes = self.file_client.get(filepath) #以二进制方式读取图片 + # Get frame with channel order RGB directly. + + cur_frame = self._imfrombytes(img_bytes, channel_order='rgb') + imgs.append(cur_frame) + + results['imgs'] = imgs + results['original_shape'] = imgs[0].shape[:2] + results['img_shape'] = imgs[0].shape[:2] + + # we resize the gt_bboxes and proposals to their real scale + h, w = results['img_shape'] + scale_factor = np.array([w, h, w, h]) + if 'gt_bboxes' in results: + gt_bboxes = results['gt_bboxes'] + gt_bboxes_new = (gt_bboxes * scale_factor).astype(np.float32) + results['gt_bboxes'] = gt_bboxes_new + if 'proposals' in results and results['proposals'] is not None: + proposals = results['proposals'] + proposals = (proposals * scale_factor).astype(np.float32) + results['proposals'] = proposals + return results + + def __repr__(self): + repr_str = (f'{self.__class__.__name__}(' + f'io_backend={self.io_backend}, ' + f'decoding_backend={self.decoding_backend})') + return repr_str + +@PIPELINES.register() +class SampleAVAFrames(SampleFrames): + + def __init__(self, clip_len, frame_interval=2, test_mode=False): + + super().__init__(clip_len, frame_interval, test_mode=test_mode) + + def _get_clips(self, center_index, skip_offsets, shot_info): + start = center_index - (self.clip_len // 2) * self.frame_interval + end = center_index + ((self.clip_len + 1) // 2) * self.frame_interval + frame_inds = list(range(start, end, self.frame_interval)) + frame_inds = frame_inds + skip_offsets + frame_inds = np.clip(frame_inds, shot_info[0], shot_info[1] - 1) + + return frame_inds + + def __call__(self, results): + fps = results['fps'] + timestamp = results['timestamp'] + timestamp_start = results['timestamp_start'] + shot_info = results['shot_info'] + + #delta=(timestamp - timestamp_start) 为该帧距离15min视频开头有几秒 + #center_index=fps*delta为该帧距离15min视频开头有几帧 + #center_index+1是为了避免后续采样时出现负数? + #后续需要以center_index为中心前后采样视频帧片段 + center_index = fps * (timestamp - timestamp_start) + 1 + + skip_offsets = np.random.randint( + -self.frame_interval // 2, (self.frame_interval + 1) // 2, + size=self.clip_len) + frame_inds = self._get_clips(center_index, skip_offsets, shot_info) + + results['frame_inds'] = np.array(frame_inds, dtype=np.int) + results['clip_len'] = self.clip_len + results['frame_interval'] = self.frame_interval + results['num_clips'] = 1 + results['crop_quadruple'] = np.array([0, 0, 1, 1], dtype=np.float32) + return results + + def __repr__(self): + repr_str = (f'{self.__class__.__name__}(' + f'clip_len={self.clip_len}, ' + f'frame_interval={self.frame_interval}, ' + f'test_mode={self.test_mode})') + return repr_str + diff --git a/paddlevideo/loader/pipelines/segmentation.py b/paddlevideo/loader/pipelines/segmentation.py new file mode 100644 index 0000000000000000000000000000000000000000..247144267cc3a246754d8f4568bea346b485f116 --- /dev/null +++ b/paddlevideo/loader/pipelines/segmentation.py @@ -0,0 +1,130 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from PIL import Image +import copy +import cv2 +from ..registry import PIPELINES + + +@PIPELINES.register() +class MultiRestrictSize(object): + def __init__(self, + min_size=None, + max_size=800, + flip=False, + multi_scale=[1.3]): + self.min_size = min_size + self.max_size = max_size + self.multi_scale = multi_scale + self.flip = flip + assert ((min_size is None)) or ((max_size is None)) + + def __call__(self, sample): + samples = [] + image = sample['current_img'] + h, w = image.shape[:2] + for scale in self.multi_scale: + # Fixed range of scales + sc = None + # Align short edge + if not (self.min_size is None): + if h > w: + short_edge = w + else: + short_edge = h + if short_edge > self.min_size: + sc = float(self.min_size) / short_edge + else: + if h > w: + long_edge = h + else: + long_edge = w + if long_edge > self.max_size: + sc = float(self.max_size) / long_edge + + if sc is None: + new_h = h + new_w = w + else: + new_h = sc * h + new_w = sc * w + new_h = int(new_h * scale) + new_w = int(new_w * scale) + + if (new_h - 1) % 16 != 0: + new_h = int(np.around((new_h - 1) / 16.) * 16 + 1) + if (new_w - 1) % 16 != 0: + new_w = int(np.around((new_w - 1) / 16.) * 16 + 1) + + if new_h == h and new_w == w: + samples.append(sample) + else: + new_sample = {} + for elem in sample.keys(): + if 'meta' in elem: + new_sample[elem] = sample[elem] + continue + tmp = sample[elem] + if 'label' in elem: + new_sample[elem] = sample[elem] + continue + else: + flagval = cv2.INTER_CUBIC + tmp = cv2.resize(tmp, + dsize=(new_w, new_h), + interpolation=flagval) + new_sample[elem] = tmp + samples.append(new_sample) + + if self.flip: + now_sample = samples[-1] + new_sample = {} + for elem in now_sample.keys(): + if 'meta' in elem: + new_sample[elem] = now_sample[elem].copy() + new_sample[elem]['flip'] = True + continue + tmp = now_sample[elem] + tmp = tmp[:, ::-1].copy() + new_sample[elem] = tmp + samples.append(new_sample) + + return samples + + +@PIPELINES.register() +class MultiNorm(object): + def __call__(self, samples): + for idx in range(len(samples)): + sample = samples[idx] + for elem in sample.keys(): + if 'meta' in elem: + continue + tmp = sample[elem] + if tmp is None: + continue + + if tmp.ndim == 2: + tmp = tmp[:, :, np.newaxis] + else: + tmp = tmp / 255. + tmp -= (0.485, 0.456, 0.406) + tmp /= (0.229, 0.224, 0.225) + + tmp = tmp.transpose((2, 0, 1)) + samples[idx][elem] = tmp + + return samples diff --git a/paddlevideo/loader/pipelines/segmentation_pipline.py b/paddlevideo/loader/pipelines/segmentation_pipline.py new file mode 100644 index 0000000000000000000000000000000000000000..dda6deec4043e8caa117ddc757c90808f329e5e2 --- /dev/null +++ b/paddlevideo/loader/pipelines/segmentation_pipline.py @@ -0,0 +1,40 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import copy + +import os +import numpy as np +import random +import paddle +from ..registry import PIPELINES +""" +pipeline ops for Action Segmentation Dataset. +""" + + +@PIPELINES.register() +class SegmentationSampler(object): + + def __init__(self, sample_rate): + self.sample_rate = sample_rate + + def __call__(self, results): + for key, data in results.items(): + if len(data.shape) == 1: + data = data[::self.sample_rate] + results[key] = copy.deepcopy(data) + else: + data = data[:, ::self.sample_rate] + results[key] = copy.deepcopy(data) + return results diff --git a/paddlevideo/loader/pipelines/skeleton_pipeline.py b/paddlevideo/loader/pipelines/skeleton_pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..105a3eef4db45e373b6b5fd9dabbe9dddc48256e --- /dev/null +++ b/paddlevideo/loader/pipelines/skeleton_pipeline.py @@ -0,0 +1,280 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import numpy as np +import paddle.nn.functional as F +import random +import paddle +from ..registry import PIPELINES +"""pipeline ops for Activity Net. +""" + + +@PIPELINES.register() +class AutoPadding(object): + """ + Sample or Padding frame skeleton feature. + Args: + window_size: int, temporal size of skeleton feature. + random_pad: bool, whether do random padding when frame length < window size. Default: False. + """ + + def __init__(self, window_size, random_pad=False): + self.window_size = window_size + self.random_pad = random_pad + + def get_frame_num(self, data): + C, T, V, M = data.shape + for i in range(T - 1, -1, -1): + tmp = np.sum(data[:, i, :, :]) + if tmp > 0: + T = i + 1 + break + return T + + def __call__(self, results): + data = results['data'] + + C, T, V, M = data.shape + T = self.get_frame_num(data) + if T == self.window_size: + data_pad = data[:, :self.window_size, :, :] + elif T < self.window_size: + begin = random.randint(0, self.window_size - + T) if self.random_pad else 0 + data_pad = np.zeros((C, self.window_size, V, M)) + data_pad[:, begin:begin + T, :, :] = data[:, :T, :, :] + else: + if self.random_pad: + index = np.random.choice(T, self.window_size, + replace=False).astype('int64') + else: + index = np.linspace(0, T, self.window_size).astype("int64") + data_pad = data[:, index, :, :] + + results['data'] = data_pad + return results + + +@PIPELINES.register() +class SkeletonNorm(object): + """ + Normalize skeleton feature. + Args: + aixs: dimensions of vertex coordinate. 2 for (x,y), 3 for (x,y,z). Default: 2. + """ + + def __init__(self, axis=2, squeeze=False): + self.axis = axis + self.squeeze = squeeze + + def __call__(self, results): + data = results['data'] + + # Centralization + data = data - data[:, :, 8:9, :] + data = data[:self.axis, :, :, :] # get (x,y) from (x,y, acc) + C, T, V, M = data.shape + if self.squeeze: + data = data.reshape((C, T, V)) # M = 1 + + results['data'] = data.astype('float32') + if 'label' in results: + label = results['label'] + results['label'] = np.expand_dims(label, 0).astype('int64') + return results + + +@PIPELINES.register() +class Iden(object): + """ + Wrapper Pipeline + """ + + def __init__(self, label_expand=True): + self.label_expand = label_expand + + def __call__(self, results): + data = results['data'] + results['data'] = data.astype('float32') + + if 'label' in results and self.label_expand: + label = results['label'] + results['label'] = np.expand_dims(label, 0).astype('int64') + return results + + +@PIPELINES.register() +class RandomRotation(object): + """ + Random rotation sketeton. + Args: + argument: bool, if rotation. + theta: float, rotation rate. + """ + + def __init__(self, argument, theta=0.3): + self.theta = theta + self.argument = argument + + def _rot(self, rot): + """ + rot: T,3 + """ + cos_r, sin_r = np.cos(rot), np.sin(rot) # T,3 + zeros = np.zeros((rot.shape[0], 1)) # T,1 + ones = np.ones((rot.shape[0], 1)) # T,1 + + r1 = np.stack((ones, zeros, zeros), axis=-1) # T,1,3 + rx2 = np.stack((zeros, cos_r[:, 0:1], sin_r[:, 0:1]), axis=-1) # T,1,3 + rx3 = np.stack((zeros, -sin_r[:, 0:1], cos_r[:, 0:1]), axis=-1) # T,1,3 + rx = np.concatenate((r1, rx2, rx3), axis=1) # T,3,3 + + ry1 = np.stack((cos_r[:, 1:2], zeros, -sin_r[:, 1:2]), axis=-1) + r2 = np.stack((zeros, ones, zeros), axis=-1) + ry3 = np.stack((sin_r[:, 1:2], zeros, cos_r[:, 1:2]), axis=-1) + ry = np.concatenate((ry1, r2, ry3), axis=1) + + rz1 = np.stack((cos_r[:, 2:3], sin_r[:, 2:3], zeros), axis=-1) + r3 = np.stack((zeros, zeros, ones), axis=-1) + rz2 = np.stack((-sin_r[:, 2:3], cos_r[:, 2:3], zeros), axis=-1) + rz = np.concatenate((rz1, rz2, r3), axis=1) + + rot = np.matmul(np.matmul(rz, ry), rx) + return rot + + def __call__(self, results): + # C,T,V,M + data = results['data'] + if self.argument: + C, T, V, M = data.shape + data_numpy = np.transpose(data, (1, 0, 2, 3)).conjugate().reshape( + T, C, V * M) # T,3,V*M + rot = np.random.uniform(-self.theta, self.theta, 3) + rot = np.stack([ + rot, + ] * T, axis=0) + rot = self._rot(rot) # T,3,3 + data_numpy = np.matmul(rot, data_numpy) + data_numpy = data_numpy.reshape(T, C, V, M) + data_numpy = np.transpose(data_numpy, (1, 0, 2, 3)) + data = data_numpy + results['data'] = data.astype(np.float32) + return results + + +@PIPELINES.register() +class SketeonCropSample(object): + """ + Sketeon Crop Sampler. + Args: + crop_model: str, crop model, support: ['center']. + p_interval: list, crop len + window_size: int, sample windows size. + """ + + def __init__(self, window_size, crop_model='center', p_interval=1): + assert crop_model in ['center'], "Don't support :" + crop_model + + self.crop_model = crop_model + self.window_size = window_size + self.p_interval = p_interval + + def __call__(self, results): + if self.crop_model == 'center': + # input: C,T,V,M + data = results['data'] + valid_frame_num = np.sum(data.sum(0).sum(-1).sum(-1) != 0) + + C, T, V, M = data.shape + begin = 0 + end = valid_frame_num + valid_size = end - begin + + #crop + if len(self.p_interval) == 1: + p = self.p_interval[0] + bias = int((1 - p) * valid_size / 2) + data = data[:, begin + bias:end - bias, :, :] # center_crop + cropped_length = data.shape[1] + else: + p = np.random.rand(1) * (self.p_interval[1] - self.p_interval[0] + ) + self.p_interval[0] + # constraint cropped_length lower bound as 64 + cropped_length = np.minimum( + np.maximum(int(np.floor(valid_size * p)), 64), valid_size) + bias = np.random.randint(0, valid_size - cropped_length + 1) + data = data[:, begin + bias:begin + bias + cropped_length, :, :] + + # resize + data = np.transpose(data, (0, 2, 3, 1)).conjugate().reshape( + C * V * M, cropped_length) + data = data[None, None, :, :] + # could perform both up sample and down sample + data_tensor = paddle.to_tensor(data) + data_tensor = F.interpolate(data_tensor, + size=(C * V * M, self.window_size), + mode='bilinear', + align_corners=False).squeeze() + data = paddle.transpose( + paddle.reshape(data_tensor, (C, V, M, self.window_size)), + (0, 3, 1, 2)).numpy() + else: + raise NotImplementedError + results['data'] = data + return results + + +@PIPELINES.register() +class SketeonModalityTransform(object): + """ + Sketeon Crop Sampler. + Args: + crop_model: str, crop model, support: ['center']. + p_interval: list, crop len + window_size: int, sample windows size. + """ + + def __init__(self, bone, motion, joint=True, graph='ntu_rgb_d'): + + self.joint = joint + self.bone = bone + self.motion = motion + self.graph = graph + if self.graph == "ntu_rgb_d": + self.bone_pairs = ((1, 2), (2, 21), (3, 21), (4, 3), (5, 21), + (6, 5), (7, 6), (8, 7), (9, 21), (10, 9), + (11, 10), (12, 11), (13, 1), (14, 13), (15, 14), + (16, 15), (17, 1), (18, 17), (19, 18), (20, 19), + (22, 23), (21, 21), (23, 8), (24, 25), (25, 12)) + else: + raise NotImplementedError + + def __call__(self, results): + if self.joint: + return results + data_numpy = results['data'] + if self.bone: + bone_data_numpy = np.zeros_like(data_numpy) + for v1, v2 in self.bone_pairs: + bone_data_numpy[:, :, v1 - + 1] = data_numpy[:, :, v1 - + 1] - data_numpy[:, :, v2 - 1] + data_numpy = bone_data_numpy + if self.motion: + data_numpy[:, :-1] = data_numpy[:, 1:] - data_numpy[:, :-1] + data_numpy[:, -1] = 0 + results['data'] = data_numpy + return results diff --git a/paddlevideo/loader/registry.py b/paddlevideo/loader/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..add663104d66b0f9ceadc2195f654d880b57f998 --- /dev/null +++ b/paddlevideo/loader/registry.py @@ -0,0 +1,18 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..utils import Registry + +PIPELINES = Registry("pipeline") +DATASETS = Registry("datasets") diff --git a/paddlevideo/metrics/ActivityNet/__init__.py b/paddlevideo/metrics/ActivityNet/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eefabbd72b3541cd3d6daeab01811065a4c9bd82 --- /dev/null +++ b/paddlevideo/metrics/ActivityNet/__init__.py @@ -0,0 +1,3 @@ +from .anet_prop import ANETproposal + +__all__ = ['ANETproposal'] diff --git a/paddlevideo/metrics/ActivityNet/anet_prop.py b/paddlevideo/metrics/ActivityNet/anet_prop.py new file mode 100644 index 0000000000000000000000000000000000000000..411b164f980655dbcf713915b0df320426db9be7 --- /dev/null +++ b/paddlevideo/metrics/ActivityNet/anet_prop.py @@ -0,0 +1,359 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import json +import numpy as np +import pandas as pd +import urllib.request as urllib2 +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +class ANETproposal(object): + """ + This class is used for calculating AR@N and AUC; + Code transfer from ActivityNet Gitub repository](https://github.com/activitynet/ActivityNet.git) + """ + GROUND_TRUTH_FIELDS = ['database', 'taxonomy', 'version'] + PROPOSAL_FIELDS = ['results', 'version', 'external_data'] + API = 'http://ec2-52-25-205-214.us-west-2.compute.amazonaws.com/challenge19/api.py' + + def __init__(self, + ground_truth_filename=None, + proposal_filename=None, + ground_truth_fields=GROUND_TRUTH_FIELDS, + proposal_fields=PROPOSAL_FIELDS, + tiou_thresholds=np.linspace(0.5, 0.95, 10), + max_avg_nr_proposals=None, + subset='validation', + verbose=False, + check_status=True): + if not ground_truth_filename: + raise IOError('Please input a valid ground truth file.') + if not proposal_filename: + raise IOError('Please input a valid proposal file.') + self.subset = subset + self.tiou_thresholds = tiou_thresholds + self.max_avg_nr_proposals = max_avg_nr_proposals + self.verbose = verbose + self.gt_fields = ground_truth_fields + self.pred_fields = proposal_fields + self.recall = None + self.avg_recall = None + self.proposals_per_video = None + self.check_status = check_status + # Retrieve blocked videos from server. + if self.check_status: + self.blocked_videos = self.get_blocked_videos() + else: + self.blocked_videos = list() + # Import ground truth and proposals. + self.ground_truth, self.activity_index = self._import_ground_truth( + ground_truth_filename) + self.proposal = self._import_proposal(proposal_filename) + + if self.verbose: + print('[INIT] Loaded annotations from {} subset.'.format(subset)) + nr_gt = len(self.ground_truth) + print('\tNumber of ground truth instances: {}'.format(nr_gt)) + nr_pred = len(self.proposal) + print('\tNumber of proposals: {}'.format(nr_pred)) + print('\tFixed threshold for tiou score: {}'.format( + self.tiou_thresholds)) + + def _import_ground_truth(self, ground_truth_filename): + """ + Reads ground truth file, checks if it is well formatted, and returns + the ground truth instances and the activity classes. + + Parameters: + ground_truth_filename (str): full path to the ground truth json file. + Returns: + ground_truth (df): Data frame containing the ground truth instances. + activity_index (dict): Dictionary containing class index. + """ + with open(ground_truth_filename, 'r') as fobj: + data = json.load(fobj) + # Checking format + if not all([field in data.keys() for field in self.gt_fields]): + raise IOError('Please input a valid ground truth file.') + + # Read ground truth data. + activity_index, cidx = {}, 0 + video_lst, t_start_lst, t_end_lst, label_lst = [], [], [], [] + for videoid, v in data['database'].items(): + if self.subset != v['subset']: + continue + if videoid in self.blocked_videos: + continue + for ann in v['annotations']: + if ann['label'] not in activity_index: + activity_index[ann['label']] = cidx + cidx += 1 + video_lst.append(videoid) + t_start_lst.append(float(ann['segment'][0])) + t_end_lst.append(float(ann['segment'][1])) + label_lst.append(activity_index[ann['label']]) + + ground_truth = pd.DataFrame({ + 'video-id': video_lst, + 't-start': t_start_lst, + 't-end': t_end_lst, + 'label': label_lst + }) + return ground_truth, activity_index + + def _import_proposal(self, proposal_filename): + """ + Reads proposal file, checks if it is well formatted, and returns + the proposal instances. + + Parameters: + proposal_filename (str): Full path to the proposal json file. + Returns: + proposal (df): Data frame containing the proposal instances. + """ + with open(proposal_filename, 'r') as fobj: + data = json.load(fobj) + # Checking format... + if not all([field in data.keys() for field in self.pred_fields]): + raise IOError('Please input a valid proposal file.') + + # Read predictions. + video_lst, t_start_lst, t_end_lst = [], [], [] + score_lst = [] + for videoid, v in data['results'].items(): + if videoid in self.blocked_videos: + continue + for result in v: + video_lst.append(videoid) + t_start_lst.append(float(result['segment'][0])) + t_end_lst.append(float(result['segment'][1])) + score_lst.append(result['score']) + proposal = pd.DataFrame({ + 'video-id': video_lst, + 't-start': t_start_lst, + 't-end': t_end_lst, + 'score': score_lst + }) + return proposal + + def evaluate(self): + """ + Evaluates a proposal file. To measure the performance of a + method for the proposal task, we computes the area under the + average recall vs average number of proposals per video curve. + """ + recall, avg_recall, proposals_per_video = self.average_recall_vs_avg_nr_proposals( + self.ground_truth, + self.proposal, + max_avg_nr_proposals=self.max_avg_nr_proposals, + tiou_thresholds=self.tiou_thresholds) + + area_under_curve = np.trapz(avg_recall, proposals_per_video) + + if self.verbose: + print('[RESULTS] Performance on ActivityNet proposal task.') + with open("data/bmn/BMN_Test_results/auc_result.txt", + "a") as text_file: + text_file.write( + '\tArea Under the AR vs AN curve: {}% \n'.format( + 100. * float(area_under_curve) / + proposals_per_video[-1])) + print('\tArea Under the AR vs AN curve: {}%'.format( + 100. * float(area_under_curve) / proposals_per_video[-1])) + + self.recall = recall + self.avg_recall = avg_recall + self.proposals_per_video = proposals_per_video + + def average_recall_vs_avg_nr_proposals(self, + ground_truth, + proposals, + max_avg_nr_proposals=None, + tiou_thresholds=np.linspace( + 0.5, 0.95, 10)): + """ + Computes the average recall given an average number of + proposals per video. + + Parameters: + ground_truth(df): Data frame containing the ground truth instances. + Required fields: ['video-id', 't-start', 't-end'] + proposal(df): Data frame containing the proposal instances. + Required fields: ['video-id, 't-start', 't-end', 'score'] + tiou_thresholds(1d-array | optional): array with tiou thresholds. + + Returns: + recall(2d-array): recall[i,j] is recall at ith tiou threshold at the jth + average number of average number of proposals per video. + average_recall(1d-array): recall averaged over a list of tiou threshold. + This is equivalent to recall.mean(axis=0). + proposals_per_video(1d-array): average number of proposals per video. + """ + + # Get list of videos. + video_lst = ground_truth['video-id'].unique() + + if not max_avg_nr_proposals: + max_avg_nr_proposals = float( + proposals.shape[0]) / video_lst.shape[0] + + ratio = max_avg_nr_proposals * float( + video_lst.shape[0]) / proposals.shape[0] + + # Adaptation to query faster + ground_truth_gbvn = ground_truth.groupby('video-id') + proposals_gbvn = proposals.groupby('video-id') + + # For each video, computes tiou scores among the retrieved proposals. + score_lst = [] + total_nr_proposals = 0 + for videoid in video_lst: + # Get ground-truth instances associated to this video. + ground_truth_videoid = ground_truth_gbvn.get_group(videoid) + this_video_ground_truth = ground_truth_videoid.loc[:, [ + 't-start', 't-end' + ]].values + + # Get proposals for this video. + try: + proposals_videoid = proposals_gbvn.get_group(videoid) + except: + n = this_video_ground_truth.shape[0] + score_lst.append(np.zeros((n, 1))) + continue + + this_video_proposals = proposals_videoid.loc[:, + ['t-start', 't-end' + ]].values + + if this_video_proposals.shape[0] == 0: + n = this_video_ground_truth.shape[0] + score_lst.append(np.zeros((n, 1))) + continue + + # Sort proposals by score. + sort_idx = proposals_videoid['score'].argsort()[::-1] + this_video_proposals = this_video_proposals[sort_idx, :] + + if this_video_proposals.ndim != 2: + this_video_proposals = np.expand_dims(this_video_proposals, + axis=0) + if this_video_ground_truth.ndim != 2: + this_video_ground_truth = np.expand_dims( + this_video_ground_truth, axis=0) + + nr_proposals = np.minimum( + int(this_video_proposals.shape[0] * ratio), + this_video_proposals.shape[0]) + total_nr_proposals += nr_proposals + this_video_proposals = this_video_proposals[:nr_proposals, :] + + # Compute tiou scores. + tiou = self.wrapper_segment_iou(this_video_proposals, + this_video_ground_truth) + score_lst.append(tiou) + + # Given that the length of the videos is really varied, we + # compute the number of proposals in terms of a ratio of the total + # proposals retrieved, i.e. average recall at a percentage of proposals + # retrieved per video. + + # Computes average recall. + pcn_lst = np.arange(1, 101) / 100.0 * (max_avg_nr_proposals * float( + video_lst.shape[0]) / total_nr_proposals) + matches = np.empty((video_lst.shape[0], pcn_lst.shape[0])) + positives = np.empty(video_lst.shape[0]) + recall = np.empty((tiou_thresholds.shape[0], pcn_lst.shape[0])) + # Iterates over each tiou threshold. + for ridx, tiou in enumerate(tiou_thresholds): + + # Inspect positives retrieved per video at different + # number of proposals (percentage of the total retrieved). + for i, score in enumerate(score_lst): + # Total positives per video. + positives[i] = score.shape[0] + # Find proposals that satisfies minimum tiou threshold. + true_positives_tiou = score >= tiou + # Get number of proposals as a percentage of total retrieved. + pcn_proposals = np.minimum( + (score.shape[1] * pcn_lst).astype(int), score.shape[1]) + + for j, nr_proposals in enumerate(pcn_proposals): + # Compute the number of matches for each percentage of the proposals + matches[i, j] = np.count_nonzero( + (true_positives_tiou[:, :nr_proposals]).sum(axis=1)) + + # Computes recall given the set of matches per video. + recall[ridx, :] = matches.sum(axis=0) / positives.sum() + + # Recall is averaged. + avg_recall = recall.mean(axis=0) + + # Get the average number of proposals per video. + proposals_per_video = pcn_lst * (float(total_nr_proposals) / + video_lst.shape[0]) + + return recall, avg_recall, proposals_per_video + + def get_blocked_videos(self, api=API): + api_url = '{}?action=get_blocked'.format(api) + req = urllib2.Request(api_url) + response = urllib2.urlopen(req) + return json.loads(response.read()) + + def wrapper_segment_iou(self, target_segments, candidate_segments): + """ + Compute intersection over union btw segments + Parameters: + target_segments(nd-array): 2-dim array in format [m x 2:=[init, end]] + candidate_segments(nd-array): 2-dim array in format [n x 2:=[init, end]] + Returns: + tiou(nd-array): 2-dim array [n x m] with IOU ratio. + Note: It assumes that candidate-segments are more scarce that target-segments + """ + if candidate_segments.ndim != 2 or target_segments.ndim != 2: + raise ValueError('Dimension of arguments is incorrect') + + n, m = candidate_segments.shape[0], target_segments.shape[0] + tiou = np.empty((n, m)) + for i in range(m): + tiou[:, i] = self.segment_iou(target_segments[i, :], + candidate_segments) + + return tiou + + def segment_iou(self, target_segment, candidate_segments): + """ + Compute the temporal intersection over union between a + target segment and all the test segments. + + Parameters: + target_segment(1d-array): Temporal target segment containing [starting, ending] times. + candidate_segments(2d-array): Temporal candidate segments containing N x [starting, ending] times. + + Returns: + tiou(1d-array): Temporal intersection over union score of the N's candidate segments. + """ + tt1 = np.maximum(target_segment[0], candidate_segments[:, 0]) + tt2 = np.minimum(target_segment[1], candidate_segments[:, 1]) + # Intersection including Non-negative overlap score. + segments_intersection = (tt2 - tt1).clip(0) + # Segment union. + segments_union = (candidate_segments[:, 1] - candidate_segments[:, 0]) \ + + (target_segment[1] - target_segment[0]) - segments_intersection + # Compute overlap as the ratio of the intersection + # over union of two segments. + tIoU = segments_intersection.astype(float) / segments_union + return tIoU diff --git a/paddlevideo/metrics/__init__.py b/paddlevideo/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f979f27fe1e6977ce35193824fbc48cf4268bf08 --- /dev/null +++ b/paddlevideo/metrics/__init__.py @@ -0,0 +1,34 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .bmn_metric import BMNMetric +from .build import build_metric +from .center_crop_metric import CenterCropMetric +from .depth_metric import DepthMetric +from .msrvtt_metric import MSRVTTMetric +from .multi_crop_metric import MultiCropMetric +from .registry import METRIC +from .skeleton_metric import SkeletonMetric +from .transnetv2_metric import TransNetV2Metric +from .youtube8m.eval_util import HitOneMetric +from .segmentation_metric import SegmentationMetric +from .ava_metric import AVAMetric +from .vos_metric import VOSMetric +from .center_crop_metric_MRI import CenterCropMetric_MRI + +__all__ = [ + 'METRIC', 'build_metric', 'MultiCropMetric', 'BMNMetric', + 'CenterCropMetric', 'SkeletonMetric', 'HitOneMetric', 'TransNetV2Metric', + 'DepthMetric', 'MSRVTTMetric', 'VOSMetric', 'CenterCropMetric_MRI','AVAMetric', 'SegmentationMetric' +] diff --git a/paddlevideo/metrics/ava_evaluation/README.md b/paddlevideo/metrics/ava_evaluation/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7414d0fbbd32d24d1e1b745d1df6a3fd2a2c2a43 --- /dev/null +++ b/paddlevideo/metrics/ava_evaluation/README.md @@ -0,0 +1,2 @@ +The code under this folder is from the official [ActivityNet repo](https://github.com/activitynet/ActivityNet). +Some unused codes are removed to minimize the length of codes added. diff --git a/paddlevideo/metrics/ava_evaluation/__init__.py b/paddlevideo/metrics/ava_evaluation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/paddlevideo/metrics/ava_evaluation/metrics.py b/paddlevideo/metrics/ava_evaluation/metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..13eb034697be05a4c8030e4c1f93ece73a5bab1e --- /dev/null +++ b/paddlevideo/metrics/ava_evaluation/metrics.py @@ -0,0 +1,143 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Functions for computing metrics like precision, recall, CorLoc and etc.""" + +import numpy as np + + +def compute_precision_recall(scores, labels, num_gt): + """Compute precision and recall. + + Args: + scores: A float numpy array representing detection score + labels: A boolean numpy array representing true/false positive labels + num_gt: Number of ground truth instances + + Raises: + ValueError: if the input is not of the correct format + + Returns: + precision: Fraction of positive instances over detected ones. This + value is None if no ground truth labels are present. + recall: Fraction of detected positive instance over all positive + instances. This value is None if no ground truth labels are + present. + """ + if (not isinstance(labels, np.ndarray) or labels.dtype != np.bool + or len(labels.shape) != 1): + raise ValueError('labels must be single dimension bool numpy array') + + if not isinstance(scores, np.ndarray) or len(scores.shape) != 1: + raise ValueError('scores must be single dimension numpy array') + + if num_gt < np.sum(labels): + raise ValueError( + 'Number of true positives must be smaller than num_gt.') + + if len(scores) != len(labels): + raise ValueError('scores and labels must be of the same size.') + + if num_gt == 0: + return None, None + + sorted_indices = np.argsort(scores) + sorted_indices = sorted_indices[::-1] + labels = labels.astype(int) + true_positive_labels = labels[sorted_indices] + false_positive_labels = 1 - true_positive_labels + cum_true_positives = np.cumsum(true_positive_labels) + cum_false_positives = np.cumsum(false_positive_labels) + precision = cum_true_positives.astype(float) / ( + cum_true_positives + cum_false_positives) + recall = cum_true_positives.astype(float) / num_gt + return precision, recall + + +def compute_average_precision(precision, recall): + """Compute Average Precision according to the definition in VOCdevkit. + + Precision is modified to ensure that it does not decrease as recall + decrease. + + Args: + precision: A float [N, 1] numpy array of precisions + recall: A float [N, 1] numpy array of recalls + + Raises: + ValueError: if the input is not of the correct format + + Returns: + average_precison: The area under the precision recall curve. NaN if + precision and recall are None. + """ + if precision is None: + if recall is not None: + raise ValueError('If precision is None, recall must also be None') + return np.NAN + + if not isinstance(precision, np.ndarray) or not isinstance( + recall, np.ndarray): + raise ValueError('precision and recall must be numpy array') + if precision.dtype != np.float or recall.dtype != np.float: + raise ValueError('input must be float numpy array.') + if len(precision) != len(recall): + raise ValueError('precision and recall must be of the same size.') + if not precision.size: + return 0.0 + if np.amin(precision) < 0 or np.amax(precision) > 1: + raise ValueError('Precision must be in the range of [0, 1].') + if np.amin(recall) < 0 or np.amax(recall) > 1: + raise ValueError('recall must be in the range of [0, 1].') + if not all(recall[i] <= recall[i + 1] for i in range(len(recall) - 1)): + raise ValueError('recall must be a non-decreasing array') + + recall = np.concatenate([[0], recall, [1]]) + precision = np.concatenate([[0], precision, [0]]) + + # Preprocess precision to be a non-decreasing array + for i in range(len(precision) - 2, -1, -1): + precision[i] = np.maximum(precision[i], precision[i + 1]) + + indices = np.where(recall[1:] != recall[:-1])[0] + 1 + average_precision = np.sum( + (recall[indices] - recall[indices - 1]) * precision[indices]) + return average_precision + + +def compute_cor_loc(num_gt_imgs_per_class, + num_images_correctly_detected_per_class): + """Compute CorLoc according to the definition in the following paper. + + https://www.robots.ox.ac.uk/~vgg/rg/papers/deselaers-eccv10.pdf + + Returns nans if there are no ground truth images for a class. + + Args: + num_gt_imgs_per_class: 1D array, representing number of images + containing at least one object instance of a particular class + num_images_correctly_detected_per_class: 1D array, representing number + of images that are correctly detected at least one object instance + of a particular class + + Returns: + corloc_per_class: A float numpy array represents the corloc score of + each class + """ + # Divide by zero expected for classes with no gt examples. + with np.errstate(divide='ignore', invalid='ignore'): + return np.where( + num_gt_imgs_per_class == 0, np.nan, + num_images_correctly_detected_per_class / num_gt_imgs_per_class) diff --git a/paddlevideo/metrics/ava_evaluation/np_box_list.py b/paddlevideo/metrics/ava_evaluation/np_box_list.py new file mode 100644 index 0000000000000000000000000000000000000000..f9b101e6f5d5dffdbf37c0986a93f1a3c71e0c48 --- /dev/null +++ b/paddlevideo/metrics/ava_evaluation/np_box_list.py @@ -0,0 +1,138 @@ +# Copyright 2017 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================= +"""Numpy BoxList classes and functions.""" + +import numpy as np + + +class BoxList: + """Box collection. + + BoxList represents a list of bounding boxes as numpy array, where each + bounding box is represented as a row of 4 numbers, + [y_min, x_min, y_max, x_max]. It is assumed that all bounding boxes within + a given list correspond to a single image. + + Optionally, users can add additional related fields (such as + objectness/classification scores). + """ + + def __init__(self, data): + """Constructs box collection. + + Args: + data: a numpy array of shape [N, 4] representing box coordinates + + Raises: + ValueError: if bbox data is not a numpy array + ValueError: if invalid dimensions for bbox data + """ + if not isinstance(data, np.ndarray): + raise ValueError('data must be a numpy array.') + if len(data.shape) != 2 or data.shape[1] != 4: + raise ValueError('Invalid dimensions for box data.') + if data.dtype != np.float32 and data.dtype != np.float64: + raise ValueError( + 'Invalid data type for box data: float is required.') + if not self._is_valid_boxes(data): + raise ValueError('Invalid box data. data must be a numpy array of ' + 'N*[y_min, x_min, y_max, x_max]') + self.data = {'boxes': data} + + def num_boxes(self): + """Return number of boxes held in collections.""" + return self.data['boxes'].shape[0] + + def get_extra_fields(self): + """Return all non-box fields.""" + return [k for k in self.data if k != 'boxes'] + + def has_field(self, field): + return field in self.data + + def add_field(self, field, field_data): + """Add data to a specified field. + + Args: + field: a string parameter used to speficy a related field to be + accessed. + field_data: a numpy array of [N, ...] representing the data + associated with the field. + Raises: + ValueError: if the field is already exist or the dimension of the + field data does not matches the number of boxes. + """ + if self.has_field(field): + raise ValueError('Field ' + field + 'already exists') + if len(field_data.shape) < 1 or field_data.shape[0] != self.num_boxes( + ): + raise ValueError('Invalid dimensions for field data') + self.data[field] = field_data + + def get(self): + """Convenience function for accesssing box coordinates. + + Returns: + a numpy array of shape [N, 4] representing box corners + """ + return self.get_field('boxes') + + def get_field(self, field): + """Accesses data associated with the specified field in the box + collection. + + Args: + field: a string parameter used to speficy a related field to be + accessed. + + Returns: + a numpy 1-d array representing data of an associated field + + Raises: + ValueError: if invalid field + """ + if not self.has_field(field): + raise ValueError(f'field {field} does not exist') + return self.data[field] + + def get_coordinates(self): + """Get corner coordinates of boxes. + + Returns: + a list of 4 1-d numpy arrays [y_min, x_min, y_max, x_max] + """ + box_coordinates = self.get() + y_min = box_coordinates[:, 0] + x_min = box_coordinates[:, 1] + y_max = box_coordinates[:, 2] + x_max = box_coordinates[:, 3] + return [y_min, x_min, y_max, x_max] + + def _is_valid_boxes(self, data): + """Check whether data fullfills the format of N*[ymin, xmin, ymax, + xmin]. + + Args: + data: a numpy array of shape [N, 4] representing box coordinates + + Returns: + a boolean indicating whether all ymax of boxes are equal or greater + than ymin, and all xmax of boxes are equal or greater than xmin. + """ + if len(data): + for v in data: + if v[0] > v[2] or v[1] > v[3]: + return False + return True diff --git a/paddlevideo/metrics/ava_evaluation/np_box_ops.py b/paddlevideo/metrics/ava_evaluation/np_box_ops.py new file mode 100644 index 0000000000000000000000000000000000000000..94e7d300c80195f8a0299fbf33000dba9719bb0d --- /dev/null +++ b/paddlevideo/metrics/ava_evaluation/np_box_ops.py @@ -0,0 +1,98 @@ +# Copyright 2017 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Operations for [N, 4] numpy arrays representing bounding boxes. + +Example box operations that are supported: + * Areas: compute bounding box areas + * IOU: pairwise intersection-over-union scores +""" + +import numpy as np + + +def area(boxes): + """Computes area of boxes. + + Args: + boxes: Numpy array with shape [N, 4] holding N boxes + + Returns: + a numpy array with shape [N*1] representing box areas + """ + return (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) + + +def intersection(boxes1, boxes2): + """Compute pairwise intersection areas between boxes. + + Args: + boxes1: a numpy array with shape [N, 4] holding N boxes + boxes2: a numpy array with shape [M, 4] holding M boxes + + Returns: + a numpy array with shape [N*M] representing pairwise intersection area + """ + [y_min1, x_min1, y_max1, x_max1] = np.split(boxes1, 4, axis=1) + [y_min2, x_min2, y_max2, x_max2] = np.split(boxes2, 4, axis=1) + + all_pairs_min_ymax = np.minimum(y_max1, np.transpose(y_max2)) + all_pairs_max_ymin = np.maximum(y_min1, np.transpose(y_min2)) + intersect_heights = np.maximum( + np.zeros(all_pairs_max_ymin.shape), + all_pairs_min_ymax - all_pairs_max_ymin) + all_pairs_min_xmax = np.minimum(x_max1, np.transpose(x_max2)) + all_pairs_max_xmin = np.maximum(x_min1, np.transpose(x_min2)) + intersect_widths = np.maximum( + np.zeros(all_pairs_max_xmin.shape), + all_pairs_min_xmax - all_pairs_max_xmin) + return intersect_heights * intersect_widths + + +def iou(boxes1, boxes2): + """Computes pairwise intersection-over-union between box collections. + + Args: + boxes1: a numpy array with shape [N, 4] holding N boxes. + boxes2: a numpy array with shape [M, 4] holding N boxes. + + Returns: + a numpy array with shape [N, M] representing pairwise iou scores. + """ + intersect = intersection(boxes1, boxes2) + area1 = area(boxes1) + area2 = area(boxes2) + union = ( + np.expand_dims(area1, axis=1) + np.expand_dims(area2, axis=0) - + intersect) + return intersect / union + + +def ioa(boxes1, boxes2): + """Computes pairwise intersection-over-area between box collections. + + Intersection-over-area (ioa) between two boxes box1 and box2 is defined as + their intersection area over box2's area. Note that ioa is not symmetric, + that is, IOA(box1, box2) != IOA(box2, box1). + + Args: + boxes1: a numpy array with shape [N, 4] holding N boxes. + boxes2: a numpy array with shape [M, 4] holding N boxes. + + Returns: + a numpy array with shape [N, M] representing pairwise ioa scores. + """ + intersect = intersection(boxes1, boxes2) + areas = np.expand_dims(area(boxes2), axis=0) + return intersect / areas diff --git a/paddlevideo/metrics/ava_evaluation/object_detection_evaluation.py b/paddlevideo/metrics/ava_evaluation/object_detection_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..c9f00540f936c7e1993c41c0977527dcb125369e --- /dev/null +++ b/paddlevideo/metrics/ava_evaluation/object_detection_evaluation.py @@ -0,0 +1,658 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================= +"""object_detection_evaluation module. + +ObjectDetectionEvaluation is a class which manages ground truth information of +a object detection dataset, and computes frequently used detection metrics such +as Precision, Recall, CorLoc of the provided detection results. +It supports the following operations: +1) Add ground truth information of images sequentially. +2) Add detection result of images sequentially. +3) Evaluate detection metrics on already inserted detection results. +4) Write evaluation result into a pickle file for future processing or + visualization. + +Note: This module operates on numpy boxes and box lists. +""" +import collections +import logging +from abc import ABCMeta, abstractmethod + +import numpy as np + +from . import metrics, per_image_evaluation, standard_fields + + +class DetectionEvaluator: + """Interface for object detection evalution classes. + + Example usage of the Evaluator: + ------------------------------ + evaluator = DetectionEvaluator(categories) + + # Detections and groundtruth for image 1. + evaluator.add_single_groundtruth_image_info(...) + evaluator.add_single_detected_image_info(...) + + # Detections and groundtruth for image 2. + evaluator.add_single_groundtruth_image_info(...) + evaluator.add_single_detected_image_info(...) + + metrics_dict = evaluator.evaluate() + """ + + __metaclass__ = ABCMeta + + def __init__(self, categories): + """Constructor. + + Args: + categories: A list of dicts, each of which has the following keys - + 'id': (required) an integer id uniquely identifying this + category. + 'name': (required) string representing category name e.g., + 'cat', 'dog'. + """ + self._categories = categories + + @abstractmethod + def add_single_ground_truth_image_info(self, image_id, groundtruth_dict): + """Adds groundtruth for a single image to be used for evaluation. + + Args: + image_id: A unique string/integer identifier for the image. + groundtruth_dict: A dictionary of groundtruth numpy arrays required + for evaluations. + """ + + @abstractmethod + def add_single_detected_image_info(self, image_id, detections_dict): + """Adds detections for a single image to be used for evaluation. + + Args: + image_id: A unique string/integer identifier for the image. + detections_dict: A dictionary of detection numpy arrays required + for evaluation. + """ + + @abstractmethod + def evaluate(self): + """Evaluates detections and returns a dictionary of metrics.""" + + @abstractmethod + def clear(self): + """Clears the state to prepare for a fresh evaluation.""" + + +class ObjectDetectionEvaluator(DetectionEvaluator): + """A class to evaluate detections.""" + + def __init__( + self, + categories, + matching_iou_threshold=0.5, + evaluate_corlocs=False, + metric_prefix=None, + use_weighted_mean_ap=False, + evaluate_masks=False, + ): + """Constructor. + + Args: + categories: A list of dicts, each of which has the following keys - + 'id': (required) an integer id uniquely identifying this + category. + 'name': (required) string representing category name e.g., + 'cat', 'dog'. + matching_iou_threshold: IOU threshold to use for matching + groundtruth boxes to detection boxes. + evaluate_corlocs: (optional) boolean which determines if corloc + scores are to be returned or not. + metric_prefix: (optional) string prefix for metric name; if None, + no prefix is used. + use_weighted_mean_ap: (optional) boolean which determines if the + mean average precision is computed directly from the scores and + tp_fp_labels of all classes. + evaluate_masks: If False, evaluation will be performed based on + boxes. If True, mask evaluation will be performed instead. + + Raises: + ValueError: If the category ids are not 1-indexed. + """ + super(ObjectDetectionEvaluator, self).__init__(categories) + self._num_classes = max([cat['id'] for cat in categories]) + if min(cat['id'] for cat in categories) < 1: + raise ValueError('Classes should be 1-indexed.') + self._matching_iou_threshold = matching_iou_threshold + self._use_weighted_mean_ap = use_weighted_mean_ap + self._label_id_offset = 1 + self._evaluate_masks = evaluate_masks + self._evaluation = ObjectDetectionEvaluation( + num_groundtruth_classes=self._num_classes, + matching_iou_threshold=self._matching_iou_threshold, + use_weighted_mean_ap=self._use_weighted_mean_ap, + label_id_offset=self._label_id_offset, + ) + self._image_ids = set([]) + self._evaluate_corlocs = evaluate_corlocs + self._metric_prefix = (metric_prefix + '_') if metric_prefix else '' + + def add_single_ground_truth_image_info(self, image_id, groundtruth_dict): + """Adds groundtruth for a single image to be used for evaluation. + + Args: + image_id: A unique string/integer identifier for the image. + groundtruth_dict: A dictionary containing - + standard_fields.InputDataFields.groundtruth_boxes: float32 + numpy array of shape [num_boxes, 4] containing `num_boxes` + groundtruth boxes of the format [ymin, xmin, ymax, xmax] in + absolute image coordinates. + standard_fields.InputDataFields.groundtruth_classes: integer + numpy array of shape [num_boxes] containing 1-indexed + groundtruth classes for the boxes. + standard_fields.InputDataFields.groundtruth_difficult: Optional + length M numpy boolean array denoting whether a ground + truth box is a difficult instance or not. This field is + optional to support the case that no boxes are difficult. + standard_fields.InputDataFields.groundtruth_instance_masks: + Optional numpy array of shape [num_boxes, height, width] + with values in {0, 1}. + + Raises: + ValueError: On adding groundtruth for an image more than once. Will + also raise error if instance masks are not in groundtruth + dictionary. + """ + if image_id in self._image_ids: + raise ValueError( + 'Image with id {} already added.'.format(image_id)) + + groundtruth_classes = ( + groundtruth_dict[ + standard_fields.InputDataFields.groundtruth_classes] - + self._label_id_offset) + # If the key is not present in the groundtruth_dict or the array is + # empty (unless there are no annotations for the groundtruth on this + # image) use values from the dictionary or insert None otherwise. + if (standard_fields.InputDataFields.groundtruth_difficult + in groundtruth_dict.keys()) and (groundtruth_dict[ + standard_fields.InputDataFields.groundtruth_difficult].size + or + not groundtruth_classes.size): + groundtruth_difficult = groundtruth_dict[ + standard_fields.InputDataFields.groundtruth_difficult] + else: + groundtruth_difficult = None + if not len(self._image_ids) % 1000: + logging.warn(('image %s does not have groundtruth difficult ' + 'flag specified'), image_id) + groundtruth_masks = None + if self._evaluate_masks: + if (standard_fields.InputDataFields.groundtruth_instance_masks + not in groundtruth_dict): + raise ValueError( + 'Instance masks not in groundtruth dictionary.') + groundtruth_masks = groundtruth_dict[ + standard_fields.InputDataFields.groundtruth_instance_masks] + self._evaluation.add_single_ground_truth_image_info( + image_key=image_id, + groundtruth_boxes=groundtruth_dict[ + standard_fields.InputDataFields.groundtruth_boxes], + groundtruth_class_labels=groundtruth_classes, + groundtruth_is_difficult_list=groundtruth_difficult, + groundtruth_masks=groundtruth_masks, + ) + self._image_ids.update([image_id]) + + def add_single_detected_image_info(self, image_id, detections_dict): + """Adds detections for a single image to be used for evaluation. + + Args: + image_id: A unique string/integer identifier for the image. + detections_dict: A dictionary containing - + standard_fields.DetectionResultFields.detection_boxes: float32 + numpy array of shape [num_boxes, 4] containing `num_boxes` + detection boxes of the format [ymin, xmin, ymax, xmax] in + absolute image coordinates. + standard_fields.DetectionResultFields.detection_scores: float32 + numpy array of shape [num_boxes] containing detection + scores for the boxes. + standard_fields.DetectionResultFields.detection_classes: + integer numpy array of shape [num_boxes] containing + 1-indexed detection classes for the boxes. + standard_fields.DetectionResultFields.detection_masks: uint8 + numpy array of shape [num_boxes, height, width] containing + `num_boxes` masks of values ranging between 0 and 1. + + Raises: + ValueError: If detection masks are not in detections dictionary. + """ + detection_classes = ( + detections_dict[ + standard_fields.DetectionResultFields.detection_classes] - + self._label_id_offset) + detection_masks = None + if self._evaluate_masks: + if (standard_fields.DetectionResultFields.detection_masks + not in detections_dict): + raise ValueError( + 'Detection masks not in detections dictionary.') + detection_masks = detections_dict[ + standard_fields.DetectionResultFields.detection_masks] + self._evaluation.add_single_detected_image_info( + image_key=image_id, + detected_boxes=detections_dict[ + standard_fields.DetectionResultFields.detection_boxes], + detected_scores=detections_dict[ + standard_fields.DetectionResultFields.detection_scores], + detected_class_labels=detection_classes, + detected_masks=detection_masks, + ) + + def create_category_index(self, categories): + """Creates dictionary of COCO compatible categories keyed by category + id. + + Args: + categories: a list of dicts, each of which has the following keys: + 'id': (required) an integer id uniquely identifying this + category. + 'name': (required) string representing category name + e.g., 'cat', 'dog', 'pizza'. + + Returns: + category_index: a dict containing the same entries as categories, + but keyed by the 'id' field of each category. + """ + category_index = {} + for cat in categories: + category_index[cat['id']] = cat + return category_index + + def evaluate(self): + """Compute evaluation result. + + Returns: + A dictionary of metrics with the following fields - + + 1. summary_metrics: + 'Precision/mAP@IOU': mean average + precision at the specified IOU threshold + + 2. per_category_ap: category specific results with keys of the form + 'PerformanceByCategory/mAP@IOU/category' + """ + ( + per_class_ap, + mean_ap, + _, + _, + per_class_corloc, + mean_corloc, + ) = self._evaluation.evaluate() + + metric = f'mAP@{self._matching_iou_threshold}IOU' + pascal_metrics = {self._metric_prefix + metric: mean_ap} + if self._evaluate_corlocs: + pascal_metrics[self._metric_prefix + + 'Precision/meanCorLoc@{}IOU'.format( + self._matching_iou_threshold)] = mean_corloc + category_index = self.create_category_index(self._categories) + for idx in range(per_class_ap.size): + if idx + self._label_id_offset in category_index: + display_name = ( + self._metric_prefix + + 'PerformanceByCategory/AP@{}IOU/{}'.format( + self._matching_iou_threshold, + category_index[idx + self._label_id_offset]['name'], + )) + pascal_metrics[display_name] = per_class_ap[idx] + + # Optionally add CorLoc metrics.classes + if self._evaluate_corlocs: #False + display_name = ( + self._metric_prefix + + 'PerformanceByCategory/CorLoc@{}IOU/{}'.format( + self._matching_iou_threshold, + category_index[idx + + self._label_id_offset]['name'], + )) + pascal_metrics[display_name] = per_class_corloc[idx] + + return pascal_metrics + + def clear(self): + """Clears the state to prepare for a fresh evaluation.""" + self._evaluation = ObjectDetectionEvaluation( + num_groundtruth_classes=self._num_classes, + matching_iou_threshold=self._matching_iou_threshold, + use_weighted_mean_ap=self._use_weighted_mean_ap, + label_id_offset=self._label_id_offset, + ) + self._image_ids.clear() + + +class PascalDetectionEvaluator(ObjectDetectionEvaluator): + """A class to evaluate detections using PASCAL metrics.""" + + def __init__(self, categories, matching_iou_threshold=0.5): + super(PascalDetectionEvaluator, self).__init__( + categories, + matching_iou_threshold=matching_iou_threshold, + evaluate_corlocs=False, + use_weighted_mean_ap=False, + ) + + +ObjectDetectionEvalMetrics = collections.namedtuple( + 'ObjectDetectionEvalMetrics', + [ + 'average_precisions', + 'mean_ap', + 'precisions', + 'recalls', + 'corlocs', + 'mean_corloc', + ], +) + + +class ObjectDetectionEvaluation: + """Internal implementation of Pascal object detection metrics.""" + + def __init__( + self, + num_groundtruth_classes, + matching_iou_threshold=0.5, + nms_iou_threshold=1.0, + nms_max_output_boxes=10000, + use_weighted_mean_ap=False, + label_id_offset=0, + ): + if num_groundtruth_classes < 1: + raise ValueError( + 'Need at least 1 groundtruth class for evaluation.') + + self.per_image_eval = per_image_evaluation.PerImageEvaluation( + num_groundtruth_classes=num_groundtruth_classes, + matching_iou_threshold=matching_iou_threshold, + ) + self.num_class = num_groundtruth_classes + self.use_weighted_mean_ap = use_weighted_mean_ap + self.label_id_offset = label_id_offset + + self.groundtruth_boxes = {} + self.groundtruth_class_labels = {} + self.groundtruth_masks = {} + self.groundtruth_is_difficult_list = {} + self.groundtruth_is_group_of_list = {} + self.num_gt_instances_per_class = np.zeros(self.num_class, dtype=int) + self.num_gt_imgs_per_class = np.zeros(self.num_class, dtype=int) + + self._initialize_detections() + + def _initialize_detections(self): + self.detection_keys = set() + self.scores_per_class = [[] for _ in range(self.num_class)] + self.tp_fp_labels_per_class = [[] for _ in range(self.num_class)] + self.num_images_correctly_detected_per_class = np.zeros(self.num_class) + self.average_precision_per_class = np.empty( + self.num_class, dtype=float) + self.average_precision_per_class.fill(np.nan) + self.precisions_per_class = [] + self.recalls_per_class = [] + self.corloc_per_class = np.ones(self.num_class, dtype=float) + + def clear_detections(self): + self._initialize_detections() + + def add_single_ground_truth_image_info( + self, + image_key, + groundtruth_boxes, + groundtruth_class_labels, + groundtruth_is_difficult_list=None, + groundtruth_is_group_of_list=None, + groundtruth_masks=None, + ): + """Adds groundtruth for a single image to be used for evaluation. + + Args: + image_key: A unique string/integer identifier for the image. + groundtruth_boxes: float32 numpy array of shape [num_boxes, 4] + containing `num_boxes` groundtruth boxes of the format + [ymin, xmin, ymax, xmax] in absolute image coordinates. + groundtruth_class_labels: integer numpy array of shape [num_boxes] + containing 0-indexed groundtruth classes for the boxes. + groundtruth_is_difficult_list: A length M numpy boolean array + denoting whether a ground truth box is a difficult instance or + not. To support the case that no boxes are difficult, it is by + default set as None. + groundtruth_is_group_of_list: A length M numpy boolean array + denoting whether a ground truth box is a group-of box or not. + To support the case that no boxes are groups-of, it is by + default set as None. + groundtruth_masks: uint8 numpy array of shape + [num_boxes, height, width] containing `num_boxes` groundtruth + masks. The mask values range from 0 to 1. + """ + if image_key in self.groundtruth_boxes: + logging.warn(('image %s has already been added to the ground ' + 'truth database.'), image_key) + return + + self.groundtruth_boxes[image_key] = groundtruth_boxes + self.groundtruth_class_labels[image_key] = groundtruth_class_labels + self.groundtruth_masks[image_key] = groundtruth_masks + if groundtruth_is_difficult_list is None: + num_boxes = groundtruth_boxes.shape[0] + groundtruth_is_difficult_list = np.zeros(num_boxes, dtype=bool) + self.groundtruth_is_difficult_list[ + image_key] = groundtruth_is_difficult_list.astype(dtype=bool) + if groundtruth_is_group_of_list is None: + num_boxes = groundtruth_boxes.shape[0] + groundtruth_is_group_of_list = np.zeros(num_boxes, dtype=bool) + self.groundtruth_is_group_of_list[ + image_key] = groundtruth_is_group_of_list.astype(dtype=bool) + + self._update_ground_truth_statistics( + groundtruth_class_labels, + groundtruth_is_difficult_list.astype(dtype=bool), + groundtruth_is_group_of_list.astype(dtype=bool), + ) + + def add_single_detected_image_info( + self, + image_key, + detected_boxes, + detected_scores, + detected_class_labels, + detected_masks=None, + ): + """Adds detections for a single image to be used for evaluation. + + Args: + image_key: A unique string/integer identifier for the image. + detected_boxes: float32 numpy array of shape [num_boxes, 4] + containing `num_boxes` detection boxes of the format + [ymin, xmin, ymax, xmax] in absolute image coordinates. + detected_scores: float32 numpy array of shape [num_boxes] + containing detection scores for the boxes. + detected_class_labels: integer numpy array of shape [num_boxes] + containing 0-indexed detection classes for the boxes. + detected_masks: np.uint8 numpy array of shape + [num_boxes, height, width] containing `num_boxes` detection + masks with values ranging between 0 and 1. + + Raises: + ValueError: if the number of boxes, scores and class labels differ + in length. + """ + if len(detected_boxes) != len(detected_scores) or len( + detected_boxes) != len(detected_class_labels): + raise ValueError( + 'detected_boxes, detected_scores and ' + 'detected_class_labels should all have same lengths. Got' + '[%d, %d, %d]' % len(detected_boxes), + len(detected_scores), + len(detected_class_labels), + ) + + if image_key in self.detection_keys: + logging.warn(('image %s has already been added to the ground ' + 'truth database.'), image_key) + return + + self.detection_keys.add(image_key) + if image_key in self.groundtruth_boxes: + groundtruth_boxes = self.groundtruth_boxes[image_key] + groundtruth_class_labels = self.groundtruth_class_labels[image_key] + # Masks are popped instead of look up. The reason is that we do not + # want to keep all masks in memory which can cause memory overflow. + groundtruth_masks = self.groundtruth_masks.pop(image_key) + groundtruth_is_difficult_list = self.groundtruth_is_difficult_list[ + image_key] + groundtruth_is_group_of_list = self.groundtruth_is_group_of_list[ + image_key] + else: + groundtruth_boxes = np.empty(shape=[0, 4], dtype=float) + groundtruth_class_labels = np.array([], dtype=int) + if detected_masks is None: + groundtruth_masks = None + else: + groundtruth_masks = np.empty(shape=[0, 1, 1], dtype=float) + groundtruth_is_difficult_list = np.array([], dtype=bool) + groundtruth_is_group_of_list = np.array([], dtype=bool) + ( + scores, + tp_fp_labels, + ) = self.per_image_eval.compute_object_detection_metrics( + detected_boxes=detected_boxes, + detected_scores=detected_scores, + detected_class_labels=detected_class_labels, + groundtruth_boxes=groundtruth_boxes, + groundtruth_class_labels=groundtruth_class_labels, + groundtruth_is_difficult_list=groundtruth_is_difficult_list, + groundtruth_is_group_of_list=groundtruth_is_group_of_list, + detected_masks=detected_masks, + groundtruth_masks=groundtruth_masks, + ) + + for i in range(self.num_class): + if scores[i].shape[0] > 0: + self.scores_per_class[i].append(scores[i]) + self.tp_fp_labels_per_class[i].append(tp_fp_labels[i]) + + def _update_ground_truth_statistics( + self, + groundtruth_class_labels, + groundtruth_is_difficult_list, + groundtruth_is_group_of_list, + ): + """Update grouth truth statitistics. + + 1. Difficult boxes are ignored when counting the number of ground truth + instances as done in Pascal VOC devkit. + 2. Difficult boxes are treated as normal boxes when computing CorLoc + related statitistics. + + Args: + groundtruth_class_labels: An integer numpy array of length M, + representing M class labels of object instances in ground truth + groundtruth_is_difficult_list: A boolean numpy array of length M + denoting whether a ground truth box is a difficult instance or + not + groundtruth_is_group_of_list: A boolean numpy array of length M + denoting whether a ground truth box is a group-of box or not + """ + for class_index in range(self.num_class): + num_gt_instances = np.sum(groundtruth_class_labels[ + ~groundtruth_is_difficult_list + & ~groundtruth_is_group_of_list] == class_index) + self.num_gt_instances_per_class[class_index] += num_gt_instances + if np.any(groundtruth_class_labels == class_index): + self.num_gt_imgs_per_class[class_index] += 1 + + def evaluate(self): + """Compute evaluation result. + + Returns: + A named tuple with the following fields - + average_precision: float numpy array of average precision for + each class. + mean_ap: mean average precision of all classes, float scalar + precisions: List of precisions, each precision is a float numpy + array + recalls: List of recalls, each recall is a float numpy array + corloc: numpy float array + mean_corloc: Mean CorLoc score for each class, float scalar + """ + if (self.num_gt_instances_per_class == 0).any(): + print( + 'The following classes have no ground truth examples: %s', + np.squeeze(np.argwhere(self.num_gt_instances_per_class == 0)) + + self.label_id_offset, "self.detection_keys:",self.detection_keys + ) + + if self.use_weighted_mean_ap: + all_scores = np.array([], dtype=float) + all_tp_fp_labels = np.array([], dtype=bool) + + for class_index in range(self.num_class): + if self.num_gt_instances_per_class[class_index] == 0: + continue + + if not self.scores_per_class[class_index]: + scores = np.array([], dtype=float) + tp_fp_labels = np.array([], dtype=bool) + else: + scores = np.concatenate(self.scores_per_class[class_index]) + tp_fp_labels = np.concatenate( + self.tp_fp_labels_per_class[class_index]) + if self.use_weighted_mean_ap: + all_scores = np.append(all_scores, scores) + all_tp_fp_labels = np.append(all_tp_fp_labels, tp_fp_labels) + precision, recall = metrics.compute_precision_recall( + scores, + tp_fp_labels, + self.num_gt_instances_per_class[class_index], + ) + self.precisions_per_class.append(precision) + self.recalls_per_class.append(recall) + average_precision = metrics.compute_average_precision( + precision, recall) + self.average_precision_per_class[class_index] = average_precision + + self.corloc_per_class = metrics.compute_cor_loc( + self.num_gt_imgs_per_class, + self.num_images_correctly_detected_per_class, + ) + + if self.use_weighted_mean_ap: + num_gt_instances = np.sum(self.num_gt_instances_per_class) + precision, recall = metrics.compute_precision_recall( + all_scores, all_tp_fp_labels, num_gt_instances) + mean_ap = metrics.compute_average_precision(precision, recall) + else: + mean_ap = np.nanmean(self.average_precision_per_class) + mean_corloc = np.nanmean(self.corloc_per_class) + return ObjectDetectionEvalMetrics( + self.average_precision_per_class, + mean_ap, + self.precisions_per_class, + self.recalls_per_class, + self.corloc_per_class, + mean_corloc, + ) diff --git a/paddlevideo/metrics/ava_evaluation/per_image_evaluation.py b/paddlevideo/metrics/ava_evaluation/per_image_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..3013ae7ce2fb84ec3733474fdeff61a8d3ba20a8 --- /dev/null +++ b/paddlevideo/metrics/ava_evaluation/per_image_evaluation.py @@ -0,0 +1,452 @@ +# Copyright 2017 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================= +"""Evaluate Object Detection result on a single image. + +Annotate each detected result as true positives or false positive according to +a predefined IOU ratio. Non Maximum Supression is used by default. Multi class +detection is supported by default. Based on the settings, per image evaluation +is either performed on boxes or on object masks. +""" + +import numpy as np + +from . import np_box_list, np_box_ops + + +class PerImageEvaluation: + """Evaluate detection result of a single image.""" + + def __init__(self, num_groundtruth_classes, matching_iou_threshold=0.5): + """Initialized PerImageEvaluation by evaluation parameters. + + Args: + num_groundtruth_classes: Number of ground truth object classes + matching_iou_threshold: A ratio of area intersection to union, + which is the threshold to consider whether a detection is true + positive or not + """ + self.matching_iou_threshold = matching_iou_threshold + self.num_groundtruth_classes = num_groundtruth_classes + + def compute_object_detection_metrics( + self, + detected_boxes, + detected_scores, + detected_class_labels, + groundtruth_boxes, + groundtruth_class_labels, + groundtruth_is_difficult_list, + groundtruth_is_group_of_list, + detected_masks=None, + groundtruth_masks=None, + ): + """Evaluates detections as being tp, fp or ignored from a single image. + + The evaluation is done in two stages: + 1. All detections are matched to non group-of boxes; true positives + are determined and detections matched to difficult boxes are + ignored. + 2. Detections that are determined as false positives are matched + against group-of boxes and ignored if matched. + + Args: + detected_boxes: A float numpy array of shape [N, 4], representing N + regions of detected object regions. + Each row is of the format [y_min, x_min, y_max, x_max] + detected_scores: A float numpy array of shape [N, 1], representing + the confidence scores of the detected N object instances. + detected_class_labels: A integer numpy array of shape [N, 1], + repreneting the class labels of the detected N object + instances. + groundtruth_boxes: A float numpy array of shape [M, 4], + representing M regions of object instances in ground truth + groundtruth_class_labels: An integer numpy array of shape [M, 1], + representing M class labels of object instances in ground truth + groundtruth_is_difficult_list: A boolean numpy array of length M + denoting whether a ground truth box is a difficult instance or + not + groundtruth_is_group_of_list: A boolean numpy array of length M + denoting whether a ground truth box has group-of tag + detected_masks: (optional) A uint8 numpy array of shape + [N, height, width]. If not None, the metrics will be computed + based on masks. + groundtruth_masks: (optional) A uint8 numpy array of shape + [M, height, width]. + + Returns: + scores: A list of C float numpy arrays. Each numpy array is of + shape [K, 1], representing K scores detected with object class + label c + tp_fp_labels: A list of C boolean numpy arrays. Each numpy array + is of shape [K, 1], representing K True/False positive label of + object instances detected with class label c + """ + ( + detected_boxes, + detected_scores, + detected_class_labels, + detected_masks, + ) = self._remove_invalid_boxes( + detected_boxes, + detected_scores, + detected_class_labels, + detected_masks, + ) + scores, tp_fp_labels = self._compute_tp_fp( + detected_boxes=detected_boxes, + detected_scores=detected_scores, + detected_class_labels=detected_class_labels, + groundtruth_boxes=groundtruth_boxes, + groundtruth_class_labels=groundtruth_class_labels, + groundtruth_is_difficult_list=groundtruth_is_difficult_list, + groundtruth_is_group_of_list=groundtruth_is_group_of_list, + detected_masks=detected_masks, + groundtruth_masks=groundtruth_masks, + ) + + return scores, tp_fp_labels + + def _compute_tp_fp( + self, + detected_boxes, + detected_scores, + detected_class_labels, + groundtruth_boxes, + groundtruth_class_labels, + groundtruth_is_difficult_list, + groundtruth_is_group_of_list, + detected_masks=None, + groundtruth_masks=None, + ): + """Labels true/false positives of detections of an image across all + classes. + + Args: + detected_boxes: A float numpy array of shape [N, 4], representing N + regions of detected object regions. + Each row is of the format [y_min, x_min, y_max, x_max] + detected_scores: A float numpy array of shape [N, 1], representing + the confidence scores of the detected N object instances. + detected_class_labels: A integer numpy array of shape [N, 1], + repreneting the class labels of the detected N object + instances. + groundtruth_boxes: A float numpy array of shape [M, 4], + representing M regions of object instances in ground truth + groundtruth_class_labels: An integer numpy array of shape [M, 1], + representing M class labels of object instances in ground truth + groundtruth_is_difficult_list: A boolean numpy array of length M + denoting whether a ground truth box is a difficult instance or + not + groundtruth_is_group_of_list: A boolean numpy array of length M + denoting whether a ground truth box has group-of tag + detected_masks: (optional) A np.uint8 numpy array of shape + [N, height, width]. If not None, the scores will be computed + based on masks. + groundtruth_masks: (optional) A np.uint8 numpy array of shape + [M, height, width]. + + Returns: + result_scores: A list of float numpy arrays. Each numpy array is of + shape [K, 1], representing K scores detected with object class + label c + result_tp_fp_labels: A list of boolean numpy array. Each numpy + array is of shape [K, 1], representing K True/False positive + label of object instances detected with class label c + + Raises: + ValueError: If detected masks is not None but groundtruth masks are + None, or the other way around. + """ + if detected_masks is not None and groundtruth_masks is None: + raise ValueError( + 'Detected masks is available but groundtruth masks is not.') + if detected_masks is None and groundtruth_masks is not None: + raise ValueError( + 'Groundtruth masks is available but detected masks is not.') + + result_scores = [] + result_tp_fp_labels = [] + for i in range(self.num_groundtruth_classes): + groundtruth_is_difficult_list_at_ith_class = ( + groundtruth_is_difficult_list[groundtruth_class_labels == i]) + groundtruth_is_group_of_list_at_ith_class = ( + groundtruth_is_group_of_list[groundtruth_class_labels == i]) + ( + gt_boxes_at_ith_class, + gt_masks_at_ith_class, + detected_boxes_at_ith_class, + detected_scores_at_ith_class, + detected_masks_at_ith_class, + ) = self._get_ith_class_arrays(detected_boxes, detected_scores, + detected_masks, + detected_class_labels, + groundtruth_boxes, + groundtruth_masks, + groundtruth_class_labels, i) + scores, tp_fp_labels = self._compute_tp_fp_for_single_class( + detected_boxes=detected_boxes_at_ith_class, + detected_scores=detected_scores_at_ith_class, + groundtruth_boxes=gt_boxes_at_ith_class, + groundtruth_is_difficult_list=( + groundtruth_is_difficult_list_at_ith_class), + groundtruth_is_group_of_list=( + groundtruth_is_group_of_list_at_ith_class), + detected_masks=detected_masks_at_ith_class, + groundtruth_masks=gt_masks_at_ith_class, + ) + result_scores.append(scores) + result_tp_fp_labels.append(tp_fp_labels) + return result_scores, result_tp_fp_labels + + def _get_overlaps_and_scores_box_mode( + self, + detected_boxes, + detected_scores, + groundtruth_boxes, + groundtruth_is_group_of_list, + ): + """Computes overlaps and scores between detected and groudntruth boxes. + + Args: + detected_boxes: A numpy array of shape [N, 4] representing detected + box coordinates + detected_scores: A 1-d numpy array of length N representing + classification score + groundtruth_boxes: A numpy array of shape [M, 4] representing + ground truth box coordinates + groundtruth_is_group_of_list: A boolean numpy array of length M + denoting whether a ground truth box has group-of tag. If a + groundtruth box is group-of box, every detection matching this + box is ignored. + + Returns: + iou: A float numpy array of size [num_detected_boxes, + num_gt_boxes]. If gt_non_group_of_boxlist.num_boxes() == 0 it + will be None. + ioa: A float numpy array of size [num_detected_boxes, + num_gt_boxes]. If gt_group_of_boxlist.num_boxes() == 0 it will + be None. + scores: The score of the detected boxlist. + num_boxes: Number of non-maximum suppressed detected boxes. + """ + detected_boxlist = np_box_list.BoxList(detected_boxes) + detected_boxlist.add_field('scores', detected_scores) + gt_non_group_of_boxlist = np_box_list.BoxList( + groundtruth_boxes[~groundtruth_is_group_of_list]) + + iou = np_box_ops.iou(detected_boxlist.get(), + gt_non_group_of_boxlist.get()) + scores = detected_boxlist.get_field('scores') + num_boxes = detected_boxlist.num_boxes() + return iou, None, scores, num_boxes + + def _compute_tp_fp_for_single_class( + self, + detected_boxes, + detected_scores, + groundtruth_boxes, + groundtruth_is_difficult_list, + groundtruth_is_group_of_list, + detected_masks=None, + groundtruth_masks=None, + ): + """Labels boxes detected with the same class from the same image as + tp/fp. + + Args: + detected_boxes: A numpy array of shape [N, 4] representing detected + box coordinates + detected_scores: A 1-d numpy array of length N representing + classification score + groundtruth_boxes: A numpy array of shape [M, 4] representing + groundtruth box coordinates + groundtruth_is_difficult_list: A boolean numpy array of length M + denoting whether a ground truth box is a difficult instance or + not. If a groundtruth box is difficult, every detection + matching this box is ignored. + groundtruth_is_group_of_list: A boolean numpy array of length M + denoting whether a ground truth box has group-of tag. If a + groundtruth box is group-of box, every detection matching this + box is ignored. + detected_masks: (optional) A uint8 numpy array of shape + [N, height, width]. If not None, the scores will be computed + based on masks. + groundtruth_masks: (optional) A uint8 numpy array of shape + [M, height, width]. + + Returns: + Two arrays of the same size, containing all boxes that were + evaluated as being true positives or false positives; if a box + matched to a difficult box or to a group-of box, it is ignored. + + scores: A numpy array representing the detection scores. + tp_fp_labels: a boolean numpy array indicating whether a detection + is a true positive. + """ + if detected_boxes.size == 0: + return np.array([], dtype=float), np.array([], dtype=bool) + + ( + iou, + _, + scores, + num_detected_boxes, + ) = self._get_overlaps_and_scores_box_mode( + detected_boxes=detected_boxes, + detected_scores=detected_scores, + groundtruth_boxes=groundtruth_boxes, + groundtruth_is_group_of_list=groundtruth_is_group_of_list, + ) + + if groundtruth_boxes.size == 0: + return scores, np.zeros(num_detected_boxes, dtype=bool) + + tp_fp_labels = np.zeros(num_detected_boxes, dtype=bool) + is_matched_to_difficult_box = np.zeros(num_detected_boxes, dtype=bool) + is_matched_to_group_of_box = np.zeros(num_detected_boxes, dtype=bool) + + # The evaluation is done in two stages: + # 1. All detections are matched to non group-of boxes; true positives + # are determined and detections matched to difficult boxes are + # ignored. + # 2. Detections that are determined as false positives are matched + # against group-of boxes and ignored if matched. + + # Tp-fp evaluation for non-group of boxes (if any). + if iou.shape[1] > 0: + groundtruth_nongroup_of_is_difficult_list = ( + groundtruth_is_difficult_list[~groundtruth_is_group_of_list]) + max_overlap_gt_ids = np.argmax(iou, axis=1) + is_gt_box_detected = np.zeros(iou.shape[1], dtype=bool) + for i in range(num_detected_boxes): + gt_id = max_overlap_gt_ids[i] + if iou[i, gt_id] >= self.matching_iou_threshold: + if not groundtruth_nongroup_of_is_difficult_list[gt_id]: + if not is_gt_box_detected[gt_id]: + tp_fp_labels[i] = True + is_gt_box_detected[gt_id] = True + else: + is_matched_to_difficult_box[i] = True + + return ( + scores[~is_matched_to_difficult_box & ~is_matched_to_group_of_box], + tp_fp_labels[~is_matched_to_difficult_box + & ~is_matched_to_group_of_box], + ) + + def _get_ith_class_arrays( + self, + detected_boxes, + detected_scores, + detected_masks, + detected_class_labels, + groundtruth_boxes, + groundtruth_masks, + groundtruth_class_labels, + class_index, + ): + """Returns numpy arrays belonging to class with index `class_index`. + + Args: + detected_boxes: A numpy array containing detected boxes. + detected_scores: A numpy array containing detected scores. + detected_masks: A numpy array containing detected masks. + detected_class_labels: A numpy array containing detected class + labels. + groundtruth_boxes: A numpy array containing groundtruth boxes. + groundtruth_masks: A numpy array containing groundtruth masks. + groundtruth_class_labels: A numpy array containing groundtruth + class labels. + class_index: An integer index. + + Returns: + gt_boxes_at_ith_class: A numpy array containing groundtruth boxes + labeled as ith class. + gt_masks_at_ith_class: A numpy array containing groundtruth masks + labeled as ith class. + detected_boxes_at_ith_class: A numpy array containing detected + boxes corresponding to the ith class. + detected_scores_at_ith_class: A numpy array containing detected + scores corresponding to the ith class. + detected_masks_at_ith_class: A numpy array containing detected + masks corresponding to the ith class. + """ + selected_groundtruth = groundtruth_class_labels == class_index + gt_boxes_at_ith_class = groundtruth_boxes[selected_groundtruth] + if groundtruth_masks is not None: + gt_masks_at_ith_class = groundtruth_masks[selected_groundtruth] + else: + gt_masks_at_ith_class = None + selected_detections = detected_class_labels == class_index + detected_boxes_at_ith_class = detected_boxes[selected_detections] + detected_scores_at_ith_class = detected_scores[selected_detections] + if detected_masks is not None: + detected_masks_at_ith_class = detected_masks[selected_detections] + else: + detected_masks_at_ith_class = None + return ( + gt_boxes_at_ith_class, + gt_masks_at_ith_class, + detected_boxes_at_ith_class, + detected_scores_at_ith_class, + detected_masks_at_ith_class, + ) + + def _remove_invalid_boxes( + self, + detected_boxes, + detected_scores, + detected_class_labels, + detected_masks=None, + ): + """Removes entries with invalid boxes. + + A box is invalid if either its xmax is smaller than its xmin, or its + ymax is smaller than its ymin. + + Args: + detected_boxes: A float numpy array of size [num_boxes, 4] + containing box coordinates in [ymin, xmin, ymax, xmax] format. + detected_scores: A float numpy array of size [num_boxes]. + detected_class_labels: A int32 numpy array of size [num_boxes]. + detected_masks: A uint8 numpy array of size + [num_boxes, height, width]. + + Returns: + valid_detected_boxes: A float numpy array of size + [num_valid_boxes, 4] containing box coordinates in + [ymin, xmin, ymax, xmax] format. + valid_detected_scores: A float numpy array of size + [num_valid_boxes]. + valid_detected_class_labels: A int32 numpy array of size + [num_valid_boxes]. + valid_detected_masks: A uint8 numpy array of size + [num_valid_boxes, height, width]. + """ + valid_indices = np.logical_and( + detected_boxes[:, 0] < detected_boxes[:, 2], + detected_boxes[:, 1] < detected_boxes[:, 3], + ) + detected_boxes = detected_boxes[valid_indices] + detected_scores = detected_scores[valid_indices] + detected_class_labels = detected_class_labels[valid_indices] + if detected_masks is not None: + detected_masks = detected_masks[valid_indices] + return [ + detected_boxes, + detected_scores, + detected_class_labels, + detected_masks, + ] diff --git a/paddlevideo/metrics/ava_evaluation/standard_fields.py b/paddlevideo/metrics/ava_evaluation/standard_fields.py new file mode 100644 index 0000000000000000000000000000000000000000..8edf46d0816ab34458e5587b39b735c977f71572 --- /dev/null +++ b/paddlevideo/metrics/ava_evaluation/standard_fields.py @@ -0,0 +1,115 @@ +# Copyright 2017 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================= +"""Contains classes specifying naming conventions used for object detection. + +Specifies: + InputDataFields: standard fields used by reader/preprocessor/batcher. + DetectionResultFields: standard fields returned by object detector. +""" + + +class InputDataFields: + """Names for the input tensors. + + Holds the standard data field names to use for identifying input tensors. + This should be used by the decoder to identify keys for the returned + tensor_dict containing input tensors. And it should be used by the model to + identify the tensors it needs. + + Attributes: + image: image. + original_image: image in the original input size. + key: unique key corresponding to image. + source_id: source of the original image. + filename: original filename of the dataset (without common path). + groundtruth_image_classes: image-level class labels. + groundtruth_boxes: coordinates of the ground truth boxes in the image. + groundtruth_classes: box-level class labels. + groundtruth_label_types: box-level label types (e.g. explicit + negative). + groundtruth_is_crowd: [DEPRECATED, use groundtruth_group_of instead] + is the groundtruth a single object or a crowd. + groundtruth_area: area of a groundtruth segment. + groundtruth_difficult: is a `difficult` object + groundtruth_group_of: is a `group_of` objects, e.g. multiple objects of + the same class, forming a connected group, where instances are + heavily occluding each other. + proposal_boxes: coordinates of object proposal boxes. + proposal_objectness: objectness score of each proposal. + groundtruth_instance_masks: ground truth instance masks. + groundtruth_instance_boundaries: ground truth instance boundaries. + groundtruth_instance_classes: instance mask-level class labels. + groundtruth_keypoints: ground truth keypoints. + groundtruth_keypoint_visibilities: ground truth keypoint visibilities. + groundtruth_label_scores: groundtruth label scores. + groundtruth_weights: groundtruth weight factor for bounding boxes. + num_groundtruth_boxes: number of groundtruth boxes. + true_image_shapes: true shapes of images in the resized images, as + resized images can be padded with zeros. + """ + + image = 'image' + original_image = 'original_image' + key = 'key' + source_id = 'source_id' + filename = 'filename' + groundtruth_image_classes = 'groundtruth_image_classes' + groundtruth_boxes = 'groundtruth_boxes' + groundtruth_classes = 'groundtruth_classes' + groundtruth_label_types = 'groundtruth_label_types' + groundtruth_is_crowd = 'groundtruth_is_crowd' + groundtruth_area = 'groundtruth_area' + groundtruth_difficult = 'groundtruth_difficult' + groundtruth_group_of = 'groundtruth_group_of' + proposal_boxes = 'proposal_boxes' + proposal_objectness = 'proposal_objectness' + groundtruth_instance_masks = 'groundtruth_instance_masks' + groundtruth_instance_boundaries = 'groundtruth_instance_boundaries' + groundtruth_instance_classes = 'groundtruth_instance_classes' + groundtruth_keypoints = 'groundtruth_keypoints' + groundtruth_keypoint_visibilities = 'groundtruth_keypoint_visibilities' + groundtruth_label_scores = 'groundtruth_label_scores' + groundtruth_weights = 'groundtruth_weights' + num_groundtruth_boxes = 'num_groundtruth_boxes' + true_image_shape = 'true_image_shape' + + +class DetectionResultFields: + """Naming conventions for storing the output of the detector. + + Attributes: + source_id: source of the original image. + key: unique key corresponding to image. + detection_boxes: coordinates of the detection boxes in the image. + detection_scores: detection scores for the detection boxes in the + image. + detection_classes: detection-level class labels. + detection_masks: contains a segmentation mask for each detection box. + detection_boundaries: contains an object boundary for each detection + box. + detection_keypoints: contains detection keypoints for each detection + box. + num_detections: number of detections in the batch. + """ + + source_id = 'source_id' + key = 'key' + detection_boxes = 'detection_boxes' + detection_scores = 'detection_scores' + detection_classes = 'detection_classes' + detection_masks = 'detection_masks' + detection_boundaries = 'detection_boundaries' + detection_keypoints = 'detection_keypoints' + num_detections = 'num_detections' diff --git a/paddlevideo/metrics/ava_metric.py b/paddlevideo/metrics/ava_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..4ee21bdb6e8d8c10fb9cd579d02677ee027d7683 --- /dev/null +++ b/paddlevideo/metrics/ava_metric.py @@ -0,0 +1,94 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np +import paddle +from paddle.hapi.model import _all_gather +from collections import OrderedDict +from paddlevideo.utils import get_logger, load, log_batch, AverageMeter +from .registry import METRIC +from .base import BaseMetric +import time +from datetime import datetime +from .ava_utils import ava_evaluate_results + +logger = get_logger("paddlevideo") +""" An example for metrics class. + MultiCropMetric for slowfast. +""" + + +@METRIC.register +class AVAMetric(BaseMetric): + + def __init__(self, + data_size, + batch_size, + file_path, + exclude_file, + label_file, + custom_classes, + log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + + self.file_path = file_path + self.exclude_file = exclude_file + self.label_file = label_file + self.custom_classes = custom_classes + + self.results = [] + + record_list = [ + ("loss", AverageMeter('loss', '7.5f')), + ("recall@thr=0.5", AverageMeter("recall@thr=0.5", '.5f')), + ("prec@thr=0.5", AverageMeter("prec@thr=0.5", '.5f')), + ("recall@top3", AverageMeter("recall@top3", '.5f')), + ("prec@top3", AverageMeter("prec@top3", '.5f')), + ("recall@top5", AverageMeter("recall@top5", '.5f')), + ("prec@top5", AverageMeter("prec@top5", '.5f')), + ("mAP@0.5IOU", AverageMeter("mAP@0.5IOU", '.5f')), + ("batch_time", AverageMeter('batch_cost', '.5f')), + ("reader_time", AverageMeter('reader_cost', '.5f')), + ] + + self.record_list = OrderedDict(record_list) + + self.tic = time.time() + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + + self.results.extend(outputs) + self.record_list['batch_time'].update(time.time() - self.tic) + tic = time.time() + ips = "ips: {:.5f} instance/sec.".format( + self.batch_size / self.record_list["batch_time"].val) + log_batch(self.record_list, batch_id, 0, 0, "test", ips) + + def set_dataset_info(self, info, dataset_len): + self.info = info + self.dataset_len = dataset_len + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + test_res = ava_evaluate_results(self.info, self.dataset_len, + self.results, None, self.label_file, + self.file_path, self.exclude_file) + + for name, value in test_res.items(): + self.record_list[name].update(value, self.batch_size) + + return self.record_list diff --git a/paddlevideo/metrics/ava_utils.py b/paddlevideo/metrics/ava_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b127267ede788f1dfa10cb17fc9523e55fdec2c0 --- /dev/null +++ b/paddlevideo/metrics/ava_utils.py @@ -0,0 +1,394 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import csv +import heapq +import logging +import time +from collections import defaultdict +from .ava_evaluation import object_detection_evaluation as det_eval +from .ava_evaluation import standard_fields +from .recall import eval_recalls +import shutil +import pickle +import time +import os +import os.path as osp +from paddlevideo.utils import get_logger, get_dist_info +import paddle.distributed as dist +import sys +import numpy as np +from pathlib import Path +from datetime import datetime +import paddle + + +def det2csv(info, dataset_len, results, custom_classes): + csv_results = [] + for idx in range(dataset_len): + video_id = info[idx]['video_id'] + timestamp = info[idx]['timestamp'] + + result = results[idx] + for label, _ in enumerate(result): + for bbox in result[label]: + if type(bbox) == paddle.Tensor: + bbox = bbox.numpy() + + bbox_ = tuple(bbox.tolist()) + if custom_classes is not None: + actual_label = custom_classes[label + 1] + else: + actual_label = label + 1 + csv_results.append(( + video_id, + timestamp, + ) + bbox_[:4] + (actual_label, ) + bbox_[4:]) + return csv_results + + +# results is organized by class +def results2csv(info, dataset_len, results, out_file, custom_classes=None): + if isinstance(results[0], list): + csv_results = det2csv(info, dataset_len, results, custom_classes) + + # save space for float + def tostr(item): + if isinstance(item, float): + return f'{item:.3f}' + return str(item) + + with open(out_file, 'w') as f: + for csv_result in csv_results: + f.write(','.join(map(lambda x: tostr(x), csv_result))) + f.write('\n') + + +def print_time(message, start): + print('==> %g seconds to %s' % (time.time() - start, message)) + + +def make_image_key(video_id, timestamp): + """Returns a unique identifier for a video id & timestamp.""" + return f'{video_id},{int(timestamp):04d}' + + +def read_csv(csv_file, class_whitelist=None, capacity=0): + """Loads boxes and class labels from a CSV file in the AVA format. + + CSV file format described at https://research.google.com/ava/download.html. + + Args: + csv_file: A file object. + class_whitelist: If provided, boxes corresponding to (integer) class + labels not in this set are skipped. + capacity: Maximum number of labeled boxes allowed for each example. + Default is 0 where there is no limit. + + Returns: + boxes: A dictionary mapping each unique image key (string) to a list of + boxes, given as coordinates [y1, x1, y2, x2]. + labels: A dictionary mapping each unique image key (string) to a list + of integer class lables, matching the corresponding box in `boxes`. + scores: A dictionary mapping each unique image key (string) to a list + of score values lables, matching the corresponding label in `labels`. + If scores are not provided in the csv, then they will default to 1.0. + """ + start = time.time() + entries = defaultdict(list) + boxes = defaultdict(list) + labels = defaultdict(list) + scores = defaultdict(list) + reader = csv.reader(csv_file) + for row in reader: + assert len(row) in [7, 8], 'Wrong number of columns: ' + row + image_key = make_image_key(row[0], row[1]) + x1, y1, x2, y2 = [float(n) for n in row[2:6]] + action_id = int(row[6]) + if class_whitelist and action_id not in class_whitelist: + continue + + score = 1.0 + if len(row) == 8: + score = float(row[7]) + if capacity < 1 or len(entries[image_key]) < capacity: + heapq.heappush(entries[image_key], + (score, action_id, y1, x1, y2, x2)) + elif score > entries[image_key][0][0]: + heapq.heapreplace(entries[image_key], + (score, action_id, y1, x1, y2, x2)) + for image_key in entries: + # Evaluation API assumes boxes with descending scores + entry = sorted(entries[image_key], key=lambda tup: -tup[0]) + for item in entry: + score, action_id, y1, x1, y2, x2 = item + boxes[image_key].append([y1, x1, y2, x2]) + labels[image_key].append(action_id) + scores[image_key].append(score) + print_time('read file ' + csv_file.name, start) + return boxes, labels, scores + + +def read_exclusions(exclusions_file): + """Reads a CSV file of excluded timestamps. + + Args: + exclusions_file: A file object containing a csv of video-id,timestamp. + + Returns: + A set of strings containing excluded image keys, e.g. + "aaaaaaaaaaa,0904", + or an empty set if exclusions file is None. + """ + excluded = set() + if exclusions_file: + reader = csv.reader(exclusions_file) + for row in reader: + assert len(row) == 2, 'Expected only 2 columns, got: ' + row + excluded.add(make_image_key(row[0], row[1])) + return excluded + + +def read_labelmap(labelmap_file): + """Reads a labelmap without the dependency on protocol buffers. + + Args: + labelmap_file: A file object containing a label map protocol buffer. + + Returns: + labelmap: The label map in the form used by the + object_detection_evaluation + module - a list of {"id": integer, "name": classname } dicts. + class_ids: A set containing all of the valid class id integers. + """ + labelmap = [] + class_ids = set() + name = '' + class_id = '' + for line in labelmap_file: + if line.startswith(' name:'): + name = line.split('"')[1] + elif line.startswith(' id:') or line.startswith(' label_id:'): + class_id = int(line.strip().split(' ')[-1]) + labelmap.append({'id': class_id, 'name': name}) + class_ids.add(class_id) + return labelmap, class_ids + + +# Seems there is at most 100 detections for each image +def ava_eval(result_file, + result_type, + label_file, + ann_file, + exclude_file, + max_dets=(100, ), + verbose=True, + custom_classes=None): + + assert result_type in ['mAP'] + start = time.time() + categories, class_whitelist = read_labelmap(open(label_file)) + + if custom_classes is not None: + custom_classes = custom_classes[1:] + assert set(custom_classes).issubset(set(class_whitelist)) + class_whitelist = custom_classes + categories = [cat for cat in categories if cat['id'] in custom_classes] + + # loading gt, do not need gt score + gt_boxes, gt_labels, _ = read_csv(open(ann_file), class_whitelist, 0) + if verbose: + print_time('Reading detection results', start) + + if exclude_file is not None: + excluded_keys = read_exclusions(open(exclude_file)) + else: + excluded_keys = list() + + start = time.time() + boxes, labels, scores = read_csv(open(result_file), class_whitelist, 0) + if verbose: + print_time('Reading detection results', start) + + if result_type == 'proposal': + gts = [ + np.array(gt_boxes[image_key], dtype=float) for image_key in gt_boxes + ] + proposals = [] + for image_key in gt_boxes: + if image_key in boxes: + proposals.append( + np.concatenate( + (np.array(boxes[image_key], dtype=float), + np.array(scores[image_key], dtype=float)[:, None]), + axis=1)) + else: + # if no corresponding proposal, add a fake one + proposals.append(np.array([0, 0, 1, 1, 1])) + + # Proposals used here are with scores + recalls = eval_recalls(gts, proposals, np.array(max_dets), + np.arange(0.5, 0.96, 0.05)) + ar = recalls.mean(axis=1) + ret = {} + for i, num in enumerate(max_dets): + print(f'Recall@0.5@{num}\t={recalls[i, 0]:.4f}') + print(f'AR@{num}\t={ar[i]:.4f}') + ret[f'Recall@0.5@{num}'] = recalls[i, 0] + ret[f'AR@{num}'] = ar[i] + return ret + + if result_type == 'mAP': + pascal_evaluator = det_eval.PascalDetectionEvaluator(categories) + + start = time.time() + for image_key in gt_boxes: + if verbose and image_key in excluded_keys: + logging.info( + 'Found excluded timestamp in detections: %s.' + 'It will be ignored.', image_key) + continue + pascal_evaluator.add_single_ground_truth_image_info( + image_key, { + standard_fields.InputDataFields.groundtruth_boxes: + np.array(gt_boxes[image_key], dtype=float), + standard_fields.InputDataFields.groundtruth_classes: + np.array(gt_labels[image_key], dtype=int), + standard_fields.InputDataFields.groundtruth_difficult: + np.zeros(len(gt_boxes[image_key]), dtype=bool) + }) + if verbose: + print_time('Convert groundtruth', start) + + start = time.time() + for image_key in boxes: + if verbose and image_key in excluded_keys: + logging.info( + 'Found excluded timestamp in detections: %s.' + 'It will be ignored.', image_key) + continue + pascal_evaluator.add_single_detected_image_info( + image_key, { + standard_fields.DetectionResultFields.detection_boxes: + np.array(boxes[image_key], dtype=float), + standard_fields.DetectionResultFields.detection_classes: + np.array(labels[image_key], dtype=int), + standard_fields.DetectionResultFields.detection_scores: + np.array(scores[image_key], dtype=float) + }) + if verbose: + print_time('convert detections', start) + + start = time.time() + metrics = pascal_evaluator.evaluate() + if verbose: + print_time('run_evaluator', start) + for display_name in metrics: + print(f'{display_name}=\t{metrics[display_name]}') + ret = { + display_name: metrics[display_name] + for display_name in metrics if 'ByCategory' not in display_name + } + return ret + + +def mkdir_or_exist(dir_name, mode=0o777): + if dir_name == '': + return + dir_name = osp.expanduser(dir_name) + os.makedirs(dir_name, mode=mode, exist_ok=True) + + +def dump_to_fileobj(obj, file, **kwargs): + kwargs.setdefault('protocol', 2) + pickle.dump(obj, file, **kwargs) + + +def dump_to_path(obj, filepath, mode='wb'): + with open(filepath, mode) as f: + dump_to_fileobj(obj, f) + + +def load_from_fileobj(file, **kwargs): + return pickle.load(file, **kwargs) + + +def load_from_path(filepath, mode='rb'): + with open(filepath, mode) as f: + return load_from_fileobj(f) + + +def collect_results_cpu(result_part, size): + """Collect results in cpu mode. + It saves the results on different gpus to 'tmpdir' and collects + them by the rank 0 worker. + """ + tmpdir = osp.join('./', 'collect_results_cpu') + #1. load results of all parts from tmp dir + mkdir_or_exist(tmpdir) + rank, world_size = get_dist_info() + dump_to_path(result_part, osp.join(tmpdir, f'part_{rank}.pkl')) + dist.barrier() + if rank != 0: + return None + #2. collect all parts + while 1: + all_exist = True + for i in range(world_size): + part_file = osp.join(tmpdir, f'part_{i}.pkl') + if not Path(part_file).exists(): + all_exist = False + if all_exist: + break + else: + time.sleep(60) + time.sleep(120) + #3. load results of all parts from tmp dir + part_list = [] + for i in range(world_size): + part_file = osp.join(tmpdir, f'part_{i}.pkl') + part_list.append(load_from_path(part_file)) + #4. sort the results + ordered_results = [] + for res in zip(*part_list): + ordered_results.extend(list(res)) + ordered_results = ordered_results[: + size] #the dataloader may pad some samples + #5. remove results of all parts from tmp dir, avoid dump_file fail to tmp dir when dir not exists. + for i in range(world_size): + part_file = osp.join(tmpdir, f'part_{i}.pkl') + os.remove(part_file) + + return ordered_results + + +def ava_evaluate_results(info, dataset_len, results, custom_classes, label_file, + file_path, exclude_file): + # need to create a temp result file + time_now = datetime.now().strftime('%Y%m%d_%H%M%S') + temp_file = f'AVA_{time_now}_result.csv' + results2csv(info, dataset_len, results, temp_file) + ret = {} + eval_result = ava_eval( + temp_file, + 'mAP', + label_file, + file_path, #ann_file, + exclude_file, + custom_classes=custom_classes) + ret.update(eval_result) + + os.remove(temp_file) + + return ret diff --git a/paddlevideo/metrics/base.py b/paddlevideo/metrics/base.py new file mode 100644 index 0000000000000000000000000000000000000000..5c42b72883183f982aafa8d4337ae13483dbc23b --- /dev/null +++ b/paddlevideo/metrics/base.py @@ -0,0 +1,34 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from abc import abstractmethod +import numpy as np +import paddle +from paddlevideo.utils import get_dist_info + +from .registry import METRIC + + +class BaseMetric(object): + def __init__(self, data_size, batch_size, log_interval=1, **kwargs): + self.data_size = data_size + self.batch_size = batch_size + _, self.world_size = get_dist_info() + self.log_interval = log_interval + + @abstractmethod + def update(self): + raise NotImplemented + + @abstractmethod + def accumulate(self): + raise NotImplemented diff --git a/paddlevideo/metrics/bmn_metric.py b/paddlevideo/metrics/bmn_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..cc36283f98a8c66ab7e0f14e47086d341c5c77e2 --- /dev/null +++ b/paddlevideo/metrics/bmn_metric.py @@ -0,0 +1,304 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +import json +import numpy as np +import pandas as pd +import multiprocessing as mp + +from .registry import METRIC +from .base import BaseMetric +from .ActivityNet import ANETproposal +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +def iou_with_anchors(anchors_min, anchors_max, box_min, box_max): + """Compute jaccard score between a box and the anchors. + """ + len_anchors = anchors_max - anchors_min + int_xmin = np.maximum(anchors_min, box_min) + int_xmax = np.minimum(anchors_max, box_max) + inter_len = np.maximum(int_xmax - int_xmin, 0.) + union_len = len_anchors - inter_len + box_max - box_min + jaccard = np.divide(inter_len, union_len) + return jaccard + + +def boundary_choose(score_list): + """Choose start and end boundary from score. + """ + max_score = max(score_list) + mask_high = (score_list > max_score * 0.5) + score_list = list(score_list) + score_middle = np.array([0.0] + score_list + [0.0]) + score_front = np.array([0.0, 0.0] + score_list) + score_back = np.array(score_list + [0.0, 0.0]) + mask_peak = ((score_middle > score_front) & (score_middle > score_back)) + mask_peak = mask_peak[1:-1] + mask = (mask_high | mask_peak).astype('float32') + return mask + + +def soft_nms(df, alpha, t1, t2): + ''' + df: proposals generated by network; + alpha: alpha value of Gaussian decaying function; + t1, t2: threshold for soft nms. + ''' + df = df.sort_values(by="score", ascending=False) + tstart = list(df.xmin.values[:]) + tend = list(df.xmax.values[:]) + tscore = list(df.score.values[:]) + + rstart = [] + rend = [] + rscore = [] + + while len(tscore) > 1 and len(rscore) < 101: + max_index = tscore.index(max(tscore)) + tmp_iou_list = iou_with_anchors(np.array(tstart), np.array(tend), + tstart[max_index], tend[max_index]) + for idx in range(0, len(tscore)): + if idx != max_index: + tmp_iou = tmp_iou_list[idx] + tmp_width = tend[max_index] - tstart[max_index] + if tmp_iou > t1 + (t2 - t1) * tmp_width: + tscore[idx] = tscore[idx] * np.exp( + -np.square(tmp_iou) / alpha) + + rstart.append(tstart[max_index]) + rend.append(tend[max_index]) + rscore.append(tscore[max_index]) + tstart.pop(max_index) + tend.pop(max_index) + tscore.pop(max_index) + + newDf = pd.DataFrame() + newDf['score'] = rscore + newDf['xmin'] = rstart + newDf['xmax'] = rend + return newDf + + +@METRIC.register +class BMNMetric(BaseMetric): + """ + Metrics for BMN. Two Stages in this metric: + (1) Get test results using trained model, results will be saved in BMNMetric.result_path; + (2) Calculate metrics using results file from stage (1). + """ + + def __init__(self, + data_size, + batch_size, + tscale, + dscale, + file_path, + ground_truth_filename, + subset, + output_path, + result_path, + get_metrics=True, + log_interval=1): + """ + Init for BMN metrics. + Params: + get_metrics: whether to calculate AR@N and AUC metrics or not, default True. + """ + super().__init__(data_size, batch_size, log_interval) + assert self.batch_size == 1, " Now we just support batch_size==1 test" + assert self.world_size == 1, " Now we just support single-card test" + + self.tscale = tscale + self.dscale = dscale + self.file_path = file_path + self.ground_truth_filename = ground_truth_filename + self.subset = subset + self.output_path = output_path + self.result_path = result_path + self.get_metrics = get_metrics + + if not os.path.isdir(self.output_path): + os.makedirs(self.output_path) + if not os.path.isdir(self.result_path): + os.makedirs(self.result_path) + + self.video_dict, self.video_list = self.get_dataset_dict( + self.file_path, self.subset) + + def get_dataset_dict(self, file_path, subset): + annos = json.load(open(file_path)) + video_dict = {} + for video_name in annos.keys(): + video_subset = annos[video_name]["subset"] + if subset in video_subset: + video_dict[video_name] = annos[video_name] + video_list = list(video_dict.keys()) + video_list.sort() + return video_dict, video_list + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + fid = data[4].numpy() + pred_bm, pred_start, pred_end = outputs + pred_bm = pred_bm.numpy() + pred_start = pred_start[0].numpy() + pred_end = pred_end[0].numpy() + + snippet_xmins = [1.0 / self.tscale * i for i in range(self.tscale)] + snippet_xmaxs = [ + 1.0 / self.tscale * i for i in range(1, self.tscale + 1) + ] + cols = ["xmin", "xmax", "score"] + + video_name = self.video_list[fid[0]] + pred_bm = pred_bm[0, 0, :, :] * pred_bm[0, 1, :, :] + start_mask = boundary_choose(pred_start) + start_mask[0] = 1. + end_mask = boundary_choose(pred_end) + end_mask[-1] = 1. + score_vector_list = [] + for idx in range(self.dscale): + for jdx in range(self.tscale): + start_index = jdx + end_index = start_index + idx + if end_index < self.tscale and start_mask[ + start_index] == 1 and end_mask[end_index] == 1: + xmin = snippet_xmins[start_index] + xmax = snippet_xmaxs[end_index] + xmin_score = pred_start[start_index] + xmax_score = pred_end[end_index] + bm_score = pred_bm[idx, jdx] + conf_score = xmin_score * xmax_score * bm_score + score_vector_list.append([xmin, xmax, conf_score]) + + score_vector_list = np.stack(score_vector_list) + video_df = pd.DataFrame(score_vector_list, columns=cols) + video_df.to_csv(os.path.join(self.output_path, "%s.csv" % video_name), + index=False) + + if batch_id % self.log_interval == 0: + logger.info("Processing................ batch {}".format(batch_id)) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + # check clip index of each video + #Stage1 + self.bmn_post_processing(self.video_dict, self.subset, self.output_path, + self.result_path) + if self.get_metrics: + logger.info("[TEST] calculate metrics...") + #Stage2 + uniform_average_nr_proposals_valid, uniform_average_recall_valid, uniform_recall_valid = self.cal_metrics( + self.ground_truth_filename, + os.path.join(self.result_path, "bmn_results_validation.json"), + max_avg_nr_proposals=100, + tiou_thresholds=np.linspace(0.5, 0.95, 10), + subset='validation') + logger.info("AR@1; AR@5; AR@10; AR@100") + logger.info("%.02f %.02f %.02f %.02f" % + (100 * np.mean(uniform_recall_valid[:, 0]), + 100 * np.mean(uniform_recall_valid[:, 4]), + 100 * np.mean(uniform_recall_valid[:, 9]), + 100 * np.mean(uniform_recall_valid[:, -1]))) + + def bmn_post_processing(self, video_dict, subset, output_path, result_path): + video_list = list(video_dict.keys()) + global result_dict + result_dict = mp.Manager().dict() + pp_num = 12 + + num_videos = len(video_list) + num_videos_per_thread = int(num_videos / pp_num) + processes = [] + for tid in range(pp_num - 1): + tmp_video_list = video_list[tid * num_videos_per_thread:(tid + 1) * + num_videos_per_thread] + p = mp.Process(target=self.video_process, + args=(tmp_video_list, video_dict, output_path, + result_dict)) + p.start() + processes.append(p) + tmp_video_list = video_list[(pp_num - 1) * num_videos_per_thread:] + p = mp.Process(target=self.video_process, + args=(tmp_video_list, video_dict, output_path, + result_dict)) + p.start() + processes.append(p) + for p in processes: + p.join() + + result_dict = dict(result_dict) + output_dict = { + "version": "VERSION 1.3", + "results": result_dict, + "external_data": {} + } + outfile = open( + os.path.join(result_path, "bmn_results_%s.json" % subset), "w") + + # json.dump(output_dict, outfile) + # in case of file name in chinese + json.dump(output_dict, outfile, ensure_ascii=False) + outfile.close() + + def video_process(self, + video_list, + video_dict, + output_path, + result_dict, + snms_alpha=0.4, + snms_t1=0.55, + snms_t2=0.9): + + for video_name in video_list: + logger.info("Processing video........" + video_name) + df = pd.read_csv(os.path.join(output_path, video_name + ".csv")) + if len(df) > 1: + df = soft_nms(df, snms_alpha, snms_t1, snms_t2) + + video_duration = video_dict[video_name]["duration_second"] + proposal_list = [] + for idx in range(min(100, len(df))): + tmp_prop={"score":df.score.values[idx], \ + "segment":[max(0,df.xmin.values[idx])*video_duration, \ + min(1,df.xmax.values[idx])*video_duration]} + proposal_list.append(tmp_prop) + + video_name = video_name[2:] if video_name[:2] == 'v_' else video_name + result_dict[video_name] = proposal_list + + def cal_metrics(self, + ground_truth_filename, + proposal_filename, + max_avg_nr_proposals=100, + tiou_thresholds=np.linspace(0.5, 0.95, 10), + subset='validation'): + + anet_proposal = ANETproposal(ground_truth_filename, + proposal_filename, + tiou_thresholds=tiou_thresholds, + max_avg_nr_proposals=max_avg_nr_proposals, + subset=subset, + verbose=True, + check_status=False) + anet_proposal.evaluate() + recall = anet_proposal.recall + average_recall = anet_proposal.avg_recall + average_nr_proposals = anet_proposal.proposals_per_video + + return (average_nr_proposals, average_recall, recall) diff --git a/paddlevideo/metrics/build.py b/paddlevideo/metrics/build.py new file mode 100644 index 0000000000000000000000000000000000000000..82e4b502611f3190e5f2c67d9e70fd2201be8233 --- /dev/null +++ b/paddlevideo/metrics/build.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .registry import METRIC +from ..utils import build + + +def build_metric(cfg): + return build(cfg, METRIC) diff --git a/paddlevideo/metrics/center_crop_metric.py b/paddlevideo/metrics/center_crop_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..b3bfb1b23f7ddd23f7e9563b7e8313776987f064 --- /dev/null +++ b/paddlevideo/metrics/center_crop_metric.py @@ -0,0 +1,58 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np +import paddle +from paddle.hapi.model import _all_gather + +from .registry import METRIC +from .base import BaseMetric +from paddlevideo.utils import get_logger +logger = get_logger("paddlevideo") + + +@METRIC.register +class CenterCropMetric(BaseMetric): + def __init__(self, data_size, batch_size, log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.top1 = [] + self.top5 = [] + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + labels = data[1] + + top1 = paddle.metric.accuracy(input=outputs, label=labels, k=1) + top5 = paddle.metric.accuracy(input=outputs, label=labels, k=5) + #NOTE(shipping): deal with multi cards validate + if self.world_size > 1: + top1 = paddle.distributed.all_reduce( + top1, op=paddle.distributed.ReduceOp.SUM) / self.world_size + top5 = paddle.distributed.all_reduce( + top5, op=paddle.distributed.ReduceOp.SUM) / self.world_size + + self.top1.append(top1.numpy()) + self.top5.append(top5.numpy()) + # preds ensemble + if batch_id % self.log_interval == 0: + logger.info("[TEST] Processing batch {}/{} ...".format( + batch_id, + self.data_size // (self.batch_size * self.world_size))) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + logger.info('[TEST] finished, avg_acc1= {}, avg_acc5= {} '.format( + np.mean(np.array(self.top1)), np.mean(np.array(self.top5)))) diff --git a/paddlevideo/metrics/center_crop_metric_MRI.py b/paddlevideo/metrics/center_crop_metric_MRI.py new file mode 100644 index 0000000000000000000000000000000000000000..843a9c36a3eca77feaf4463323d4a25129361e39 --- /dev/null +++ b/paddlevideo/metrics/center_crop_metric_MRI.py @@ -0,0 +1,62 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np +import paddle +from paddle.hapi.model import _all_gather + +from .registry import METRIC +from .base import BaseMetric +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@METRIC.register +class CenterCropMetric_MRI(BaseMetric): + def __init__(self, data_size, batch_size, log_interval=1, if_slowfast=0): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.top1 = [] + self.if_slowfast = if_slowfast + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + labels = data[1] + + if self.if_slowfast: + labels = data[2] + + top1 = paddle.metric.accuracy(input=outputs, label=labels, k=1) + #top5 = paddle.metric.accuracy(input=outputs, label=labels, k=5) + #NOTE(shipping): deal with multi cards validate + if self.world_size > 1: + top1 = paddle.distributed.all_reduce( + top1, op=paddle.distributed.ReduceOp.SUM) / self.world_size + # top5 = paddle.distributed.all_reduce( + # top5, op=paddle.distributed.ReduceOp.SUM) / self.world_size + + self.top1.append(top1.numpy()) + #self.top5.append(top5.numpy()) + # preds ensemble + if batch_id % self.log_interval == 0: + logger.info("[TEST] Processing batch {}/{} ...".format( + batch_id, + self.data_size // (self.batch_size * self.world_size))) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + logger.info('[TEST] finished, avg_acc1= {}'.format( + np.mean(np.array(self.top1)))) diff --git a/paddlevideo/metrics/depth_metric.py b/paddlevideo/metrics/depth_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..c160e16bae02397bacd8bc667421d29ddf402853 --- /dev/null +++ b/paddlevideo/metrics/depth_metric.py @@ -0,0 +1,77 @@ +import numpy as np +import paddle +from paddlevideo.utils import get_logger + +from .base import BaseMetric +from .registry import METRIC + +logger = get_logger("paddlevideo") + + +@METRIC.register +class DepthMetric(BaseMetric): + def __init__(self, data_size, batch_size, log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.abs_rel = [] + self.sq_rel = [] + self.rmse = [] + self.rmse_log = [] + self.a1 = [] + self.a2 = [] + self.a3 = [] + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + abs_rel, sq_rel, rmse, rmse_log, a1, a2, a3 = outputs['abs_rel'], outputs['sq_rel'], outputs['rmse'], \ + outputs['rmse_log'], outputs['a1'], outputs['a2'],outputs['a3'] + # preds ensemble + if self.world_size > 1: + abs_rel = paddle.distributed.all_reduce( + outputs['abs_rel'], + op=paddle.distributed.ReduceOp.SUM) / self.world_size + sq_rel = paddle.distributed.all_reduce( + outputs['sq_rel'], + op=paddle.distributed.ReduceOp.SUM) / self.world_size + rmse = paddle.distributed.all_reduce( + outputs['rmse'], + op=paddle.distributed.ReduceOp.SUM) / self.world_size + rmse_log = paddle.distributed.all_reduce( + outputs['rmse_log'], + op=paddle.distributed.ReduceOp.SUM) / self.world_size + a1 = paddle.distributed.all_reduce( + outputs['a1'], + op=paddle.distributed.ReduceOp.SUM) / self.world_size + a2 = paddle.distributed.all_reduce( + outputs['a2'], + op=paddle.distributed.ReduceOp.SUM) / self.world_size + a3 = paddle.distributed.all_reduce( + outputs['a3'], + op=paddle.distributed.ReduceOp.SUM) / self.world_size + + self.abs_rel.append(abs_rel) + self.sq_rel.append(sq_rel) + self.rmse.append(rmse) + self.rmse_log.append(rmse_log) + self.a1.append(a1) + self.a2.append(a2) + self.a3.append(a3) + if batch_id % self.log_interval == 0: + logger.info("[TEST] Processing batch {}/{} ...".format( + batch_id, + self.data_size // (self.batch_size * self.world_size))) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + logger.info( + '[TEST] finished, abs_rel= {}, sq_rel= {} , rmse= {}, rmse_log= {},' + 'a1= {}, a2= {}, a3= {}'.format(np.mean(np.array(self.abs_rel)), + np.mean(np.array(self.sq_rel)), + np.mean(np.array(self.rmse)), + np.mean(np.array(self.rmse_log)), + np.mean(np.array(self.a1)), + np.mean(np.array(self.a2)), + np.mean(np.array(self.a3)))) diff --git a/paddlevideo/metrics/msrvtt_metric.py b/paddlevideo/metrics/msrvtt_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..ec452f2e4d07f0b54de98330a2eca8fe3e6f7187 --- /dev/null +++ b/paddlevideo/metrics/msrvtt_metric.py @@ -0,0 +1,63 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np +import paddle +import paddle.nn.functional as F +from paddle.hapi.model import _all_gather + +from .registry import METRIC +from .base import BaseMetric +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@METRIC.register +class MSRVTTMetric(BaseMetric): + def __init__(self, data_size, batch_size, log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.score_matrix = np.zeros((data_size, data_size)) + self.target_matrix = np.zeros((data_size, data_size)) + self.rank_matrix = np.ones((data_size)) * data_size + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + target = data[-1] + cm_logit = outputs[-1] + + self.score_matrix[batch_id, :] = F.softmax( + cm_logit, axis=1)[:, 0].reshape([-1]).numpy() + self.target_matrix[batch_id, :] = target.reshape([-1]).numpy() + + rank = np.where((np.argsort(-self.score_matrix[batch_id]) == np.where( + self.target_matrix[batch_id] == 1)[0][0]) == 1)[0][0] + self.rank_matrix[batch_id] = rank + + rank_matrix_tmp = self.rank_matrix[:batch_id + 1] + r1 = 100.0 * np.sum(rank_matrix_tmp < 1) / len(rank_matrix_tmp) + r5 = 100.0 * np.sum(rank_matrix_tmp < 5) / len(rank_matrix_tmp) + r10 = 100.0 * np.sum(rank_matrix_tmp < 10) / len(rank_matrix_tmp) + + medr = np.floor(np.median(rank_matrix_tmp) + 1) + meanr = np.mean(rank_matrix_tmp) + 1 + logger.info( + "[{}] Final r1:{:.3f}, r5:{:.3f}, r10:{:.3f}, mder:{:.3f}, meanr:{:.3f}" + .format(batch_id, r1, r5, r10, medr, meanr)) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + logger.info("Eval Finished!") diff --git a/paddlevideo/metrics/multi_crop_metric.py b/paddlevideo/metrics/multi_crop_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..2cad6679d63f6d4bb092b5b9d222c41086bbc037 --- /dev/null +++ b/paddlevideo/metrics/multi_crop_metric.py @@ -0,0 +1,108 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np +import paddle +from paddle.hapi.model import _all_gather + +from paddlevideo.utils import get_logger +from .registry import METRIC +from .base import BaseMetric + +logger = get_logger("paddlevideo") +""" An example for metrics class. + MultiCropMetric for slowfast. +""" + + +@METRIC.register +class MultiCropMetric(BaseMetric): + def __init__(self, + data_size, + batch_size, + num_ensemble_views, + num_spatial_crops, + num_classes, + log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.num_ensemble_views = num_ensemble_views + self.num_spatial_crops = num_spatial_crops + self.num_classes = num_classes + + self.num_clips = self.num_ensemble_views * self.num_spatial_crops + num_videos = self.data_size // self.num_clips + self.video_preds = np.zeros((num_videos, self.num_classes)) + self.video_labels = np.zeros((num_videos, 1), dtype="int64") + self.clip_count = {} + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + labels = data[2] + clip_ids = data[3] + + # gather mulit card, results of following process in each card is the same. + if self.world_size > 1: + outputs = _all_gather(outputs, self.world_size) + labels = _all_gather(labels, self.world_size) + clip_ids = _all_gather(clip_ids, self.world_size) + + # to numpy + preds = outputs.numpy() + labels = labels.numpy().astype("int64") + clip_ids = clip_ids.numpy() + + # preds ensemble + for ind in range(preds.shape[0]): + vid_id = int(clip_ids[ind]) // self.num_clips + ts_idx = int(clip_ids[ind]) % self.num_clips + if vid_id not in self.clip_count: + self.clip_count[vid_id] = [] + if ts_idx in self.clip_count[vid_id]: + logger.info( + "[TEST] Passed!! read video {} clip index {} / {} repeatedly." + .format(vid_id, ts_idx, clip_ids[ind])) + else: + self.clip_count[vid_id].append(ts_idx) + self.video_preds[vid_id] += preds[ind] # ensemble method: sum + if self.video_labels[vid_id].sum() > 0: + assert self.video_labels[vid_id] == labels[ind] + self.video_labels[vid_id] = labels[ind] + if batch_id % self.log_interval == 0: + logger.info("[TEST] Processing batch {}/{} ...".format( + batch_id, + self.data_size // (self.batch_size * self.world_size))) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + # check clip index of each video + for key in self.clip_count.keys(): + if len(self.clip_count[key]) != self.num_clips or sum( + self.clip_count[key]) != self.num_clips * (self.num_clips - + 1) / 2: + logger.info( + "[TEST] Count Error!! video [{}] clip count [{}] not match number clips {}" + .format(key, self.clip_count[key], self.num_clips)) + + video_preds = paddle.to_tensor(self.video_preds) + video_labels = paddle.to_tensor(self.video_labels) + acc_top1 = paddle.metric.accuracy(input=video_preds, + label=video_labels, + k=1) + acc_top5 = paddle.metric.accuracy(input=video_preds, + label=video_labels, + k=5) + logger.info('[TEST] finished, avg_acc1= {}, avg_acc5= {} '.format( + acc_top1.numpy(), acc_top5.numpy())) diff --git a/paddlevideo/metrics/recall.py b/paddlevideo/metrics/recall.py new file mode 100644 index 0000000000000000000000000000000000000000..3612e2244d430c593a859694f8b2c7abfb63134f --- /dev/null +++ b/paddlevideo/metrics/recall.py @@ -0,0 +1,84 @@ +import numpy as np +import paddle + +def _recalls(all_ious, proposal_nums, thrs): + + img_num = all_ious.shape[0] + total_gt_num = sum([ious.shape[0] for ious in all_ious]) + + ious_ = np.zeros((proposal_nums.size, total_gt_num), dtype=np.float32) + for k, proposal_num in enumerate(proposal_nums): + tmp_ious = np.zeros(0) + for i in range(img_num): + ious = all_ious[i][:, :proposal_num].copy() + gt_ious = np.zeros(ious.shape[0]) + if ious.size == 0: + tmp_ious = np.hstack((tmp_ious, gt_ious)) + continue + for j in range(ious.shape[0]): + gt_max_overlaps = ious.argmax(axis=1) + max_ious = ious[np.arange(0, ious.shape[0]), gt_max_overlaps] + gt_idx = max_ious.argmax() + gt_ious[j] = max_ious[gt_idx] + box_idx = gt_max_overlaps[gt_idx] + ious[gt_idx, :] = -1 + ious[:, box_idx] = -1 + tmp_ious = np.hstack((tmp_ious, gt_ious)) + ious_[k, :] = tmp_ious + + ious_ = np.fliplr(np.sort(ious_, axis=1)) + recalls = np.zeros((proposal_nums.size, thrs.size)) + for i, thr in enumerate(thrs): + recalls[:, i] = (ious_ >= thr).sum(axis=1) / float(total_gt_num) + + return recalls + + +def set_recall_param(proposal_nums, iou_thrs): + if isinstance(proposal_nums, list): + proposal_nums_ = np.array(proposal_nums) + elif isinstance(proposal_nums, int): + proposal_nums_ = np.array([proposal_nums]) + else: + proposal_nums_ = proposal_nums + + if iou_thrs is None: + _iou_thrs = np.array([0.5]) + elif isinstance(iou_thrs, list): + _iou_thrs = np.array(iou_thrs) + elif isinstance(iou_thrs, float): + _iou_thrs = np.array([iou_thrs]) + else: + _iou_thrs = iou_thrs + + return proposal_nums_, _iou_thrs + + +def eval_recalls(gts, proposals, proposal_nums=None, iou_thrs=None): + """Calculate recalls. """ + img_num = len(gts) + assert img_num == len(proposals) + + proposal_nums, iou_thrs = set_recall_param(proposal_nums, iou_thrs) + + all_ious = [] + for i in range(img_num): + if proposals[i].ndim == 2 and proposals[i].shape[1] == 5: + scores = proposals[i][:, 4] + sort_idx = np.argsort(scores)[::-1] + img_proposal = proposals[i][sort_idx, :] + else: + img_proposal = proposals[i] + + prop_num = min(img_proposal.shape[0], proposal_nums[-1]) + if gts[i] is None or gts[i].shape[0] == 0: + ious = np.zeros((0, img_proposal.shape[0]), dtype=np.float32) + else: + ious = bbox_overlaps( + torch.tensor(gts[i]), + torch.tensor(img_proposal[:prop_num, :4])) + ious = ious.data.numpy() + all_ious.append(ious) + all_ious = np.array(all_ious) + recalls = _recalls(all_ious, proposal_nums, iou_thrs) + return recalls diff --git a/paddlevideo/metrics/registry.py b/paddlevideo/metrics/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..221444023345011cfe6f0922fa939a635b46d738 --- /dev/null +++ b/paddlevideo/metrics/registry.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..utils import Registry + +METRIC = Registry('metric') diff --git a/paddlevideo/metrics/segmentation_metric.py b/paddlevideo/metrics/segmentation_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..3719450e4a12a825468a077c1f1084c849f36d9b --- /dev/null +++ b/paddlevideo/metrics/segmentation_metric.py @@ -0,0 +1,389 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np +import argparse +import pandas as pd + +from .registry import METRIC +from .base import BaseMetric +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +def get_labels_scores_start_end_time(input_np, + frame_wise_labels, + actions_dict, + bg_class=["background", "None"]): + labels = [] + starts = [] + ends = [] + scores = [] + + boundary_score_ptr = 0 + + last_label = frame_wise_labels[0] + if frame_wise_labels[0] not in bg_class: + labels.append(frame_wise_labels[0]) + starts.append(0) + for i in range(len(frame_wise_labels)): + if frame_wise_labels[i] != last_label: + if frame_wise_labels[i] not in bg_class: + labels.append(frame_wise_labels[i]) + starts.append(i) + if last_label not in bg_class: + ends.append(i) + score = np.mean( + input_np[actions_dict[labels[boundary_score_ptr]], \ + starts[boundary_score_ptr]:(ends[boundary_score_ptr] + 1)] + ) + scores.append(score) + boundary_score_ptr = boundary_score_ptr + 1 + last_label = frame_wise_labels[i] + if last_label not in bg_class: + ends.append(i + 1) + score = np.mean( + input_np[actions_dict[labels[boundary_score_ptr]], \ + starts[boundary_score_ptr]:(ends[boundary_score_ptr] + 1)] + ) + scores.append(score) + boundary_score_ptr = boundary_score_ptr + 1 + + return labels, starts, ends, scores + + +def get_labels_start_end_time(frame_wise_labels, + bg_class=["background", "None"]): + labels = [] + starts = [] + ends = [] + last_label = frame_wise_labels[0] + if frame_wise_labels[0] not in bg_class: + labels.append(frame_wise_labels[0]) + starts.append(0) + for i in range(len(frame_wise_labels)): + if frame_wise_labels[i] != last_label: + if frame_wise_labels[i] not in bg_class: + labels.append(frame_wise_labels[i]) + starts.append(i) + if last_label not in bg_class: + ends.append(i) + last_label = frame_wise_labels[i] + if last_label not in bg_class: + ends.append(i + 1) + return labels, starts, ends + + +def levenstein(p, y, norm=False): + m_row = len(p) + n_col = len(y) + D = np.zeros([m_row + 1, n_col + 1], np.float) + for i in range(m_row + 1): + D[i, 0] = i + for i in range(n_col + 1): + D[0, i] = i + + for j in range(1, n_col + 1): + for i in range(1, m_row + 1): + if y[j - 1] == p[i - 1]: + D[i, j] = D[i - 1, j - 1] + else: + D[i, j] = min(D[i - 1, j] + 1, D[i, j - 1] + 1, + D[i - 1, j - 1] + 1) + + if norm: + score = (1 - D[-1, -1] / max(m_row, n_col)) * 100 + else: + score = D[-1, -1] + + return score + + +def edit_score(recognized, + ground_truth, + norm=True, + bg_class=["background", "None"]): + P, _, _ = get_labels_start_end_time(recognized, bg_class) + Y, _, _ = get_labels_start_end_time(ground_truth, bg_class) + return levenstein(P, Y, norm) + + +def f_score(recognized, ground_truth, overlap, bg_class=["background", "None"]): + p_label, p_start, p_end = get_labels_start_end_time(recognized, bg_class) + y_label, y_start, y_end = get_labels_start_end_time(ground_truth, bg_class) + + tp = 0 + fp = 0 + + hits = np.zeros(len(y_label)) + + for j in range(len(p_label)): + intersection = np.minimum(p_end[j], y_end) - np.maximum( + p_start[j], y_start) + union = np.maximum(p_end[j], y_end) - np.minimum(p_start[j], y_start) + IoU = (1.0 * intersection / union) * ( + [p_label[j] == y_label[x] for x in range(len(y_label))]) + # Get the best scoring segment + idx = np.array(IoU).argmax() + + if IoU[idx] >= overlap and not hits[idx]: + tp += 1 + hits[idx] = 1 + else: + fp += 1 + fn = len(y_label) - sum(hits) + return float(tp), float(fp), float(fn) + + +def boundary_AR(pred_boundary, gt_boundary, overlap_list, max_proposal): + + p_label, p_start, p_end, p_scores = pred_boundary + y_label, y_start, y_end, _ = gt_boundary + + # sort proposal + pred_dict = { + "label": p_label, + "start": p_start, + "end": p_end, + "scores": p_scores + } + pdf = pd.DataFrame(pred_dict) + pdf = pdf.sort_values(by="scores", ascending=False) + p_label = list(pdf["label"]) + p_start = list(pdf["start"]) + p_end = list(pdf["end"]) + p_scores = list(pdf["scores"]) + + # refine AN + if len(p_label) < max_proposal and len(p_label) > 0: + p_label = p_label + [p_label[-1]] * (max_proposal - len(p_label)) + p_start = p_start + [p_start[-1]] * (max_proposal - len(p_start)) + p_start = p_start + p_start[len(p_start) - + (max_proposal - len(p_start)):] + p_end = p_end + [p_end[-1]] * (max_proposal - len(p_end)) + p_scores = p_scores + [p_scores[-1]] * (max_proposal - len(p_scores)) + elif len(p_label) > max_proposal: + p_label[max_proposal:] = [] + p_start[max_proposal:] = [] + p_end[max_proposal:] = [] + p_scores[max_proposal:] = [] + + t_AR = np.zeros(len(overlap_list)) + + for i in range(len(overlap_list)): + overlap = overlap_list[i] + + tp = 0 + fp = 0 + hits = np.zeros(len(y_label)) + + for j in range(len(p_label)): + intersection = np.minimum(p_end[j], y_end) - np.maximum( + p_start[j], y_start) + union = np.maximum(p_end[j], y_end) - np.minimum( + p_start[j], y_start) + IoU = (1.0 * intersection / union) + # Get the best scoring segment + idx = np.array(IoU).argmax() + + if IoU[idx] >= overlap and not hits[idx]: + tp += 1 + hits[idx] = 1 + else: + fp += 1 + fn = len(y_label) - sum(hits) + + recall = float(tp) / (float(tp) + float(fn)) + t_AR[i] = recall + + AR = np.mean(t_AR) + return AR + + +@METRIC.register +class SegmentationMetric(BaseMetric): + """ + Test for Video Segmentation based model. + """ + + def __init__(self, + data_size, + batch_size, + overlap, + actions_map_file_path, + log_interval=1, + tolerance=5, + boundary_threshold=0.7, + max_proposal=100): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + # actions dict generate + file_ptr = open(actions_map_file_path, 'r') + actions = file_ptr.read().split('\n')[:-1] + file_ptr.close() + self.actions_dict = dict() + for a in actions: + self.actions_dict[a.split()[1]] = int(a.split()[0]) + + # cls score + self.overlap = overlap + self.overlap_len = len(overlap) + + self.cls_tp = np.zeros(self.overlap_len) + self.cls_fp = np.zeros(self.overlap_len) + self.cls_fn = np.zeros(self.overlap_len) + self.total_correct = 0 + self.total_edit = 0 + self.total_frame = 0 + self.total_video = 0 + + # boundary score + self.max_proposal = max_proposal + self.AR_at_AN = [[] for _ in range(max_proposal)] + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + groundTruth = data[1] + + predicted = outputs['predict'] + output_np = outputs['output_np'] + + outputs_np = predicted.numpy() + outputs_arr = output_np.numpy()[0, :] + gt_np = groundTruth.numpy()[0, :] + + recognition = [] + for i in range(outputs_np.shape[0]): + recognition = np.concatenate((recognition, [ + list(self.actions_dict.keys())[list( + self.actions_dict.values()).index(outputs_np[i])] + ])) + recog_content = list(recognition) + + gt_content = [] + for i in range(gt_np.shape[0]): + gt_content = np.concatenate((gt_content, [ + list(self.actions_dict.keys())[list( + self.actions_dict.values()).index(gt_np[i])] + ])) + gt_content = list(gt_content) + + pred_boundary = get_labels_scores_start_end_time( + outputs_arr, recog_content, self.actions_dict) + gt_boundary = get_labels_scores_start_end_time( + np.ones(outputs_arr.shape), gt_content, self.actions_dict) + + # cls score + correct = 0 + total = 0 + edit = 0 + + for i in range(len(gt_content)): + total += 1 + #accumulate + self.total_frame += 1 + + if gt_content[i] == recog_content[i]: + correct += 1 + #accumulate + self.total_correct += 1 + + edit_num = edit_score(recog_content, gt_content) + edit += edit_num + self.total_edit += edit_num + + for s in range(self.overlap_len): + tp1, fp1, fn1 = f_score(recog_content, gt_content, self.overlap[s]) + + # accumulate + self.cls_tp[s] += tp1 + self.cls_fp[s] += fp1 + self.cls_fn[s] += fn1 + + # accumulate + self.total_video += 1 + + # proposal score + for AN in range(self.max_proposal): + AR = boundary_AR(pred_boundary, + gt_boundary, + self.overlap, + max_proposal=(AN + 1)) + self.AR_at_AN[AN].append(AR) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + # cls metric + Acc = 100 * float(self.total_correct) / self.total_frame + Edit = (1.0 * self.total_edit) / self.total_video + Fscore = dict() + for s in range(self.overlap_len): + precision = self.cls_tp[s] / float(self.cls_tp[s] + self.cls_fp[s]) + recall = self.cls_tp[s] / float(self.cls_tp[s] + self.cls_fn[s]) + + f1 = 2.0 * (precision * recall) / (precision + recall) + + f1 = np.nan_to_num(f1) * 100 + Fscore[self.overlap[s]] = f1 + + # proposal metric + proposal_AUC = np.array(self.AR_at_AN) * 100 + AUC = np.mean(proposal_AUC) + AR_at_AN1 = np.mean(proposal_AUC[0, :]) + AR_at_AN5 = np.mean(proposal_AUC[4, :]) + AR_at_AN15 = np.mean(proposal_AUC[14, :]) + + # log metric + log_mertic_info = "dataset model performence: " + # preds ensemble + log_mertic_info += "Acc: {:.4f}, ".format(Acc) + log_mertic_info += 'Edit: {:.4f}, '.format(Edit) + for s in range(len(self.overlap)): + log_mertic_info += 'F1@{:0.2f}: {:.4f}, '.format( + self.overlap[s], Fscore[self.overlap[s]]) + + # boundary metric + log_mertic_info += "Auc: {:.4f}, ".format(AUC) + log_mertic_info += "AR@AN1: {:.4f}, ".format(AR_at_AN1) + log_mertic_info += "AR@AN5: {:.4f}, ".format(AR_at_AN5) + log_mertic_info += "AR@AN15: {:.4f}, ".format(AR_at_AN15) + logger.info(log_mertic_info) + + # log metric + metric_dict = dict() + metric_dict['Acc'] = Acc + metric_dict['Edit'] = Edit + for s in range(len(self.overlap)): + metric_dict['F1@{:0.2f}'.format( + self.overlap[s])] = Fscore[self.overlap[s]] + metric_dict['Auc'] = AUC + metric_dict['AR@AN1'] = AR_at_AN1 + metric_dict['AR@AN5'] = AR_at_AN5 + metric_dict['AR@AN15'] = AR_at_AN15 + + # clear for next epoch + # cls + self.cls_tp = np.zeros(self.overlap_len) + self.cls_fp = np.zeros(self.overlap_len) + self.cls_fn = np.zeros(self.overlap_len) + self.total_correct = 0 + self.total_edit = 0 + self.total_frame = 0 + self.total_video = 0 + # proposal + self.AR_at_AN = [[] for _ in range(self.max_proposal)] + + return metric_dict diff --git a/paddlevideo/metrics/skeleton_metric.py b/paddlevideo/metrics/skeleton_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..8c2e762fba7ce0a8585bb75928a57364b4a7be0e --- /dev/null +++ b/paddlevideo/metrics/skeleton_metric.py @@ -0,0 +1,90 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np +import paddle +import csv +import paddle.nn.functional as F + +from .registry import METRIC +from .base import BaseMetric +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@METRIC.register +class SkeletonMetric(BaseMetric): + """ + Test for Skeleton based model. + note: only support batch size = 1, single card test. + + Args: + out_file: str, file to save test results. + """ + + def __init__(self, + data_size, + batch_size, + out_file='submission.csv', + log_interval=1, + top_k=5): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.top1 = [] + self.top5 = [] + self.values = [] + self.out_file = out_file + self.k = top_k + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + if len(data) == 2: # data with label + labels = data[1] + top1 = paddle.metric.accuracy(input=outputs, label=labels, k=1) + top5 = paddle.metric.accuracy(input=outputs, label=labels, k=self.k) + if self.world_size > 1: + top1 = paddle.distributed.all_reduce( + top1, op=paddle.distributed.ReduceOp.SUM) / self.world_size + top5 = paddle.distributed.all_reduce( + top5, op=paddle.distributed.ReduceOp.SUM) / self.world_size + self.top1.append(top1.numpy()) + self.top5.append(top5.numpy()) + else: # data without label, only support batch_size=1. Used for fsd-10. + prob = F.softmax(outputs) + clas = paddle.argmax(prob, axis=1).numpy()[0] + self.values.append((batch_id, clas)) + + # preds ensemble + if batch_id % self.log_interval == 0: + logger.info("[TEST] Processing batch {}/{} ...".format( + batch_id, + self.data_size // (self.batch_size * self.world_size))) + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + if self.top1: # data with label + logger.info('[TEST] finished, avg_acc1= {}, avg_acc5= {}'.format( + np.mean(np.array(self.top1)), np.mean(np.array(self.top5)))) + else: + headers = ['sample_index', 'predict_category'] + with open( + self.out_file, + 'w', + ) as fp: + writer = csv.writer(fp) + writer.writerow(headers) + writer.writerows(self.values) + logger.info("Results saved in {} !".format(self.out_file)) diff --git a/paddlevideo/metrics/transnetv2_metric.py b/paddlevideo/metrics/transnetv2_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..33708817602591ce126270d6b044ed103d888521 --- /dev/null +++ b/paddlevideo/metrics/transnetv2_metric.py @@ -0,0 +1,174 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import numpy as np + +from .registry import METRIC +from .base import BaseMetric +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +def predictions_to_scenes(predictions): + scenes = [] + t, t_prev, start = -1, 0, 0 + for i, t in enumerate(predictions): + if t_prev == 1 and t == 0: + start = i + if t_prev == 0 and t == 1 and i != 0: + scenes.append([start, i]) + t_prev = t + if t == 0: + scenes.append([start, i]) + + # just fix if all predictions are 1 + if len(scenes) == 0: + return np.array([[0, len(predictions) - 1]], dtype=np.int32) + + return np.array(scenes, dtype=np.int32) + + +def evaluate_scenes(gt_scenes, pred_scenes, n_frames_miss_tolerance=2): + """ + Adapted from: https://github.com/gyglim/shot-detection-evaluation + The original based on: http://imagelab.ing.unimore.it/imagelab/researchActivity.asp?idActivity=19 + + n_frames_miss_tolerance: + Number of frames it is possible to miss ground truth by, and still being counted as a correct detection. + + Examples of computation with different tolerance margin: + n_frames_miss_tolerance = 0 + pred_scenes: [[0, 5], [6, 9]] -> pred_trans: [[5.5, 5.5]] + gt_scenes: [[0, 5], [6, 9]] -> gt_trans: [[5.5, 5.5]] -> HIT + gt_scenes: [[0, 4], [5, 9]] -> gt_trans: [[4.5, 4.5]] -> MISS + n_frames_miss_tolerance = 1 + pred_scenes: [[0, 5], [6, 9]] -> pred_trans: [[5.0, 6.0]] + gt_scenes: [[0, 5], [6, 9]] -> gt_trans: [[5.0, 6.0]] -> HIT + gt_scenes: [[0, 4], [5, 9]] -> gt_trans: [[4.0, 5.0]] -> HIT + gt_scenes: [[0, 3], [4, 9]] -> gt_trans: [[3.0, 4.0]] -> MISS + n_frames_miss_tolerance = 2 + pred_scenes: [[0, 5], [6, 9]] -> pred_trans: [[4.5, 6.5]] + gt_scenes: [[0, 5], [6, 9]] -> gt_trans: [[4.5, 6.5]] -> HIT + gt_scenes: [[0, 4], [5, 9]] -> gt_trans: [[3.5, 5.5]] -> HIT + gt_scenes: [[0, 3], [4, 9]] -> gt_trans: [[2.5, 4.5]] -> HIT + gt_scenes: [[0, 2], [3, 9]] -> gt_trans: [[1.5, 3.5]] -> MISS + + Users should be careful about adopting these functions in any commercial matters. + """ + + shift = n_frames_miss_tolerance / 2 + gt_scenes = gt_scenes.astype(np.float32) + np.array([[-0.5 + shift, 0.5 - shift]]) + pred_scenes = pred_scenes.astype(np.float32) + np.array([[-0.5 + shift, 0.5 - shift]]) + + gt_trans = np.stack([gt_scenes[:-1, 1], gt_scenes[1:, 0]], 1) + pred_trans = np.stack([pred_scenes[:-1, 1], pred_scenes[1:, 0]], 1) + + i, j = 0, 0 + tp, fp, fn = 0, 0, 0 + + while i < len(gt_trans) or j < len(pred_trans): + if j == len(pred_trans) or pred_trans[j, 0] > gt_trans[i, 1]: + fn += 1 + i += 1 + elif i == len(gt_trans) or pred_trans[j, 1] < gt_trans[i, 0]: + fp += 1 + j += 1 + else: + i += 1 + j += 1 + tp += 1 + + if tp + fp != 0: + p = tp / (tp + fp) + else: + p = 0 + + if tp + fn != 0: + r = tp / (tp + fn) + else: + r = 0 + + if p + r != 0: + f1 = (p * r * 2) / (p + r) + else: + f1 = 0 + + assert tp + fn == len(gt_trans) + assert tp + fp == len(pred_trans) + + return p, r, f1, (tp, fp, fn) + + +def create_scene_based_summaries(one_hot_pred, one_hot_gt): + thresholds = np.array([ + 0.02, 0.06, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9 + ]) + precision, recall, f1, tp, fp, fn = np.zeros_like(thresholds), np.zeros_like(thresholds),\ + np.zeros_like(thresholds), np.zeros_like(thresholds),\ + np.zeros_like(thresholds), np.zeros_like(thresholds) + + gt_scenes = predictions_to_scenes(one_hot_gt) + for i in range(len(thresholds)): + pred_scenes = predictions_to_scenes( + (one_hot_pred > thresholds[i]).astype(np.uint8) + ) + precision[i], recall[i], f1[i], (tp[i], fp[i], fn[i]) = evaluate_scenes(gt_scenes, pred_scenes) + + best_idx = np.argmax(f1) + + return f1[best_idx] + + +@METRIC.register +class TransNetV2Metric(BaseMetric): + def __init__(self, data_size, batch_size, log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.predictions = [] + self.total_stats = {"tp": 0, "fp": 0, "fn": 0} + + def update(self, batch_id, data, one_hot): + """update metrics during each iter + """ + if isinstance(one_hot, tuple): + one_hot = one_hot[0] + one_hot = paddle.nn.functional.sigmoid(one_hot)[0] + self.predictions.append(one_hot.numpy()[25:75]) + gt_scenes = data[1] + is_new_file = data[2] + if is_new_file: + self.compute(gt_scenes) + # preds ensemble + if batch_id % self.log_interval == 0: + logger.info("[TEST] Processing batch {}/{} ...".format( + batch_id, + self.data_size // (self.batch_size * self.world_size))) + + def compute(self, gt_scenes): + predictions = np.concatenate(self.predictions, 0)[:len(frames)] + _, _, _, (tp, fp, fn), fp_mistakes, fn_mistakes = evaluate_scenes( + gt_scenes, predictions_to_scenes((predictions >= args.thr).astype(np.uint8))) + + self.total_stats["tp"] += tp + self.total_stats["fp"] += fp + self.total_stats["fn"] += fn + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + p = self.total_stats["tp"] / (self.total_stats["tp"] + self.total_stats["fp"]) + r = self.total_stats["tp"] / (self.total_stats["tp"] + self.total_stats["fn"]) + f1 = (p * r * 2) / (p + r) + logger.info('[TEST] finished, Precision= {:5.2f}, Recall= {:5.2f} , F1 Score= {:5.2f} '.format( + p * 100, r * 100, f1 * 100)) \ No newline at end of file diff --git a/paddlevideo/metrics/vos_metric.py b/paddlevideo/metrics/vos_metric.py new file mode 100644 index 0000000000000000000000000000000000000000..19762f68609ce923d858bb687b785593b15b1e72 --- /dev/null +++ b/paddlevideo/metrics/vos_metric.py @@ -0,0 +1,276 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import os +import paddle +import zipfile +import time +from PIL import Image + +from paddle.io import DataLoader + +from .registry import METRIC +from .base import BaseMetric +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@METRIC.register +class VOSMetric(BaseMetric): + def __init__(self, + data_size, + batch_size, + result_root, + zip_dir, + log_interval=1): + """prepare for metrics + """ + super().__init__(data_size, batch_size, log_interval) + self.video_num = 0 + self.total_time = 0 + self.total_frame = 0 + self.total_sfps = 0 + self.total_video_num = data_size + self.count = 0 + self.result_root = result_root + self.zip_dir = zip_dir + + def update(self, batch_id, data, model): + """update metrics during each iter + """ + self.video_num += 1 + seq_dataset = data + seq_name = seq_dataset.seq_name + + logger.info('Prcessing Seq {} [{}/{}]:'.format(seq_name, self.video_num, + self.total_video_num)) + seq_dataloader = DataLoader(seq_dataset, + return_list=True, + batch_size=1, + shuffle=False, + num_workers=0) + seq_total_time = 0 + seq_total_frame = 0 + ref_embeddings = [] + ref_masks = [] + prev_embedding = [] + prev_mask = [] + with paddle.no_grad(): + for frame_idx, samples in enumerate(seq_dataloader): + time_start = time.time() + all_preds = [] + join_label = None + for aug_idx in range(len(samples)): + if len(ref_embeddings) <= aug_idx: + ref_embeddings.append([]) + ref_masks.append([]) + prev_embedding.append(None) + prev_mask.append(None) + + sample = samples[aug_idx] + ref_emb = ref_embeddings[aug_idx] + ref_m = ref_masks[aug_idx] + prev_emb = prev_embedding[aug_idx] + prev_m = prev_mask[aug_idx] + + current_img = sample['current_img'] + if 'current_label' in sample.keys(): + current_label = sample['current_label'] + current_label = paddle.to_tensor(current_label) + else: + current_label = None + + obj_num = sample['meta']['obj_num'] + imgname = sample['meta']['current_name'] + ori_height = sample['meta']['height'] + ori_width = sample['meta']['width'] + current_img = current_img + obj_num = obj_num + bs, _, h, w = current_img.shape + data_batch = [ + ref_emb, ref_m, prev_emb, prev_m, current_img, + [ori_height, ori_width], obj_num + ] + + all_pred, current_embedding = model(data_batch, mode='test') + + if frame_idx == 0: + if current_label is None: + logger.info( + "No first frame label in Seq {}.".format( + seq_name)) + ref_embeddings[aug_idx].append(current_embedding) + ref_masks[aug_idx].append(current_label) + + prev_embedding[aug_idx] = current_embedding + prev_mask[aug_idx] = current_label + else: + if sample['meta']['flip']: #False + all_pred = self.flip_tensor(all_pred, 3) + # In YouTube-VOS, not all the objects appear in the first frame for the first time. Thus, we + # have to introduce new labels for new objects, if necessary. + if not sample['meta']['flip'] and not ( + current_label is None) and join_label is None: + join_label = paddle.cast(current_label, + dtype='int64') + all_preds.append(all_pred) + if current_label is not None: + ref_embeddings[aug_idx].append(current_embedding) + prev_embedding[aug_idx] = current_embedding + + if frame_idx > 0: + all_preds = paddle.concat(all_preds, axis=0) + all_preds = paddle.mean( + all_preds, axis=0) #average results if augmentation + pred_label = paddle.argmax(all_preds, axis=0) + if join_label is not None: + join_label = paddle.squeeze(paddle.squeeze(join_label, + axis=0), + axis=0) + keep = paddle.cast((join_label == 0), dtype="int64") + pred_label = pred_label * keep + join_label * (1 - keep) + pred_label = pred_label + current_label = paddle.reshape( + pred_label, shape=[1, 1, ori_height, ori_width]) + flip_pred_label = self.flip_tensor(pred_label, 1) + flip_current_label = paddle.reshape( + flip_pred_label, shape=[1, 1, ori_height, ori_width]) + + for aug_idx in range(len(samples)): + if join_label is not None: + if samples[aug_idx]['meta']['flip']: + ref_masks[aug_idx].append(flip_current_label) + else: + ref_masks[aug_idx].append(current_label) + if samples[aug_idx]['meta']['flip']: + prev_mask[aug_idx] = flip_current_label + else: + prev_mask[ + aug_idx] = current_label #update prev_mask + + one_frametime = time.time() - time_start + seq_total_time += one_frametime + seq_total_frame += 1 + obj_num = obj_num.numpy()[0].item() + logger.info('Frame: {}, Obj Num: {}, Time: {}'.format( + imgname[0], obj_num, one_frametime)) + self.save_mask( + pred_label, + os.path.join(self.result_root, seq_name, + imgname[0].split('.')[0] + '.png')) + else: + one_frametime = time.time() - time_start + seq_total_time += one_frametime + logger.info('Ref Frame: {}, Time: {}'.format( + imgname[0], one_frametime)) + + del (ref_embeddings) + del (ref_masks) + del (prev_embedding) + del (prev_mask) + del (seq_dataset) + del (seq_dataloader) + + seq_avg_time_per_frame = seq_total_time / seq_total_frame + self.total_time += seq_total_time + self.total_frame += seq_total_frame + total_avg_time_per_frame = self.total_time / self.total_frame + self.total_sfps += seq_avg_time_per_frame + avg_sfps = self.total_sfps / (batch_id + 1) + logger.info("Seq {} FPS: {}, Total FPS: {}, FPS per Seq: {}".format( + seq_name, 1. / seq_avg_time_per_frame, + 1. / total_avg_time_per_frame, 1. / avg_sfps)) + + def flip_tensor(self, tensor, dim=0): + inv_idx = paddle.cast(paddle.arange(tensor.shape[dim] - 1, -1, -1), + dtype="int64") + tensor = paddle.index_select(x=tensor, index=inv_idx, axis=dim) + return tensor + + def save_mask(self, mask_tensor, path): + _palette = [ + 0, 0, 0, 128, 0, 0, 0, 128, 0, 128, 128, 0, 0, 0, 128, 128, 0, 128, + 0, 128, 128, 128, 128, 128, 64, 0, 0, 191, 0, 0, 64, 128, 0, 191, + 128, 0, 64, 0, 128, 191, 0, 128, 64, 128, 128, 191, 128, 128, 0, 64, + 0, 128, 64, 0, 0, 191, 0, 128, 191, 0, 0, 64, 128, 128, 64, 128, 22, + 22, 22, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, + 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 32, 32, 32, 33, 33, + 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 38, 38, 38, 39, + 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 43, 44, 44, 44, + 45, 45, 45, 46, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 49, 50, 50, + 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, + 56, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 60, 60, 61, 61, 61, + 62, 62, 62, 63, 63, 63, 64, 64, 64, 65, 65, 65, 66, 66, 66, 67, 67, + 67, 68, 68, 68, 69, 69, 69, 70, 70, 70, 71, 71, 71, 72, 72, 72, 73, + 73, 73, 74, 74, 74, 75, 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, + 79, 79, 79, 80, 80, 80, 81, 81, 81, 82, 82, 82, 83, 83, 83, 84, 84, + 84, 85, 85, 85, 86, 86, 86, 87, 87, 87, 88, 88, 88, 89, 89, 89, 90, + 90, 90, 91, 91, 91, 92, 92, 92, 93, 93, 93, 94, 94, 94, 95, 95, 95, + 96, 96, 96, 97, 97, 97, 98, 98, 98, 99, 99, 99, 100, 100, 100, 101, + 101, 101, 102, 102, 102, 103, 103, 103, 104, 104, 104, 105, 105, + 105, 106, 106, 106, 107, 107, 107, 108, 108, 108, 109, 109, 109, + 110, 110, 110, 111, 111, 111, 112, 112, 112, 113, 113, 113, 114, + 114, 114, 115, 115, 115, 116, 116, 116, 117, 117, 117, 118, 118, + 118, 119, 119, 119, 120, 120, 120, 121, 121, 121, 122, 122, 122, + 123, 123, 123, 124, 124, 124, 125, 125, 125, 126, 126, 126, 127, + 127, 127, 128, 128, 128, 129, 129, 129, 130, 130, 130, 131, 131, + 131, 132, 132, 132, 133, 133, 133, 134, 134, 134, 135, 135, 135, + 136, 136, 136, 137, 137, 137, 138, 138, 138, 139, 139, 139, 140, + 140, 140, 141, 141, 141, 142, 142, 142, 143, 143, 143, 144, 144, + 144, 145, 145, 145, 146, 146, 146, 147, 147, 147, 148, 148, 148, + 149, 149, 149, 150, 150, 150, 151, 151, 151, 152, 152, 152, 153, + 153, 153, 154, 154, 154, 155, 155, 155, 156, 156, 156, 157, 157, + 157, 158, 158, 158, 159, 159, 159, 160, 160, 160, 161, 161, 161, + 162, 162, 162, 163, 163, 163, 164, 164, 164, 165, 165, 165, 166, + 166, 166, 167, 167, 167, 168, 168, 168, 169, 169, 169, 170, 170, + 170, 171, 171, 171, 172, 172, 172, 173, 173, 173, 174, 174, 174, + 175, 175, 175, 176, 176, 176, 177, 177, 177, 178, 178, 178, 179, + 179, 179, 180, 180, 180, 181, 181, 181, 182, 182, 182, 183, 183, + 183, 184, 184, 184, 185, 185, 185, 186, 186, 186, 187, 187, 187, + 188, 188, 188, 189, 189, 189, 190, 190, 190, 191, 191, 191, 192, + 192, 192, 193, 193, 193, 194, 194, 194, 195, 195, 195, 196, 196, + 196, 197, 197, 197, 198, 198, 198, 199, 199, 199, 200, 200, 200, + 201, 201, 201, 202, 202, 202, 203, 203, 203, 204, 204, 204, 205, + 205, 205, 206, 206, 206, 207, 207, 207, 208, 208, 208, 209, 209, + 209, 210, 210, 210, 211, 211, 211, 212, 212, 212, 213, 213, 213, + 214, 214, 214, 215, 215, 215, 216, 216, 216, 217, 217, 217, 218, + 218, 218, 219, 219, 219, 220, 220, 220, 221, 221, 221, 222, 222, + 222, 223, 223, 223, 224, 224, 224, 225, 225, 225, 226, 226, 226, + 227, 227, 227, 228, 228, 228, 229, 229, 229, 230, 230, 230, 231, + 231, 231, 232, 232, 232, 233, 233, 233, 234, 234, 234, 235, 235, + 235, 236, 236, 236, 237, 237, 237, 238, 238, 238, 239, 239, 239, + 240, 240, 240, 241, 241, 241, 242, 242, 242, 243, 243, 243, 244, + 244, 244, 245, 245, 245, 246, 246, 246, 247, 247, 247, 248, 248, + 248, 249, 249, 249, 250, 250, 250, 251, 251, 251, 252, 252, 252, + 253, 253, 253, 254, 254, 254, 255, 255, 255 + ] + mask = mask_tensor.cpu().numpy().astype('uint8') + mask = Image.fromarray(mask).convert('P') + mask.putpalette(_palette) + mask.save(path) + + def zip_folder(self, source_folder, zip_dir): + f = zipfile.ZipFile(zip_dir, 'w', zipfile.ZIP_DEFLATED) + pre_len = len(os.path.dirname(source_folder)) + for dirpath, dirnames, filenames in os.walk(source_folder): + for filename in filenames: + pathfile = os.path.join(dirpath, filename) + arcname = pathfile[pre_len:].strip(os.path.sep) + f.write(pathfile, arcname) + f.close() + + def accumulate(self): + """accumulate metrics when finished all iters. + """ + self.zip_folder(self.result_root, self.zip_dir) + logger.info('Save result to {}.'.format(self.zip_dir)) diff --git a/paddlevideo/metrics/youtube8m/__init__.py b/paddlevideo/metrics/youtube8m/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/paddlevideo/metrics/youtube8m/average_precision_calculator.py b/paddlevideo/metrics/youtube8m/average_precision_calculator.py new file mode 100644 index 0000000000000000000000000000000000000000..bdbd6e0d04c4cbcef18ed89a38689541df2fcb6c --- /dev/null +++ b/paddlevideo/metrics/youtube8m/average_precision_calculator.py @@ -0,0 +1,274 @@ +# Copyright 2020 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Calculate or keep track of the interpolated average precision. + +It provides an interface for calculating interpolated average precision for an +entire list or the top-n ranked items. For the definition of the +(non-)interpolated average precision: +http://trec.nist.gov/pubs/trec15/appendices/CE.MEASURES06.pdf + +Example usages: +1) Use it as a static function call to directly calculate average precision for +a short ranked list in the memory. + +``` +import random + +p = np.array([random.random() for _ in xrange(10)]) +a = np.array([random.choice([0, 1]) for _ in xrange(10)]) + +ap = average_precision_calculator.AveragePrecisionCalculator.ap(p, a) +``` + +2) Use it as an object for long ranked list that cannot be stored in memory or +the case where partial predictions can be observed at a time (Tensorflow +predictions). In this case, we first call the function accumulate many times +to process parts of the ranked list. After processing all the parts, we call +peek_interpolated_ap_at_n. +``` +p1 = np.array([random.random() for _ in xrange(5)]) +a1 = np.array([random.choice([0, 1]) for _ in xrange(5)]) +p2 = np.array([random.random() for _ in xrange(5)]) +a2 = np.array([random.choice([0, 1]) for _ in xrange(5)]) + +# interpolated average precision at 10 using 1000 break points +calculator = average_precision_calculator.AveragePrecisionCalculator(10) +calculator.accumulate(p1, a1) +calculator.accumulate(p2, a2) +ap3 = calculator.peek_ap_at_n() +``` +""" + +import heapq +import random +import numbers + +import numpy + + +class AveragePrecisionCalculator(object): + """Calculate the average precision and average precision at n.""" + def __init__(self, top_n=None): + """Construct an AveragePrecisionCalculator to calculate average precision. + + This class is used to calculate the average precision for a single label. + + Args: + top_n: A positive Integer specifying the average precision at n, or + None to use all provided data points. + + Raises: + ValueError: An error occurred when the top_n is not a positive integer. + """ + if not ((isinstance(top_n, int) and top_n >= 0) or top_n is None): + raise ValueError("top_n must be a positive integer or None.") + + self._top_n = top_n # average precision at n + self._total_positives = 0 # total number of positives have seen + self._heap = [] # max heap of (prediction, actual) + + @property + def heap_size(self): + """Gets the heap size maintained in the class.""" + return len(self._heap) + + @property + def num_accumulated_positives(self): + """Gets the number of positive samples that have been accumulated.""" + return self._total_positives + + def accumulate(self, predictions, actuals, num_positives=None): + """Accumulate the predictions and their ground truth labels. + + After the function call, we may call peek_ap_at_n to actually calculate + the average precision. + Note predictions and actuals must have the same shape. + + Args: + predictions: a list storing the prediction scores. + actuals: a list storing the ground truth labels. Any value + larger than 0 will be treated as positives, otherwise as negatives. + num_positives = If the 'predictions' and 'actuals' inputs aren't complete, + then it's possible some true positives were missed in them. In that case, + you can provide 'num_positives' in order to accurately track recall. + + Raises: + ValueError: An error occurred when the format of the input is not the + numpy 1-D array or the shape of predictions and actuals does not match. + """ + if len(predictions) != len(actuals): + raise ValueError( + "the shape of predictions and actuals does not match.") + + if not num_positives is None: + if not isinstance(num_positives, + numbers.Number) or num_positives < 0: + raise ValueError( + "'num_positives' was provided but it wan't a nonzero number." + ) + + if not num_positives is None: + self._total_positives += num_positives + else: + self._total_positives += numpy.size(numpy.where(actuals > 0)) + topk = self._top_n + heap = self._heap + + for i in range(numpy.size(predictions)): + if topk is None or len(heap) < topk: + heapq.heappush(heap, (predictions[i], actuals[i])) + else: + if predictions[i] > heap[0][0]: # heap[0] is the smallest + heapq.heappop(heap) + heapq.heappush(heap, (predictions[i], actuals[i])) + + def clear(self): + """Clear the accumulated predictions.""" + self._heap = [] + self._total_positives = 0 + + def peek_ap_at_n(self): + """Peek the non-interpolated average precision at n. + + Returns: + The non-interpolated average precision at n (default 0). + If n is larger than the length of the ranked list, + the average precision will be returned. + """ + if self.heap_size <= 0: + return 0 + predlists = numpy.array(list(zip(*self._heap))) + + ap = self.ap_at_n(predlists[0], + predlists[1], + n=self._top_n, + total_num_positives=self._total_positives) + return ap + + @staticmethod + def ap(predictions, actuals): + """Calculate the non-interpolated average precision. + + Args: + predictions: a numpy 1-D array storing the sparse prediction scores. + actuals: a numpy 1-D array storing the ground truth labels. Any value + larger than 0 will be treated as positives, otherwise as negatives. + + Returns: + The non-interpolated average precision at n. + If n is larger than the length of the ranked list, + the average precision will be returned. + + Raises: + ValueError: An error occurred when the format of the input is not the + numpy 1-D array or the shape of predictions and actuals does not match. + """ + return AveragePrecisionCalculator.ap_at_n(predictions, actuals, n=None) + + @staticmethod + def ap_at_n(predictions, actuals, n=20, total_num_positives=None): + """Calculate the non-interpolated average precision. + + Args: + predictions: a numpy 1-D array storing the sparse prediction scores. + actuals: a numpy 1-D array storing the ground truth labels. Any value + larger than 0 will be treated as positives, otherwise as negatives. + n: the top n items to be considered in ap@n. + total_num_positives : (optionally) you can specify the number of total + positive + in the list. If specified, it will be used in calculation. + + Returns: + The non-interpolated average precision at n. + If n is larger than the length of the ranked list, + the average precision will be returned. + + Raises: + ValueError: An error occurred when + 1) the format of the input is not the numpy 1-D array; + 2) the shape of predictions and actuals does not match; + 3) the input n is not a positive integer. + """ + if len(predictions) != len(actuals): + raise ValueError( + "the shape of predictions and actuals does not match.") + + if n is not None: + if not isinstance(n, int) or n <= 0: + raise ValueError("n must be 'None' or a positive integer." + " It was '%s'." % n) + + ap = 0.0 + + predictions = numpy.array(predictions) + actuals = numpy.array(actuals) + + # add a shuffler to avoid overestimating the ap + predictions, actuals = AveragePrecisionCalculator._shuffle( + predictions, actuals) + sortidx = sorted(range(len(predictions)), + key=lambda k: predictions[k], + reverse=True) + + if total_num_positives is None: + numpos = numpy.size(numpy.where(actuals > 0)) + else: + numpos = total_num_positives + + if numpos == 0: + return 0 + + if n is not None: + numpos = min(numpos, n) + delta_recall = 1.0 / numpos + poscount = 0.0 + + # calculate the ap + r = len(sortidx) + if n is not None: + r = min(r, n) + for i in range(r): + if actuals[sortidx[i]] > 0: + poscount += 1 + ap += poscount / (i + 1) * delta_recall + return ap + + @staticmethod + def _shuffle(predictions, actuals): + random.seed(0) + suffidx = random.sample(range(len(predictions)), len(predictions)) + predictions = predictions[suffidx] + actuals = actuals[suffidx] + return predictions, actuals + + @staticmethod + def _zero_one_normalize(predictions, epsilon=1e-7): + """Normalize the predictions to the range between 0.0 and 1.0. + + For some predictions like SVM predictions, we need to normalize them before + calculate the interpolated average precision. The normalization will not + change the rank in the original list and thus won't change the average + precision. + + Args: + predictions: a numpy 1-D array storing the sparse prediction scores. + epsilon: a small constant to avoid denominator being zero. + + Returns: + The normalized prediction. + """ + denominator = numpy.max(predictions) - numpy.min(predictions) + ret = (predictions - numpy.min(predictions)) / numpy.max( + denominator, epsilon) + return ret diff --git a/paddlevideo/metrics/youtube8m/eval_util.py b/paddlevideo/metrics/youtube8m/eval_util.py new file mode 100644 index 0000000000000000000000000000000000000000..abcf0d8f2d8fec5363ceac5c8c6b581a31683f7a --- /dev/null +++ b/paddlevideo/metrics/youtube8m/eval_util.py @@ -0,0 +1,198 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Provides functions to help with evaluating models.""" +import numpy as np +import paddle +from paddlevideo.utils import get_logger + +from ..base import BaseMetric +from ..registry import METRIC +from . import average_precision_calculator as ap_calculator +from . import mean_average_precision_calculator as map_calculator + +logger = get_logger("paddlevideo") + + +def flatten(l): + """ Merges a list of lists into a single list. """ + return [item for sublist in l for item in sublist] + + +def calculate_hit_at_one(predictions, actuals): + """Performs a local (numpy) calculation of the hit at one. + + Args: + predictions: Matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + actuals: Matrix containing the ground truth labels. + Dimensions are 'batch' x 'num_classes'. + + Returns: + float: The average hit at one across the entire batch. + """ + top_prediction = np.argmax(predictions, 1) + hits = actuals[np.arange(actuals.shape[0]), top_prediction] + return np.mean(hits) + + +def calculate_precision_at_equal_recall_rate(predictions, actuals): + """Performs a local (numpy) calculation of the PERR. + + Args: + predictions: Matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + actuals: Matrix containing the ground truth labels. + Dimensions are 'batch' x 'num_classes'. + + Returns: + float: The average precision at equal recall rate across the entire batch. + """ + aggregated_precision = 0.0 + num_videos = actuals.shape[0] + for row in np.arange(num_videos): + num_labels = int(np.sum(actuals[row])) + top_indices = np.argpartition(predictions[row], + -num_labels)[-num_labels:] + item_precision = 0.0 + for label_index in top_indices: + if predictions[row][label_index] > 0: + item_precision += actuals[row][label_index] + item_precision /= top_indices.size + aggregated_precision += item_precision + aggregated_precision /= num_videos + return aggregated_precision + + +def calculate_gap(predictions, actuals, top_k=20): + """Performs a local (numpy) calculation of the global average precision. + + Only the top_k predictions are taken for each of the videos. + + Args: + predictions: Matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + actuals: Matrix containing the ground truth labels. + Dimensions are 'batch' x 'num_classes'. + top_k: How many predictions to use per video. + + Returns: + float: The global average precision. + """ + gap_calculator = ap_calculator.AveragePrecisionCalculator() + sparse_predictions, sparse_labels, num_positives = top_k_by_class( + predictions, actuals, top_k) + gap_calculator.accumulate(flatten(sparse_predictions), + flatten(sparse_labels), sum(num_positives)) + return gap_calculator.peek_ap_at_n() + + +def top_k_by_class(predictions, labels, k=20): + """Extracts the top k predictions for each video, sorted by class. + + Args: + predictions: A numpy matrix containing the outputs of the model. + Dimensions are 'batch' x 'num_classes'. + k: the top k non-zero entries to preserve in each prediction. + + Returns: + A tuple (predictions,labels, true_positives). 'predictions' and 'labels' + are lists of lists of floats. 'true_positives' is a list of scalars. The + length of the lists are equal to the number of classes. The entries in the + predictions variable are probability predictions, and + the corresponding entries in the labels variable are the ground truth for + those predictions. The entries in 'true_positives' are the number of true + positives for each class in the ground truth. + + Raises: + ValueError: An error occurred when the k is not a positive integer. + """ + if k <= 0: + raise ValueError("k must be a positive integer.") + k = min(k, predictions.shape[1]) + num_classes = predictions.shape[1] + prediction_triplets = [] + for video_index in range(predictions.shape[0]): + prediction_triplets.extend( + top_k_triplets(predictions[video_index], labels[video_index], k)) + out_predictions = [[] for v in range(num_classes)] + out_labels = [[] for v in range(num_classes)] + for triplet in prediction_triplets: + out_predictions[triplet[0]].append(triplet[1]) + out_labels[triplet[0]].append(triplet[2]) + out_true_positives = [np.sum(labels[:, i]) for i in range(num_classes)] + + return out_predictions, out_labels, out_true_positives + + +def top_k_triplets(predictions, labels, k=20): + """Get the top_k for a 1-d numpy array. Returns a sparse list of tuples in + (prediction, class) format""" + m = len(predictions) + k = min(k, m) + indices = np.argpartition(predictions, -k)[-k:] + return [(index, predictions[index], labels[index]) for index in indices] + + +@METRIC.register +class HitOneMetric(BaseMetric): + """A class to store the evaluation metrics.""" + def __init__(self, + num_class, + top_k, + data_size, + batch_size, + log_interval=20): + """Construct an HitOneMetric object to store the evaluation metrics.""" + self.hit_at_one = [] + self.perr = [] + self.gap = [] + super().__init__(data_size, batch_size, log_interval) + + def accumulate(self): + logger.info( + '[TEST] finished, hit_at_one = {:.5f}, perr = {:.5f}, gap = {:.5f}'. + format(np.mean(np.array(self.hit_at_one)), + np.mean(np.array(self.perr)), np.mean(np.array(self.gap)))) + + def clear(self): + """Clear the evaluation metrics and reset the HitOneMetric object.""" + self.hit_at_one = [] + self.perr = [] + self.gap = [] + + def update(self, batch_id, data, outputs): + """update metrics during each iter + """ + hit_at_one = paddle.to_tensor(outputs['hit_at_one']) + perr = paddle.to_tensor(outputs['perr']) + gap = paddle.to_tensor(outputs['gap']) + # NOTE(shipping): deal with multi cards validate + if self.world_size > 1: + hit_at_one = paddle.distributed.all_reduce( + hit_at_one, + op=paddle.distributed.ReduceOp.SUM) / self.world_size + perr = paddle.distributed.all_reduce( + perr, op=paddle.distributed.ReduceOp.SUM) / self.world_size + gap = paddle.distributed.all_reduce( + gap, op=paddle.distributed.ReduceOp.SUM) / self.world_size + + self.hit_at_one.append(hit_at_one.numpy()) + self.perr.append(perr.numpy()) + self.gap.append(gap.numpy()) + # preds ensemble + if batch_id % self.log_interval == 0: + logger.info("[TEST] Processing batch {}/{}...".format( + batch_id, + self.data_size // (self.batch_size * self.world_size), + )) diff --git a/paddlevideo/metrics/youtube8m/mean_average_precision_calculator.py b/paddlevideo/metrics/youtube8m/mean_average_precision_calculator.py new file mode 100644 index 0000000000000000000000000000000000000000..0ae8b0ed3717aba13b7ed35b4af025be40423967 --- /dev/null +++ b/paddlevideo/metrics/youtube8m/mean_average_precision_calculator.py @@ -0,0 +1,114 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Calculate the mean average precision. + +It provides an interface for calculating mean average precision +for an entire list or the top-n ranked items. + +Example usages: +We first call the function accumulate many times to process parts of the ranked +list. After processing all the parts, we call peek_map_at_n +to calculate the mean average precision. + +``` +import random + +p = np.array([[random.random() for _ in xrange(50)] for _ in xrange(1000)]) +a = np.array([[random.choice([0, 1]) for _ in xrange(50)] + for _ in xrange(1000)]) + +# mean average precision for 50 classes. +calculator = mean_average_precision_calculator.MeanAveragePrecisionCalculator( + num_class=50) +calculator.accumulate(p, a) +aps = calculator.peek_map_at_n() +``` +""" + +import numpy +from . import average_precision_calculator + + +class MeanAveragePrecisionCalculator(object): + """This class is to calculate mean average precision. + """ + + def __init__(self, num_class): + """Construct a calculator to calculate the (macro) average precision. + + Args: + num_class: A positive Integer specifying the number of classes. + top_n_array: A list of positive integers specifying the top n for each + class. The top n in each class will be used to calculate its average + precision at n. + The size of the array must be num_class. + + Raises: + ValueError: An error occurred when num_class is not a positive integer; + or the top_n_array is not a list of positive integers. + """ + if not isinstance(num_class, int) or num_class <= 1: + raise ValueError("num_class must be a positive integer.") + + self._ap_calculators = [] # member of AveragePrecisionCalculator + self._num_class = num_class # total number of classes + for i in range(num_class): + self._ap_calculators.append( + average_precision_calculator.AveragePrecisionCalculator()) + + def accumulate(self, predictions, actuals, num_positives=None): + """Accumulate the predictions and their ground truth labels. + + Args: + predictions: A list of lists storing the prediction scores. The outer + dimension corresponds to classes. + actuals: A list of lists storing the ground truth labels. The dimensions + should correspond to the predictions input. Any value + larger than 0 will be treated as positives, otherwise as negatives. + num_positives: If provided, it is a list of numbers representing the + number of true positives for each class. If not provided, the number of + true positives will be inferred from the 'actuals' array. + + Raises: + ValueError: An error occurred when the shape of predictions and actuals + does not match. + """ + if not num_positives: + num_positives = [None for i in predictions.shape[1]] + + calculators = self._ap_calculators + for i in range(len(predictions)): + calculators[i].accumulate(predictions[i], actuals[i], + num_positives[i]) + + def clear(self): + for calculator in self._ap_calculators: + calculator.clear() + + def is_empty(self): + return ([calculator.heap_size for calculator in self._ap_calculators] == + [0 for _ in range(self._num_class)]) + + def peek_map_at_n(self): + """Peek the non-interpolated mean average precision at n. + + Returns: + An array of non-interpolated average precision at n (default 0) for each + class. + """ + aps = [ + self._ap_calculators[i].peek_ap_at_n() + for i in range(self._num_class) + ] + return aps diff --git a/paddlevideo/modeling/__init__.py b/paddlevideo/modeling/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..639bd340388389a55da37ba665f4952c9a184dd8 --- /dev/null +++ b/paddlevideo/modeling/__init__.py @@ -0,0 +1,37 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .assigners import MaxIoUAssignerAVA +from .backbones import ResNet +from .builder import (build_backbone, build_head, build_localizer, build_loss, + build_recognizer) +from .framework.detectors import BaseDetector, FastRCNN, TwoStageDetector +from .framework.recognizers import BaseRecognizer, Recognizer2D +from .heads import (AVARoIHead, BaseHead, BBoxHeadAVA, SingleRoIExtractor3D, + TSNHead) +from .losses import CrossEntropyLoss +from .registry import (BACKBONES, DETECTORS, HEADS, LOCALIZERS, LOSSES, + PARTITIONERS, RECOGNIZERS, ROI_EXTRACTORS) +from .samplers import RandomSampler +from .weight_init import kaiming_normal_, trunc_normal_, weight_init_ + +__all__ = [ + 'BACKBONES', 'HEADS', 'RECOGNIZERS', 'LOCALIZERS', 'PARTITIONERS', 'LOSSES', + 'build_recognizer', 'build_localizer', 'build_head', 'build_backbone', + 'build_loss', 'ResNet', 'TSNHead', 'BaseHead', 'BaseRecognizer', + 'Recognizer2d', 'CrossEntropyLoss', 'ROI_EXTRACTORS', + 'SingleRoIExtractor3D', 'AVARoIHead', 'BBoxHeadAVA', 'MaxIoUAssignerAVA', + 'RandomSampler', 'DETECTORS', 'kaiming_normal_', 'trunc_normal_', + 'weight_init_' +] diff --git a/paddlevideo/modeling/assigners/__init__.py b/paddlevideo/modeling/assigners/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a4570db2f0c073ece92c493de4851d8863f35022 --- /dev/null +++ b/paddlevideo/modeling/assigners/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .max_iou_assigner_ava import MaxIoUAssignerAVA + +__all__ = ['MaxIoUAssignerAVA'] diff --git a/paddlevideo/modeling/assigners/max_iou_assigner_ava.py b/paddlevideo/modeling/assigners/max_iou_assigner_ava.py new file mode 100644 index 0000000000000000000000000000000000000000..2515c858bd14e964782dd9cff4584ce5640f302e --- /dev/null +++ b/paddlevideo/modeling/assigners/max_iou_assigner_ava.py @@ -0,0 +1,148 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +import numpy as np +from ..registry import BBOX_ASSIGNERS +from ..bbox_utils import bbox_overlaps + +class AssignResult(): + def __init__(self, num_gts, gt_inds, max_overlaps, labels=None): + self.num_gts = num_gts + self.gt_inds = gt_inds + self.max_overlaps = max_overlaps + self.labels = labels + + def add_gt_(self, gt_labels): + """Add ground truth as assigned results. """ + self_inds = paddle.arange(1, len(gt_labels) + 1, dtype="int32") + gt_inds_squeeze = paddle.squeeze(self.gt_inds, axis=0) + self.gt_inds = paddle.concat([self_inds, gt_inds_squeeze]) + gt_label_ones = paddle.full((len(gt_labels), ), 1, dtype='float32') + max_overlaps_squeeze = paddle.squeeze(self.max_overlaps, axis=0) + self.max_overlaps = paddle.concat([gt_label_ones, max_overlaps_squeeze]) + if self.labels is not None: + self.labels = paddle.concat([gt_labels, self.labels]) + +@BBOX_ASSIGNERS.register() +class MaxIoUAssignerAVA(): + """Assign a corresponding gt bbox or background to each bbox. """ + def __init__(self, + pos_iou_thr, + neg_iou_thr, + min_pos_iou=.0, + gt_max_assign_all=True, + ignore_iof_thr=-1, + ignore_wrt_candidates=True, + match_low_quality=True, + gpu_assign_thr=-1, + iou_calculator=dict(type='BboxOverlaps2D')): + self.pos_iou_thr = pos_iou_thr + self.neg_iou_thr = neg_iou_thr + self.min_pos_iou = min_pos_iou + self.gt_max_assign_all = gt_max_assign_all + self.ignore_iof_thr = ignore_iof_thr + self.ignore_wrt_candidates = ignore_wrt_candidates + self.gpu_assign_thr = gpu_assign_thr + self.match_low_quality = match_low_quality + + def assign(self, + bboxes, + gt_bboxes, + gt_labels=None): + """Assign gt to bboxes. """ + overlaps = bbox_overlaps(gt_bboxes, bboxes) + assign_result = self.assign_wrt_overlaps(overlaps, gt_labels) + return assign_result + + def assign_wrt_overlaps(self, overlaps, gt_labels=None): + """Assign w.r.t. the overlaps of bboxes with gts. """ + num_gts, num_bboxes = overlaps.shape[0], overlaps.shape[1] + # 1. assign -1 + assigned_gt_inds = paddle.full((num_bboxes, ), -1, dtype='int32') + + # for each anchor, which gt best overlaps with it + # for each anchor, the max iou of all gts + max_overlaps, argmax_overlaps = paddle.topk(overlaps, k=1, axis=0) + # for each gt, which anchor best overlaps with it + # for each gt, the max iou of all proposals + gt_max_overlaps, gt_argmax_overlaps = paddle.topk(overlaps, k=1, axis=1) + + # 2. assign negative: below the negative inds are set to be 0 + match_labels = paddle.full(argmax_overlaps.shape, -1, dtype='int32') + match_labels = paddle.where(max_overlaps < self.neg_iou_thr, + paddle.zeros_like(match_labels), match_labels) + + # 3. assign positive: above positive IoU threshold + argmax_overlaps_int32 = paddle.cast(argmax_overlaps, 'int32') + match_labels = paddle.where(max_overlaps >= self.pos_iou_thr, + argmax_overlaps_int32 + 1, match_labels) + assigned_gt_inds = match_labels + if self.match_low_quality: + # Low-quality matching will overwirte the assigned_gt_inds + # assigned in Step 3. Thus, the assigned gt might not be the + # best one for prediction. + # For example, if bbox A has 0.9 and 0.8 iou with GT bbox + # 1 & 2, bbox 1 will be assigned as the best target for bbox A + # in step 3. However, if GT bbox 2's gt_argmax_overlaps = A, + # bbox A's assigned_gt_inds will be overwritten to be bbox B. + # This might be the reason that it is not used in ROI Heads. + for i in range(num_gts): + if gt_max_overlaps.numpy()[i] >= self.min_pos_iou: + if self.gt_max_assign_all: + equal_x_np = overlaps[i, :].numpy() + equal_y_np = gt_max_overlaps[i].numpy() + max_iou_inds = np.equal(equal_x_np, equal_y_np) + max_iou_inds = paddle.to_tensor(max_iou_inds) + max_iou_inds = paddle.reshape( max_iou_inds, [1,max_iou_inds.shape[0]] ) + match_labels_gts = paddle.full(max_iou_inds.shape, i+1, dtype='int32') + match_labels = paddle.where(max_iou_inds, match_labels_gts, match_labels) + assigned_gt_inds = match_labels + else: + assigned_gt_inds[gt_argmax_overlaps[i]] = i + 1 + + if gt_labels is not None: + # consider multi-class case (AVA) + assert len(gt_labels[0]) > 1 + assigned_labels = paddle.full([num_bboxes, len(gt_labels[0])], 0, dtype='float32') + assigned_gt_inds_reshape = assigned_gt_inds.reshape([assigned_gt_inds.shape[1]]) + pos_inds = paddle.nonzero( assigned_gt_inds_reshape , as_tuple=False) + pos_inds_num = paddle.numel(pos_inds).numpy()[0] + if pos_inds_num > 0: + pos_inds = paddle.squeeze(pos_inds, axis = 1 ) + assigned_gt_inds_squeeze = paddle.squeeze(assigned_gt_inds, axis=0) + assigned_gt_inds_select = paddle.index_select(assigned_gt_inds_squeeze, pos_inds) - 1 + gt_labels_select = paddle.index_select(gt_labels, assigned_gt_inds_select) + A = assigned_gt_inds_squeeze + X = assigned_gt_inds_squeeze - 1 + Y = paddle.zeros_like(X) + if A.shape[0]==1: + if A.numpy()[0]>0: + T=X + else: + T=Y + else: + T = paddle.where(A>0, X, Y) + S = paddle.index_select(gt_labels, T) + AE = paddle.expand(A, [S.shape[1], A.shape[0]]) + AET = paddle.transpose(AE, perm=[1, 0]) + R = paddle.where(AET>0, S, assigned_labels) + assigned_labels = R + else: + assigned_labels = None + ret = AssignResult( + num_gts, + assigned_gt_inds, + max_overlaps, + labels=assigned_labels) + return ret diff --git a/paddlevideo/modeling/backbones/__init__.py b/paddlevideo/modeling/backbones/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4374a6228b72d2137b578d7c5bdb0bcb156ef675 --- /dev/null +++ b/paddlevideo/modeling/backbones/__init__.py @@ -0,0 +1,51 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .actbert import BertForMultiModalPreTraining +from .adds import ADDS_DepthNet +from .agcn import AGCN +from .asrf import ASRF +from .bmn import BMN +from .cfbi import CFBI +from .movinet import MoViNet +from .ms_tcn import MSTCN +from .resnet import ResNet +from .resnet_slowfast import ResNetSlowFast +from .resnet_slowfast_MRI import ResNetSlowFast_MRI +from .resnet_tsm import ResNetTSM +from .resnet_tsm_MRI import ResNetTSM_MRI +from .resnet_tsn_MRI import ResNetTSN_MRI +from .resnet_tweaks_tsm import ResNetTweaksTSM +from .resnet_tweaks_tsn import ResNetTweaksTSN +from .stgcn import STGCN +from .swin_transformer import SwinTransformer3D +from .transnetv2 import TransNetV2 +from .vit import VisionTransformer +from .vit_tweaks import VisionTransformer_tweaks +from .ms_tcn import MSTCN +from .asrf import ASRF +from .resnet_tsn_MRI import ResNetTSN_MRI +from .resnet_tsm_MRI import ResNetTSM_MRI +from .resnet_slowfast_MRI import ResNetSlowFast_MRI +from .cfbi import CFBI +from .ctrgcn import CTRGCN +from .movinet import MoViNet + +__all__ = [ + 'ResNet', 'ResNetTSM', 'ResNetTweaksTSM', 'ResNetSlowFast', 'BMN', + 'ResNetTweaksTSN', 'VisionTransformer', 'STGCN', 'AGCN', 'TransNetV2', + 'ADDS_DepthNet', 'VisionTransformer_tweaks', 'BertForMultiModalPreTraining', + 'ResNetTSN_MRI', 'ResNetTSM_MRI', 'ResNetSlowFast_MRI', 'CFBI', 'MSTCN', + 'ASRF', 'MoViNet', 'SwinTransformer3D', 'CTRGCN' +] diff --git a/paddlevideo/modeling/backbones/actbert.py b/paddlevideo/modeling/backbones/actbert.py new file mode 100644 index 0000000000000000000000000000000000000000..dbee1fd8c6961382670ff996d254043bd823b18b --- /dev/null +++ b/paddlevideo/modeling/backbones/actbert.py @@ -0,0 +1,1158 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import numpy as np +import math +import copy + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from paddle.nn import (Conv2D, BatchNorm2D, Linear, Dropout) +from paddle.nn.initializer import Constant, Normal +from ...utils.save_load import load_ckpt +from ..registry import BACKBONES +from ..weight_init import weight_init_ + +ACT2FN = {"gelu": F.gelu, "relu": F.relu, "swish": F.swish} + + +class BertEmbeddings(nn.Layer): + """Construct the embeddings from word, position and token_type embeddings. + """ + def __init__(self, vocab_size, max_position_embeddings, type_vocab_size, + hidden_size, hidden_dropout_prob): + super(BertEmbeddings, self).__init__() + self.word_embeddings = nn.Embedding(vocab_size, + hidden_size, + padding_idx=0) + self.position_embeddings = nn.Embedding(max_position_embeddings, + hidden_size) + self.token_type_embeddings = nn.Embedding(type_vocab_size, hidden_size) + + self.LayerNorm = nn.LayerNorm(hidden_size, epsilon=1e-12) + self.dropout = nn.Dropout(hidden_dropout_prob) + + def forward(self, input_ids, token_type_ids=None): + seq_length = input_ids.shape[1] + position_ids = paddle.arange(end=seq_length, dtype="int64") + position_ids = position_ids.unsqueeze(0).expand_as(input_ids) + if token_type_ids is None: + token_type_ids = paddle.zeros_like(input_ids) + + words_embeddings = self.word_embeddings(input_ids) #8,36 -> 8,36,768 + position_embeddings = self.position_embeddings( + position_ids) #8,36 -> 8,36,768 + token_type_embeddings = self.token_type_embeddings( + token_type_ids) #8,36 -> 8,36,768 + + embeddings = words_embeddings + position_embeddings + token_type_embeddings + embeddings = self.LayerNorm(embeddings) + embeddings = self.dropout(embeddings) + return embeddings + + +class BertImageEmbeddings(nn.Layer): + def __init__(self, v_feature_size, v_hidden_size, v_hidden_dropout_prob): + super(BertImageEmbeddings, self).__init__() + self.image_embeddings = nn.Linear(v_feature_size, v_hidden_size) + self.image_location_embeddings = nn.Linear(5, v_hidden_size) + self.LayerNorm = nn.LayerNorm(v_hidden_size, epsilon=1e-12) + self.dropout = nn.Dropout(v_hidden_dropout_prob) + + def forward(self, input_ids, input_loc): + img_embeddings = self.image_embeddings( + input_ids) #8,37,2048 -> 8,37,1024 + loc_embeddings = self.image_location_embeddings( + input_loc) #8,37,5 -> 8,37,1024 + embeddings = self.LayerNorm(img_embeddings + loc_embeddings) + embeddings = self.dropout(embeddings) + return embeddings # shape: bs*seq_len*hs + + +class BertActionEmbeddings(nn.Layer): + def __init__(self, a_feature_size, a_hidden_size, a_hidden_dropout_prob): + super(BertActionEmbeddings, self).__init__() + self.action_embeddings = nn.Linear(a_feature_size, a_hidden_size) + self.LayerNorm = nn.LayerNorm(a_hidden_size, epsilon=1e-12) + self.dropout = nn.Dropout(a_hidden_dropout_prob) + + def forward(self, input_ids): + action_embeddings = self.action_embeddings( + input_ids) #8,5,2048 -> 8,5,768 + embeddings = self.LayerNorm(action_embeddings) + embeddings = self.dropout(embeddings) + return embeddings + + +class BertSelfAttention(nn.Layer): + def __init__(self, hidden_size, num_attention_heads, + attention_probs_dropout_prob): + super(BertSelfAttention, self).__init__() + if hidden_size % num_attention_heads != 0: + raise ValueError( + "The hidden size (%d) is not a multiple of the number of attention " + "heads (%d)" % (hidden_size, num_attention_heads)) + self.num_attention_heads = num_attention_heads + self.attention_head_size = int(hidden_size / num_attention_heads) + self.all_head_size = self.num_attention_heads * self.attention_head_size + + self.query = nn.Linear(hidden_size, self.all_head_size) + self.key = nn.Linear(hidden_size, self.all_head_size) + self.value = nn.Linear(hidden_size, self.all_head_size) + + self.dropout = nn.Dropout(attention_probs_dropout_prob) + + def transpose_for_scores(self, x): + new_x_shape = x.shape[:-1] + [ + self.num_attention_heads, + self.attention_head_size, + ] + x = x.reshape(new_x_shape) + return x.transpose((0, 2, 1, 3)) + + def forward(self, hidden_states, attention_mask): + mixed_query_layer = self.query(hidden_states) + mixed_key_layer = self.key(hidden_states) + mixed_value_layer = self.value(hidden_states) + + query_layer = self.transpose_for_scores(mixed_query_layer) + key_layer = self.transpose_for_scores(mixed_key_layer) + value_layer = self.transpose_for_scores(mixed_value_layer) + + # Take the dot product between "query" and "key" to get the raw attention scores. + attention_scores = paddle.matmul(query_layer, + key_layer.transpose((0, 1, 3, 2))) + attention_scores = attention_scores / math.sqrt( + self.attention_head_size) + # Apply the attention mask is (precomputed for all layers in BertModel forward() function) + attention_scores = attention_scores + attention_mask + + # Normalize the attention scores to probabilities. + attention_probs = nn.Softmax(axis=-1)(attention_scores) + + # This is actually dropping out entire tokens to attend to, which might + # seem a bit unusual, but is taken from the original Transformer paper. + attention_probs = self.dropout(attention_probs) + + context_layer = paddle.matmul(attention_probs, value_layer) + context_layer = context_layer.transpose((0, 2, 1, 3)) + new_context_layer_shape = context_layer.shape[:-2] + [ + self.all_head_size + ] + context_layer = context_layer.reshape(new_context_layer_shape) + + return context_layer, attention_probs + + +class BertSelfOutput(nn.Layer): + def __init__(self, hidden_size, hidden_dropout_prob): + super(BertSelfOutput, self).__init__() + self.dense = nn.Linear(hidden_size, hidden_size) + self.LayerNorm = nn.LayerNorm(hidden_size, epsilon=1e-12) + self.dropout = nn.Dropout(hidden_dropout_prob) + + def forward(self, hidden_states, input_tensor): + hidden_states = self.dense(hidden_states) + hidden_states = self.dropout(hidden_states) + hidden_states = self.LayerNorm(hidden_states + input_tensor) + return hidden_states + + +class BertAttention(nn.Layer): + def __init__(self, hidden_size, hidden_dropout_prob, num_attention_heads, + attention_probs_dropout_prob): + super(BertAttention, self).__init__() + self.self = BertSelfAttention(hidden_size, num_attention_heads, + attention_probs_dropout_prob) + self.output = BertSelfOutput(hidden_size, hidden_dropout_prob) + + def forward(self, input_tensor, attention_mask): + self_output, attention_probs = self.self(input_tensor, attention_mask) + attention_output = self.output(self_output, input_tensor) + return attention_output, attention_probs + + +class BertIntermediate(nn.Layer): + def __init__(self, hidden_size, intermediate_size, hidden_act): + super(BertIntermediate, self).__init__() + self.dense = nn.Linear(hidden_size, intermediate_size) + if isinstance(hidden_act, str) or (sys.version_info[0] == 2 + and isinstance(hidden_act, str)): + self.intermediate_act_fn = ACT2FN[hidden_act] + else: + self.intermediate_act_fn = hidden_act + + def forward(self, hidden_states): + hidden_states = self.dense(hidden_states) + hidden_states = self.intermediate_act_fn(hidden_states) + return hidden_states + + +class BertOutput(nn.Layer): + def __init__(self, intermediate_size, hidden_size, hidden_dropout_prob): + super(BertOutput, self).__init__() + self.dense = nn.Linear(intermediate_size, hidden_size) + self.LayerNorm = nn.LayerNorm(hidden_size, epsilon=1e-12) + self.dropout = nn.Dropout(hidden_dropout_prob) + + def forward(self, hidden_states, input_tensor): + hidden_states = self.dense(hidden_states) + hidden_states = self.dropout(hidden_states) + hidden_states = self.LayerNorm(hidden_states + input_tensor) + return hidden_states + + +class BertEntAttention(nn.Layer): + """Core mudule of tangled transformer. + """ + def __init__( + self, + hidden_size, + v_hidden_size, + a_hidden_size, + bi_hidden_size, + attention_probs_dropout_prob, + v_attention_probs_dropout_prob, + a_attention_probs_dropout_prob, + av_attention_probs_dropout_prob, + at_attention_probs_dropout_prob, + bi_num_attention_heads, + ): + super(BertEntAttention, self).__init__() + if bi_hidden_size % bi_num_attention_heads != 0: + raise ValueError( + "The hidden size (%d) is not a multiple of the number of attention " + "heads (%d)" % (bi_hidden_size, bi_num_attention_heads)) + + self.num_attention_heads = bi_num_attention_heads + self.attention_head_size = int(bi_hidden_size / bi_num_attention_heads) + self.all_head_size = self.num_attention_heads * self.attention_head_size + + # self attention layers for vision input + self.query1 = nn.Linear(v_hidden_size, self.all_head_size) + self.key1 = nn.Linear(v_hidden_size, self.all_head_size) + self.value1 = nn.Linear(v_hidden_size, self.all_head_size) + self.dropout1 = nn.Dropout(v_attention_probs_dropout_prob) + + # self attention layers for text input + self.query2 = nn.Linear(hidden_size, self.all_head_size) + self.key2 = nn.Linear(hidden_size, self.all_head_size) + self.value2 = nn.Linear(hidden_size, self.all_head_size) + self.dropout2 = nn.Dropout(attention_probs_dropout_prob) + + # self attention layers for action input + self.query3 = nn.Linear(a_hidden_size, self.all_head_size) + self.key3 = nn.Linear(a_hidden_size, self.all_head_size) + self.value3 = nn.Linear(a_hidden_size, self.all_head_size) + self.dropout3 = nn.Dropout(a_attention_probs_dropout_prob) + + # self attention layers for action_text + self.key_at = nn.Linear(bi_hidden_size, self.all_head_size) + self.value_at = nn.Linear(bi_hidden_size, self.all_head_size) + self.dropout_at = nn.Dropout(av_attention_probs_dropout_prob) + + # self attention layers for action_vision + self.key_av = nn.Linear(bi_hidden_size, self.all_head_size) + self.value_av = nn.Linear(bi_hidden_size, self.all_head_size) + self.dropout_av = nn.Dropout(at_attention_probs_dropout_prob) + + def transpose_for_scores(self, x): + new_x_shape = x.shape[:-1] + [ + self.num_attention_heads, + self.attention_head_size, + ] + x = x.reshape(new_x_shape) + return x.transpose((0, 2, 1, 3)) + + def forward( + self, + input_tensor1, + attention_mask1, + input_tensor2, + attention_mask2, + input_tensor3, + attention_mask3, + ): + + # for vision input. + mixed_query_layer1 = self.query1(input_tensor1) + mixed_key_layer1 = self.key1(input_tensor1) + mixed_value_layer1 = self.value1(input_tensor1) + + query_layer1 = self.transpose_for_scores(mixed_query_layer1) + key_layer1 = self.transpose_for_scores(mixed_key_layer1) + value_layer1 = self.transpose_for_scores(mixed_value_layer1) + + # for text input: + mixed_query_layer2 = self.query2(input_tensor2) + mixed_key_layer2 = self.key2(input_tensor2) + mixed_value_layer2 = self.value2(input_tensor2) + + query_layer2 = self.transpose_for_scores(mixed_query_layer2) + key_layer2 = self.transpose_for_scores(mixed_key_layer2) + value_layer2 = self.transpose_for_scores(mixed_value_layer2) + + # for action input: + mixed_query_layer3 = self.query3(input_tensor3) + mixed_key_layer3 = self.key3(input_tensor3) + mixed_value_layer3 = self.value3(input_tensor3) + + query_layer3 = self.transpose_for_scores(mixed_query_layer3) + key_layer3 = self.transpose_for_scores(mixed_key_layer3) + value_layer3 = self.transpose_for_scores(mixed_value_layer3) + + def do_attention(query_layer, key_layer, value_layer, attention_mask, + dropout): + """ compute attention """ + attention_scores = paddle.matmul(query_layer, + key_layer.transpose((0, 1, 3, 2))) + attention_scores = attention_scores / math.sqrt( + self.attention_head_size) + attention_scores = attention_scores + attention_mask + + # Normalize the attention scores to probabilities. + attention_probs = nn.Softmax(axis=-1)(attention_scores) + + # This is actually dropping out entire tokens to attend to, which might + # seem a bit unusual, but is taken from the original Transformer paper. + attention_probs = dropout(attention_probs) + + context_layer = paddle.matmul(attention_probs, value_layer) + context_layer = context_layer.transpose((0, 2, 1, 3)) + new_context_layer_shape = context_layer.shape[:-2] + [ + self.all_head_size + ] + context_layer = context_layer.reshape(new_context_layer_shape) + return context_layer + + context_av = do_attention(query_layer3, key_layer1, value_layer1, + attention_mask1, self.dropout_av) + context_at = do_attention(query_layer3, key_layer2, value_layer2, + attention_mask2, self.dropout_at) + + context_key_av = self.key_av(context_av).transpose((0, 2, 1)) + # interpolate only support 4-D tensor now. + context_key_av = F.interpolate(context_key_av.unsqueeze(-1), + size=(key_layer2.shape[2], + 1)).squeeze(-1) + context_key_av = self.transpose_for_scores( + context_key_av.transpose((0, 2, 1))) + key_layer2 = key_layer2 + context_key_av + + context_key_at = self.key_at(context_at).transpose((0, 2, 1)) + context_key_at = F.interpolate(context_key_at.unsqueeze(-1), + size=(key_layer1.shape[2], + 1)).squeeze(-1) + context_key_at = self.transpose_for_scores( + context_key_at.transpose((0, 2, 1))) + key_layer1 = key_layer1 + context_key_at + + context_val_av = self.value_at(context_av).transpose((0, 2, 1)) + context_val_av = F.interpolate(context_val_av.unsqueeze(-1), + size=(value_layer2.shape[2], + 1)).squeeze(-1) + context_val_av = self.transpose_for_scores( + context_val_av.transpose((0, 2, 1))) + value_layer2 = value_layer2 + context_val_av + + context_val_at = self.value_at(context_at).transpose((0, 2, 1)) + context_val_at = F.interpolate(context_val_at.unsqueeze(-1), + size=(value_layer1.shape[2], + 1)).squeeze(-1) + context_val_at = self.transpose_for_scores( + context_val_at.transpose((0, 2, 1))) + value_layer1 = value_layer1 + context_val_at + + context_layer1 = do_attention(query_layer1, key_layer1, value_layer1, + attention_mask1, self.dropout1) + context_layer2 = do_attention(query_layer2, key_layer2, value_layer2, + attention_mask2, self.dropout2) + context_layer3 = do_attention(query_layer3, key_layer3, value_layer3, + attention_mask3, self.dropout3) + + return context_layer1, context_layer2, context_layer3 # vision, text, action + + +class BertEntOutput(nn.Layer): + def __init__( + self, + bi_hidden_size, + hidden_size, + v_hidden_size, + v_hidden_dropout_prob, + hidden_dropout_prob, + ): + super(BertEntOutput, self).__init__() + + self.dense1 = nn.Linear(bi_hidden_size, v_hidden_size) + self.LayerNorm1 = nn.LayerNorm(v_hidden_size, epsilon=1e-12) + self.dropout1 = nn.Dropout(v_hidden_dropout_prob) + + self.dense2 = nn.Linear(bi_hidden_size, hidden_size) + self.LayerNorm2 = nn.LayerNorm(hidden_size, epsilon=1e-12) + self.dropout2 = nn.Dropout(hidden_dropout_prob) + + self.dense3 = nn.Linear(bi_hidden_size, hidden_size) + self.LayerNorm3 = nn.LayerNorm(hidden_size, epsilon=1e-12) + self.dropout3 = nn.Dropout(hidden_dropout_prob) + + def forward( + self, + hidden_states1, + input_tensor1, + hidden_states2, + input_tensor2, + hidden_states3, + input_tensor3, + ): + context_state1 = self.dense1(hidden_states1) + context_state1 = self.dropout1(context_state1) + + context_state2 = self.dense2(hidden_states2) + context_state2 = self.dropout2(context_state2) + + context_state3 = self.dense3(hidden_states3) + context_state3 = self.dropout3(context_state3) + + hidden_states1 = self.LayerNorm1(context_state1 + input_tensor1) + hidden_states2 = self.LayerNorm2(context_state2 + input_tensor2) + hidden_states3 = self.LayerNorm3(context_state3 + input_tensor3) + + return hidden_states1, hidden_states2, hidden_states3 + + +class BertLayer(nn.Layer): + def __init__(self, hidden_size, intermediate_size, hidden_act, + hidden_dropout_prob, num_attention_heads, + attention_probs_dropout_prob): + super(BertLayer, self).__init__() + self.attention = BertAttention(hidden_size, hidden_dropout_prob, + num_attention_heads, + attention_probs_dropout_prob) + self.intermediate = BertIntermediate(hidden_size, intermediate_size, + hidden_act) + self.output = BertOutput(intermediate_size, hidden_size, + hidden_dropout_prob) + + def forward(self, hidden_states, attention_mask): + attention_output, attention_probs = self.attention( + hidden_states, attention_mask) + intermediate_output = self.intermediate(attention_output) + layer_output = self.output(intermediate_output, attention_output) + return layer_output, attention_probs + + +class BertConnectionLayer(nn.Layer): + def __init__(self, hidden_size, v_hidden_size, a_hidden_size, + bi_hidden_size, bi_num_attention_heads, + attention_probs_dropout_prob, v_attention_probs_dropout_prob, + a_attention_probs_dropout_prob, + av_attention_probs_dropout_prob, + at_attention_probs_dropout_prob, intermediate_size, + v_intermediate_size, a_intermediate_size, hidden_act, + v_hidden_act, a_hidden_act, hidden_dropout_prob, + v_hidden_dropout_prob, a_hidden_dropout_prob): + super(BertConnectionLayer, self).__init__() + self.ent_attention = BertEntAttention( + hidden_size, + v_hidden_size, + a_hidden_size, + bi_hidden_size, + attention_probs_dropout_prob, + v_attention_probs_dropout_prob, + a_attention_probs_dropout_prob, + av_attention_probs_dropout_prob, + at_attention_probs_dropout_prob, + bi_num_attention_heads, + ) + + self.ent_output = BertEntOutput( + bi_hidden_size, + hidden_size, + v_hidden_size, + v_hidden_dropout_prob, + hidden_dropout_prob, + ) + + self.v_intermediate = BertIntermediate(v_hidden_size, + v_intermediate_size, + v_hidden_act) + self.v_output = BertOutput(v_intermediate_size, v_hidden_size, + v_hidden_dropout_prob) + + self.t_intermediate = BertIntermediate(hidden_size, intermediate_size, + hidden_act) + self.t_output = BertOutput(intermediate_size, hidden_size, + hidden_dropout_prob) + + self.a_intermediate = BertIntermediate(a_hidden_size, + a_intermediate_size, + a_hidden_act) + self.a_output = BertOutput(a_intermediate_size, a_hidden_size, + a_hidden_dropout_prob) + + def forward( + self, + input_tensor1, + attention_mask1, + input_tensor2, + attention_mask2, + input_tensor3, + attention_mask3, + ): + + ent_output1, ent_output2, ent_output3 = self.ent_attention( + input_tensor1, attention_mask1, input_tensor2, attention_mask2, + input_tensor3, attention_mask3) + + attention_output1, attention_output2, attention_output3 = self.ent_output( + ent_output1, input_tensor1, ent_output2, input_tensor2, ent_output3, + input_tensor3) + + intermediate_output1 = self.v_intermediate(attention_output1) + layer_output1 = self.v_output(intermediate_output1, attention_output1) + + intermediate_output2 = self.t_intermediate(attention_output2) + layer_output2 = self.t_output(intermediate_output2, attention_output2) + + intermediate_output3 = self.a_intermediate(attention_output3) + layer_output3 = self.a_output(intermediate_output3, attention_output3) + + return layer_output1, layer_output2, layer_output3 + + +class BertEncoder(nn.Layer): + """ + ActBert Encoder, consists 3 pathway of multi-BertLayers and BertConnectionLayer. + """ + def __init__( + self, + v_ent_attention_id, + t_ent_attention_id, + a_ent_attention_id, + fixed_t_layer, + fixed_v_layer, + hidden_size, + v_hidden_size, + a_hidden_size, + bi_hidden_size, + intermediate_size, + v_intermediate_size, + a_intermediate_size, + hidden_act, + v_hidden_act, + a_hidden_act, + hidden_dropout_prob, + v_hidden_dropout_prob, + a_hidden_dropout_prob, + attention_probs_dropout_prob, + v_attention_probs_dropout_prob, + a_attention_probs_dropout_prob, + av_attention_probs_dropout_prob, + at_attention_probs_dropout_prob, + num_attention_heads, + v_num_attention_heads, + a_num_attention_heads, + bi_num_attention_heads, + num_hidden_layers, + v_num_hidden_layers, + a_num_hidden_layers, + ): + super(BertEncoder, self).__init__() + self.v_ent_attention_id = v_ent_attention_id + self.t_ent_attention_id = t_ent_attention_id + self.a_ent_attention_id = a_ent_attention_id + self.fixed_t_layer = fixed_t_layer + self.fixed_v_layer = fixed_v_layer + + layer = BertLayer(hidden_size, intermediate_size, hidden_act, + hidden_dropout_prob, num_attention_heads, + attention_probs_dropout_prob) + v_layer = BertLayer(v_hidden_size, v_intermediate_size, v_hidden_act, + v_hidden_dropout_prob, v_num_attention_heads, + v_attention_probs_dropout_prob) + a_layer = BertLayer(a_hidden_size, a_intermediate_size, a_hidden_act, + a_hidden_dropout_prob, a_num_attention_heads, + a_attention_probs_dropout_prob) + connect_layer = BertConnectionLayer( + hidden_size, v_hidden_size, a_hidden_size, bi_hidden_size, + bi_num_attention_heads, attention_probs_dropout_prob, + v_attention_probs_dropout_prob, a_attention_probs_dropout_prob, + av_attention_probs_dropout_prob, at_attention_probs_dropout_prob, + intermediate_size, v_intermediate_size, a_intermediate_size, + hidden_act, v_hidden_act, a_hidden_act, hidden_dropout_prob, + v_hidden_dropout_prob, a_hidden_dropout_prob) + + self.layer = nn.LayerList( + [copy.deepcopy(layer) for _ in range(num_hidden_layers)]) #12 + self.v_layer = nn.LayerList( + [copy.deepcopy(v_layer) for _ in range(v_num_hidden_layers)]) #2 + self.a_layer = nn.LayerList( + [copy.deepcopy(a_layer) for _ in range(a_num_hidden_layers)]) #3 + self.c_layer = nn.LayerList([ + copy.deepcopy(connect_layer) for _ in range(len(v_ent_attention_id)) + ] #2 [0,1] + ) + + def forward( + self, + txt_embedding, + image_embedding, + action_embedding, + txt_attention_mask, + image_attention_mask, + action_attention_mask, + output_all_encoded_layers=True, + ): + v_start, a_start, t_start = 0, 0, 0 + count = 0 + all_encoder_layers_t = [] + all_encoder_layers_v = [] + all_encoder_layers_a = [] + + for v_layer_id, a_layer_id, t_layer_id in zip(self.v_ent_attention_id, + self.a_ent_attention_id, + self.t_ent_attention_id): + v_end = v_layer_id + a_end = a_layer_id + t_end = t_layer_id + + assert self.fixed_t_layer <= t_end + assert self.fixed_v_layer <= v_end + + ### region embedding + for idx in range(v_start, + self.fixed_v_layer): #两次训练,这个循环都没有进去 #前面的层固定住 + with paddle.no_grad(): + image_embedding, image_attention_probs = self.v_layer[idx]( + image_embedding, image_attention_mask) + v_start = self.fixed_v_layer + for idx in range(v_start, v_end): + image_embedding, image_attention_probs = self.v_layer[idx]( + image_embedding, image_attention_mask) + + ### action embedding + for idx in range(a_start, a_end): + action_embedding, action_attention_probs = self.a_layer[idx]( + action_embedding, action_attention_mask) + + ### text embedding + for idx in range(t_start, self.fixed_t_layer): + with paddle.no_grad(): + txt_embedding, txt_attention_probs = self.layer[idx]( + txt_embedding, txt_attention_mask) + t_start = self.fixed_t_layer + for idx in range(t_start, t_end): + txt_embedding, txt_attention_probs = self.layer[idx]( + txt_embedding, txt_attention_mask) + + image_embedding, txt_embedding, action_embedding = self.c_layer[ + count](image_embedding, image_attention_mask, txt_embedding, + txt_attention_mask, action_embedding, + action_attention_mask) + + v_start = v_end + t_start = t_end + a_start = a_end + count += 1 + + if output_all_encoded_layers: + all_encoder_layers_t.append(txt_embedding) + all_encoder_layers_v.append(image_embedding) + all_encoder_layers_a.append(action_embedding) + + for idx in range(v_start, len(self.v_layer)): # 1 + image_embedding, image_attention_probs = self.v_layer[idx]( + image_embedding, image_attention_mask) + + for idx in range(a_start, len(self.a_layer)): + action_embedding, action_attention_probs = self.a_layer[idx]( + action_embedding, action_attention_mask) + + for idx in range(t_start, len(self.layer)): + txt_embedding, txt_attention_probs = self.layer[idx]( + txt_embedding, txt_attention_mask) + + # add the end part to finish. + if not output_all_encoded_layers: + all_encoder_layers_t.append(txt_embedding) #8, 36, 768 + all_encoder_layers_v.append(image_embedding) #8, 37, 1024 + all_encoder_layers_a.append(action_embedding) #8, 5, 768 + + return all_encoder_layers_t, all_encoder_layers_v, all_encoder_layers_a + + +class BertPooler(nn.Layer): + """ "Pool" the model by simply taking the hidden state corresponding + to the first token. + """ + def __init__(self, hidden_size, bi_hidden_size): + super(BertPooler, self).__init__() + self.dense = nn.Linear(hidden_size, bi_hidden_size) + self.activation = nn.ReLU() + + def forward(self, hidden_states): + first_token_tensor = hidden_states[:, 0] #8, 768 + pooled_output = self.dense(first_token_tensor) + pooled_output = self.activation(pooled_output) + return pooled_output + + +class BertModel(nn.Layer): + def __init__( + self, + vocab_size, + max_position_embeddings, + type_vocab_size, + v_feature_size, + a_feature_size, + num_hidden_layers, + v_num_hidden_layers, + a_num_hidden_layers, + v_ent_attention_id, + t_ent_attention_id, + a_ent_attention_id, + fixed_t_layer, + fixed_v_layer, + hidden_size, + v_hidden_size, + a_hidden_size, + bi_hidden_size, + intermediate_size, + v_intermediate_size, + a_intermediate_size, + hidden_act, + v_hidden_act, + a_hidden_act, + hidden_dropout_prob, + v_hidden_dropout_prob, + a_hidden_dropout_prob, + attention_probs_dropout_prob, + v_attention_probs_dropout_prob, + a_attention_probs_dropout_prob, + av_attention_probs_dropout_prob, + at_attention_probs_dropout_prob, + num_attention_heads, + v_num_attention_heads, + a_num_attention_heads, + bi_num_attention_heads, + ): + super(BertModel, self).__init__() + # initilize word embedding + self.embeddings = BertEmbeddings(vocab_size, max_position_embeddings, + type_vocab_size, hidden_size, + hidden_dropout_prob) + # initlize the region embedding + self.v_embeddings = BertImageEmbeddings(v_feature_size, v_hidden_size, + v_hidden_dropout_prob) + # initlize the action embedding + self.a_embeddings = BertActionEmbeddings(a_feature_size, a_hidden_size, + a_hidden_dropout_prob) + + self.encoder = BertEncoder( + v_ent_attention_id, t_ent_attention_id, a_ent_attention_id, + fixed_t_layer, fixed_v_layer, hidden_size, v_hidden_size, + a_hidden_size, bi_hidden_size, intermediate_size, + v_intermediate_size, a_intermediate_size, hidden_act, v_hidden_act, + a_hidden_act, hidden_dropout_prob, v_hidden_dropout_prob, + a_hidden_dropout_prob, attention_probs_dropout_prob, + v_attention_probs_dropout_prob, a_attention_probs_dropout_prob, + av_attention_probs_dropout_prob, at_attention_probs_dropout_prob, + num_attention_heads, v_num_attention_heads, a_num_attention_heads, + bi_num_attention_heads, num_hidden_layers, v_num_hidden_layers, + a_num_hidden_layers) + + self.t_pooler = BertPooler(hidden_size, bi_hidden_size) + self.v_pooler = BertPooler(v_hidden_size, bi_hidden_size) + self.a_pooler = BertPooler(a_hidden_size, bi_hidden_size) + + def forward( + self, + text_ids, + action_feat, + image_feat, + image_loc, + token_type_ids=None, + text_mask=None, + image_mask=None, + action_mask=None, + output_all_encoded_layers=False, + ): + """ + text_ids: input text ids. Shape: [batch_size, seqence_length] + action_feat: input action feature. Shape: [batch_size, action_length, action_feature_dim] + image_feat: input image feature. Shape: [batch_size, region_length, image_feature_dim]] + image_loc: input region location. Shape: [batch_size, region_length, region_location_dim] + token_type_ids: segment ids of each video clip. Shape: [batch_size, seqence_length] + text_mask: text mask, 1 for real tokens and 0 for padding tokens. Shape: [batch_size, seqence_length] + image_mask: image mask, 1 for real tokens and 0 for padding tokens. Shape: [batch_size, region_length] + action_mask: action mask, 1 for real tokens and 0 for padding tokens. Shape: [batch_size, action_length] + output_all_encoded_layers: is output encoded layers feature or not. Type: Bool. + """ + if text_mask is None: + text_mask = paddle.ones_like(text_ids) + if token_type_ids is None: + token_type_ids = paddle.zeros_like(text_ids) + if image_mask is None: + image_mask = paddle.ones(image_feat.shape[0], + image_feat.shape[1]).astype(text_ids.dtype) + if action_mask is None: + action_mask = paddle.ones(action_feat.shape[0], + action_feat.shape[1]).astype( + text_ids.dtype) + + # We create a 3D attention mask from a 2D tensor mask. + # Sizes are [batch_size, 1, 1, to_seq_length] + # So we can broadcast to [batch_size, num_heads, from_seq_length, to_seq_length]. + extended_text_mask = text_mask.unsqueeze(1).unsqueeze(2) + extended_image_mask = image_mask.unsqueeze(1).unsqueeze(2) + extended_action_mask = action_mask.unsqueeze(1).unsqueeze(2) + + # Since attention_mask is 1.0 for positions we want to attend and 0.0 for + # masked positions, this operation will create a tensor which is 0.0 for + # positions we want to attend and -10000.0 for masked positions. + # Since we are adding it to the raw scores before the softmax, this is + # effectively the same as removing these entirely. + def set_mask(extended_attention_mask): + extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0 + return extended_attention_mask + + extended_text_mask = set_mask(extended_text_mask) + extended_image_mask = set_mask(extended_image_mask) + extended_action_mask = set_mask(extended_action_mask) + + t_embedding_output = self.embeddings(text_ids, token_type_ids) + v_embedding_output = self.v_embeddings(image_feat, image_loc) + a_embedding_output = self.a_embeddings(action_feat) + + # var = [t_embedding_output, v_embedding_output, a_embedding_output] + # import numpy as np + # for i, item in enumerate(var): + # np.save('tmp/' + str(i)+'.npy', item.numpy()) + + encoded_layers_t, encoded_layers_v, encoded_layers_a = self.encoder( + t_embedding_output, + v_embedding_output, + a_embedding_output, + extended_text_mask, + extended_image_mask, + extended_action_mask, + output_all_encoded_layers=output_all_encoded_layers, + ) + + sequence_output_t = encoded_layers_t[-1] #get item from list + sequence_output_v = encoded_layers_v[-1] + sequence_output_a = encoded_layers_a[-1] + + pooled_output_t = self.t_pooler(sequence_output_t) + pooled_output_v = self.v_pooler(sequence_output_v) + pooled_output_a = self.a_pooler(sequence_output_a) + + if not output_all_encoded_layers: + encoded_layers_t = encoded_layers_t[-1] + encoded_layers_v = encoded_layers_v[-1] + encoded_layers_a = encoded_layers_a[-1] + + return encoded_layers_t, encoded_layers_v, encoded_layers_a, \ + pooled_output_t, pooled_output_v, pooled_output_a + + +# For Head +class BertPredictionHeadTransform(nn.Layer): + def __init__(self, hidden_size, hidden_act): + super(BertPredictionHeadTransform, self).__init__() + self.dense = nn.Linear(hidden_size, hidden_size) + if isinstance(hidden_act, str) or (sys.version_info[0] == 2 + and isinstance(hidden_act, str)): + self.transform_act_fn = ACT2FN[hidden_act] + else: + self.transform_act_fn = hidden_act + self.LayerNorm = nn.LayerNorm(hidden_size, epsilon=1e-12) + + def forward(self, hidden_states): + hidden_states = self.dense(hidden_states) + hidden_states = self.transform_act_fn(hidden_states) + hidden_states = self.LayerNorm(hidden_states) + return hidden_states + + +class BertLMPredictionHead(nn.Layer): + def __init__(self, hidden_size, hidden_act, bert_model_embedding_weights): + super(BertLMPredictionHead, self).__init__() + self.transform = BertPredictionHeadTransform(hidden_size, hidden_act) + + # The output weights are the same as the input embeddings, but there is + # an output-only bias for each token. + assert bert_model_embedding_weights.shape[1] == hidden_size + vocab_size = bert_model_embedding_weights.shape[0] + + # another implementation which would create another big params: + # self.decoder = nn.Linear(hidden_size, vocab_size) # NOTE bias default: constant 0.0 + # self.decoder.weight = self.create_parameter(shape=[hidden_size, vocab_size], + # default_initializer=nn.initializer.Assign( + # bert_model_embedding_weights.t())) # transpose + + self.decoder_weight = bert_model_embedding_weights + self.decoder_bias = self.create_parameter( + shape=[vocab_size], + dtype=bert_model_embedding_weights.dtype, + is_bias=True) # NOTE bias default: constant 0.0 + + def forward(self, hidden_states): + hidden_states = self.transform(hidden_states) + hidden_states = paddle.tensor.matmul( + hidden_states, self.decoder_weight, + transpose_y=True) + self.decoder_bias + return hidden_states + + +class BertImageActionPredictionHead(nn.Layer): + def __init__(self, hidden_size, hidden_act, target_size): + super(BertImageActionPredictionHead, self).__init__() + self.transform = BertPredictionHeadTransform(hidden_size, hidden_act) + + self.decoder = nn.Linear(hidden_size, target_size) + + def forward(self, hidden_states): + hidden_states = self.transform(hidden_states) + hidden_states = self.decoder(hidden_states) + return hidden_states + + +class BertPreTrainingHeads(nn.Layer): + def __init__(self, hidden_size, v_hidden_size, a_hidden_size, + bi_hidden_size, hidden_act, v_hidden_act, a_hidden_act, + v_target_size, a_target_size, fusion_method, + bert_model_embedding_weights): + super(BertPreTrainingHeads, self).__init__() + self.predictions = BertLMPredictionHead(hidden_size, hidden_act, + bert_model_embedding_weights) + self.seq_relationship = nn.Linear(bi_hidden_size, 2) + self.imagePredictions = BertImageActionPredictionHead( + v_hidden_size, v_hidden_act, v_target_size) # visual class number + self.actionPredictions = BertImageActionPredictionHead( + a_hidden_size, a_hidden_act, a_target_size) # action class number + self.fusion_method = fusion_method + self.dropout = nn.Dropout(0.1) + + def forward(self, sequence_output_t, sequence_output_v, sequence_output_a, + pooled_output_t, pooled_output_v, pooled_output_a): + + if self.fusion_method == 'sum': + pooled_output = self.dropout(pooled_output_t + pooled_output_v + + pooled_output_a) + elif self.fusion_method == 'mul': + pooled_output = self.dropout(pooled_output_t * pooled_output_v + + pooled_output_a) + else: + assert False + + prediction_scores_t = self.predictions( + sequence_output_t) # 8, 36 ,30522 + seq_relationship_score = self.seq_relationship(pooled_output) # 8, 2 + prediction_scores_v = self.imagePredictions( + sequence_output_v) # 8, 37, 1601 + prediction_scores_a = self.actionPredictions( + sequence_output_a) # 8, 5, 401 + + return prediction_scores_t, prediction_scores_v, prediction_scores_a, seq_relationship_score + + +@BACKBONES.register() +class BertForMultiModalPreTraining(nn.Layer): + """BERT model with multi modal pre-training heads. + """ + def __init__( + self, + vocab_size=30522, + max_position_embeddings=512, + type_vocab_size=2, + v_target_size=1601, + a_target_size=700, + v_feature_size=2048, + a_feature_size=2048, + num_hidden_layers=12, + v_num_hidden_layers=2, + a_num_hidden_layers=3, + t_ent_attention_id=[10, 11], + v_ent_attention_id=[0, 1], + a_ent_attention_id=[0, 1], + fixed_t_layer=0, + fixed_v_layer=0, + hidden_size=768, + v_hidden_size=1024, + a_hidden_size=768, + bi_hidden_size=1024, + intermediate_size=3072, + v_intermediate_size=1024, + a_intermediate_size=3072, + hidden_act="gelu", + v_hidden_act="gelu", + a_hidden_act="gelu", + hidden_dropout_prob=0.1, + v_hidden_dropout_prob=0.1, + a_hidden_dropout_prob=0.1, + attention_probs_dropout_prob=0.1, + v_attention_probs_dropout_prob=0.1, + a_attention_probs_dropout_prob=0.1, + av_attention_probs_dropout_prob=0.1, + at_attention_probs_dropout_prob=0.1, + num_attention_heads=12, + v_num_attention_heads=8, + a_num_attention_heads=12, + bi_num_attention_heads=8, + fusion_method="mul", + pretrained=None, + ): + """ + vocab_size: vocabulary size. Default: 30522. + max_position_embeddings: max position id. Default: 512. + type_vocab_size: max segment id. Default: 2. + v_target_size: class number of visual word. Default: 1601. + a_target_size: class number of action word. Default: 700. + v_feature_size: input visual feature dimension. Default: 2048. + a_feature_size: input action feature dimension. Default: 2048. + num_hidden_layers: number of BertLayer in text transformer. Default: 12. + v_num_hidden_layers: number of BertLayer in visual transformer. Default: 2. + a_num_hidden_layers: number of BertLayer in action transformer. Default:3. + t_ent_attention_id: index id of BertConnectionLayer in text transformer. Default: [10, 11]. + v_ent_attention_id: index id of BertConnectionLayer in visual transformer. Default:[0, 1]. + a_ent_attention_id: index id of BertConnectionLayer in action transformer. Default:[0, 1]. + fixed_t_layer: index id of fixed BertLayer in text transformer. Default: 0. + fixed_v_layer: index id of fixed BertLayer in visual transformer. Default: 0. + hidden_size: hidden size in text BertLayer. Default: 768. + v_hidden_size: hidden size in visual BertLayer. Default: 1024. + a_hidden_size: hidden size in action BertLayer. Default: 768. + bi_hidden_size: hidden size in BertConnectionLayer. Default: 1024, + intermediate_size: intermediate size in text BertLayer. Default: 3072. + v_intermediate_size: intermediate size in visual BertLayer. Default: 1024. + a_intermediate_size: intermediate size in text BertLayer. Default: 3072. + hidden_act: hidden activation function in text BertLayer. Default: "gelu". + v_hidden_act: hidden activation function in visual BertLayer. Default: "gelu". + a_hidden_act: hidden activation function in action BertLayer. Default: "gelu". + hidden_dropout_prob: hidden dropout probability in text Embedding Layer. Default: 0.1 + v_hidden_dropout_prob: hidden dropout probability in visual Embedding Layer. Default: 0.1 + a_hidden_dropout_prob: hidden dropout probability in action Embedding Layer. Default: 0.1 + attention_probs_dropout_prob: attention dropout probability in text BertLayer. Default: 0.1 + v_attention_probs_dropout_prob: attention dropout probability in visual BertLayer. Default: 0.1 + a_attention_probs_dropout_prob: attention dropout probability in action BertLayer. Default: 0.1 + av_attention_probs_dropout_prob: attention dropout probability in action-visual BertConnectionLayer. Default: 0.1 + at_attention_probs_dropout_prob: attention dropout probability in action-text BertConnectionLayer. Default: 0.1 + num_attention_heads: number of heads in text BertLayer. Default: 12. + v_num_attention_heads: number of heads in visual BertLayer. Default: 8. + a_num_attention_heads: number of heads in action BertLayer. Default: 12. + bi_num_attention_heads: number of heads in BertConnectionLayer. Default: 8. + fusion_method: methods of fusing pooled output from 3 transformer. Default: "mul". + """ + super(BertForMultiModalPreTraining, self).__init__() + self.pretrained = pretrained + self.vocab_size = vocab_size + self.a_target_size = a_target_size + + self.bert = BertModel( + vocab_size, + max_position_embeddings, + type_vocab_size, + v_feature_size, + a_feature_size, + num_hidden_layers, + v_num_hidden_layers, + a_num_hidden_layers, + v_ent_attention_id, + t_ent_attention_id, + a_ent_attention_id, + fixed_t_layer, + fixed_v_layer, + hidden_size, + v_hidden_size, + a_hidden_size, + bi_hidden_size, + intermediate_size, + v_intermediate_size, + a_intermediate_size, + hidden_act, + v_hidden_act, + a_hidden_act, + hidden_dropout_prob, + v_hidden_dropout_prob, + a_hidden_dropout_prob, + attention_probs_dropout_prob, + v_attention_probs_dropout_prob, + a_attention_probs_dropout_prob, + av_attention_probs_dropout_prob, + at_attention_probs_dropout_prob, + num_attention_heads, + v_num_attention_heads, + a_num_attention_heads, + bi_num_attention_heads, + ) + self.cls = BertPreTrainingHeads( + hidden_size, v_hidden_size, a_hidden_size, bi_hidden_size, + hidden_act, v_hidden_act, a_hidden_act, v_target_size, + a_target_size, fusion_method, + self.bert.embeddings.word_embeddings.weight) + + def init_weights(self): + """Initiate the parameters. + """ + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, (nn.Linear, nn.Embedding)): + weight_init_(layer, 'Normal', std=0.02) + elif isinstance(layer, nn.LayerNorm): + weight_init_(layer, 'Constant', value=1) + + def forward( + self, + text_ids, #8,36 + action_feat, #8,5,2048 + image_feat, #8,37,2048 + image_loc, #8,37,5 + token_type_ids=None, #8,36 + text_mask=None, #8,36 + image_mask=None, #8,37 + action_mask=None, #8,5 + ): + """ + text_ids: input text ids. Shape: [batch_size, seqence_length] + action_feat: input action feature. Shape: [batch_size, action_length, action_feature_dim] + image_feat: input image feature. Shape: [batch_size, region_length+1, image_feature_dim]], add 1 for image global feature. + image_loc: input region location. Shape: [batch_size, region_length+1, region_location_dim], add 1 for image global feature location. + token_type_ids: segment ids of each video clip. Shape: [batch_size, seqence_length] + text_mask: text mask, 1 for real tokens and 0 for padding tokens. Shape: [batch_size, seqence_length] + image_mask: image mask, 1 for real tokens and 0 for padding tokens. Shape: [batch_size, region_length] + action_mask: action mask, 1 for real tokens and 0 for padding tokens. Shape: [batch_size, action_length] + """ + sequence_output_t, sequence_output_v, sequence_output_a, \ + pooled_output_t, pooled_output_v, pooled_output_a = self.bert( + text_ids, + action_feat, + image_feat, + image_loc, + token_type_ids, + text_mask, + image_mask, + action_mask, + output_all_encoded_layers=False, + ) + + prediction_scores_t, prediction_scores_v, prediction_scores_a, seq_relationship_score = self.cls( + sequence_output_t, sequence_output_v, sequence_output_a, + pooled_output_t, pooled_output_v, pooled_output_a) + + return prediction_scores_t, prediction_scores_v, prediction_scores_a, seq_relationship_score diff --git a/paddlevideo/modeling/backbones/adds.py b/paddlevideo/modeling/backbones/adds.py new file mode 100644 index 0000000000000000000000000000000000000000..21cd212cb23a08fe8985d7ab1b15cb5f8f7596f7 --- /dev/null +++ b/paddlevideo/modeling/backbones/adds.py @@ -0,0 +1,1146 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from collections import OrderedDict + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from paddle.nn import BatchNorm2D, Conv2D +from paddle.nn.initializer import Constant, Normal +from paddle.vision.models import ResNet + +from ...utils import load_ckpt +from ..registry import BACKBONES +from ..weight_init import kaiming_normal_, _calculate_fan_in_and_fan_out + +zeros_ = Constant(value=0.) +ones_ = Constant(value=1.) +normal_ = Normal(mean=0, std=1e-3) + + +def disp_to_depth(disp, min_depth, max_depth): + """Convert network's sigmoid output into depth prediction + The formula for this conversion is given in the 'additional considerations' + section of the paper. + """ + min_disp = 1 / max_depth + max_disp = 1 / min_depth + scaled_disp = min_disp + (max_disp - min_disp) * disp + depth = 1 / scaled_disp + return scaled_disp, depth + + +def gram_matrix(y): + (b, ch, h, w) = y.shape + features = y.reshape([b, ch, w * h]) + features_t = paddle.transpose(features, [0, 2, 1]) + gram = features.bmm(features_t) / (ch * h * w) + return gram + + +def convt_bn_relu(in_channels, + out_channels, + kernel_size, + stride=1, + padding=0, + output_padding=0, + bn=True, + relu=True): + bias = not bn + layers = [] + layers.append( + nn.Conv2DTranspose(in_channels, + out_channels, + kernel_size, + stride, + padding, + output_padding, + bias_attr=bias)) + if bn: + layers.append(nn.BatchNorm2D(out_channels)) + + if relu: + layers.append(nn.LeakyReLU(0.2)) + layers = nn.Sequential(*layers) + + # initialize the weights + for m in layers.sublayers(include_self=True): + if isinstance(m, nn.Conv2DTranspose): + normal_(m.weight) + if m.bias is not None: + zeros_(m.bias) + elif isinstance(m, nn.BatchNorm2D): + ones_(m.weight) + zeros_(m.bias) + return layers + + +def transformation_from_parameters(axisangle, translation, invert=False): + """Convert the network's (axisangle, translation) output into a 4x4 matrix + """ + R = rot_from_axisangle(axisangle) + t = translation.clone() + + if invert: + R = R.transpose([0, 2, 1]) + t *= -1 + + T = get_translation_matrix(t) + + if invert: + M = paddle.matmul(R, T) + else: + M = paddle.matmul(T, R) + + return M + + +def get_translation_matrix(translation_vector): + """Convert a translation vector into a 4x4 transformation matrix + """ + t = translation_vector.reshape([-1, 3, 1]) + gather_object = paddle.stack([ + paddle.zeros([ + translation_vector.shape[0], + ], paddle.float32), + paddle.ones([ + translation_vector.shape[0], + ], paddle.float32), + paddle.squeeze(t[:, 0], axis=-1), + paddle.squeeze(t[:, 1], axis=-1), + paddle.squeeze(t[:, 2], axis=-1), + ]) + gather_index = paddle.to_tensor([ + [1], + [0], + [0], + [2], + [0], + [1], + [0], + [3], + [0], + [0], + [1], + [4], + [0], + [0], + [0], + [1], + ]) + T = paddle.gather_nd(gather_object, gather_index) + T = T.reshape([4, 4, -1]).transpose((2, 0, 1)) + return T + + +def rot_from_axisangle(vec): + """Convert an axisangle rotation into a 4x4 transformation matrix + (adapted from https://github.com/Wallacoloo/printipi) + Input 'vec' has to be Bx1x3 + """ + angle = paddle.norm(vec, 2, 2, True) + axis = vec / (angle + 1e-7) + + ca = paddle.cos(angle) + sa = paddle.sin(angle) + C = 1 - ca + + x = axis[..., 0].unsqueeze(1) + y = axis[..., 1].unsqueeze(1) + z = axis[..., 2].unsqueeze(1) + + xs = x * sa + ys = y * sa + zs = z * sa + xC = x * C + yC = y * C + zC = z * C + xyC = x * yC + yzC = y * zC + zxC = z * xC + + gather_object = paddle.stack([ + paddle.squeeze(x * xC + ca, axis=(-1, -2)), + paddle.squeeze(xyC - zs, axis=(-1, -2)), + paddle.squeeze(zxC + ys, axis=(-1, -2)), + paddle.squeeze(xyC + zs, axis=(-1, -2)), + paddle.squeeze(y * yC + ca, axis=(-1, -2)), + paddle.squeeze(yzC - xs, axis=(-1, -2)), + paddle.squeeze(zxC - ys, axis=(-1, -2)), + paddle.squeeze(yzC + xs, axis=(-1, -2)), + paddle.squeeze(z * zC + ca, axis=(-1, -2)), + paddle.ones([ + vec.shape[0], + ], dtype=paddle.float32), + paddle.zeros([ + vec.shape[0], + ], dtype=paddle.float32) + ]) + gather_index = paddle.to_tensor([ + [0], + [1], + [2], + [10], + [3], + [4], + [5], + [10], + [6], + [7], + [8], + [10], + [10], + [10], + [10], + [9], + ]) + rot = paddle.gather_nd(gather_object, gather_index) + rot = rot.reshape([4, 4, -1]).transpose((2, 0, 1)) + return rot + + +def upsample(x): + """Upsample input tensor by a factor of 2 + """ + return F.interpolate(x, scale_factor=2, mode="nearest") + + +def get_smooth_loss(disp, img): + """Computes the smoothness loss for a disparity image + The color image is used for edge-aware smoothness + """ + grad_disp_x = paddle.abs(disp[:, :, :, :-1] - disp[:, :, :, 1:]) + grad_disp_y = paddle.abs(disp[:, :, :-1, :] - disp[:, :, 1:, :]) + + grad_img_x = paddle.mean(paddle.abs(img[:, :, :, :-1] - img[:, :, :, 1:]), + 1, + keepdim=True) + grad_img_y = paddle.mean(paddle.abs(img[:, :, :-1, :] - img[:, :, 1:, :]), + 1, + keepdim=True) + + grad_disp_x *= paddle.exp(-grad_img_x) + grad_disp_y *= paddle.exp(-grad_img_y) + + return grad_disp_x.mean() + grad_disp_y.mean() + + +def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1): + """3x3 convolution with padding""" + return nn.Conv2D(in_planes, + out_planes, + kernel_size=3, + stride=stride, + padding=dilation, + groups=groups, + bias_attr=False, + dilation=dilation) + + +def conv1x1(in_planes, out_planes, stride=1): + """1x1 convolution""" + return nn.Conv2D(in_planes, + out_planes, + kernel_size=1, + stride=stride, + bias_attr=False) + + +def resnet_multiimage_input(num_layers, num_input_images=1): + """Constructs a ResNet model. + Args: + num_layers (int): Number of resnet layers. Must be 18 or 50 + pretrained (bool): If True, returns a model pre-trained on ImageNet + num_input_images (int): Number of frames stacked as input + """ + assert num_layers in [18, 50], "Can only run with 18 or 50 layer resnet" + blocks = {18: [2, 2, 2, 2], 50: [3, 4, 6, 3]}[num_layers] + + block_type = {18: BasicBlock, 50: Bottleneck}[num_layers] + + model = ResNetMultiImageInput(block_type, + num_layers, + blocks, + num_input_images=num_input_images) + model.init_weights() + return model + + +class ConvBlock(nn.Layer): + """Layer to perform a convolution followed by ELU + """ + def __init__(self, in_channels, out_channels): + super(ConvBlock, self).__init__() + + self.conv = Conv3x3(in_channels, out_channels) + self.nonlin = nn.ELU() + + def forward(self, x): + out = self.conv(x) + out = self.nonlin(out) + return out + + +class Conv3x3(nn.Layer): + """Layer to pad and convolve input + """ + def __init__(self, in_channels, out_channels, use_refl=True): + super(Conv3x3, self).__init__() + + if use_refl: + self.pad = nn.Pad2D(1, mode='reflect') + else: + self.pad = nn.Pad2D(1) + self.conv = nn.Conv2D(int(in_channels), int(out_channels), 3) + + def forward(self, x): + out = self.pad(x) + out = self.conv(out) + return out + + +class BackprojectDepth(nn.Layer): + """Layer to transform a depth image into a point cloud + """ + def __init__(self, batch_size, height, width): + super(BackprojectDepth, self).__init__() + + self.batch_size = batch_size + self.height = height + self.width = width + + meshgrid = np.meshgrid(range(self.width), + range(self.height), + indexing='xy') + id_coords = np.stack(meshgrid, axis=0).astype(np.float32) + self.id_coords = self.create_parameter(shape=list(id_coords.shape), + dtype=paddle.float32) + self.id_coords.set_value(id_coords) + self.add_parameter("id_coords", self.id_coords) + self.id_coords.stop_gradient = True + + self.ones = self.create_parameter( + shape=[self.batch_size, 1, self.height * self.width], + default_initializer=ones_) + self.add_parameter("ones", self.ones) + self.ones.stop_gradient = True + + pix_coords = paddle.unsqueeze( + paddle.stack([ + self.id_coords[0].reshape([ + -1, + ]), self.id_coords[1].reshape([ + -1, + ]) + ], 0), 0) + pix_coords = pix_coords.tile([batch_size, 1, 1]) + pix_coords = paddle.concat([pix_coords, self.ones], 1) + self.pix_coords = self.create_parameter(shape=list(pix_coords.shape), ) + self.pix_coords.set_value(pix_coords) + self.add_parameter("pix_coords", self.pix_coords) + self.pix_coords.stop_gradient = True + + def forward(self, depth, inv_K): + cam_points = paddle.matmul(inv_K[:, :3, :3], self.pix_coords) + cam_points = depth.reshape([self.batch_size, 1, -1]) * cam_points + cam_points = paddle.concat([cam_points, self.ones], 1) + + return cam_points + + +class Project3D(nn.Layer): + """Layer which projects 3D points into a camera with intrinsics K and at position T + """ + def __init__(self, batch_size, height, width, eps=1e-7): + super(Project3D, self).__init__() + + self.batch_size = batch_size + self.height = height + self.width = width + self.eps = eps + + def forward(self, points, K, T): + P = paddle.matmul(K, T)[:, :3, :] + + cam_points = paddle.matmul(P, points) + + pix_coords = cam_points[:, :2, :] / (cam_points[:, 2, :].unsqueeze(1) + + self.eps) + pix_coords = pix_coords.reshape( + [self.batch_size, 2, self.height, self.width]) + pix_coords = pix_coords.transpose([0, 2, 3, 1]) + pix_coords[..., 0] /= self.width - 1 + pix_coords[..., 1] /= self.height - 1 + pix_coords = (pix_coords - 0.5) * 2 + return pix_coords + + +class SSIM(nn.Layer): + """Layer to compute the SSIM loss between a pair of images + """ + def __init__(self): + super(SSIM, self).__init__() + self.mu_x_pool = nn.AvgPool2D(3, 1, exclusive=False) + self.mu_y_pool = nn.AvgPool2D(3, 1, exclusive=False) + self.sig_x_pool = nn.AvgPool2D(3, 1, exclusive=False) + self.sig_y_pool = nn.AvgPool2D(3, 1, exclusive=False) + self.sig_xy_pool = nn.AvgPool2D(3, 1, exclusive=False) + + self.refl = nn.Pad2D(1, mode='reflect') + + self.C1 = 0.01**2 + self.C2 = 0.03**2 + + def forward(self, x, y): + x = self.refl(x) + y = self.refl(y) + + mu_x = self.mu_x_pool(x) + mu_y = self.mu_y_pool(y) + + sigma_x = self.sig_x_pool(x**2) - mu_x**2 + sigma_y = self.sig_y_pool(y**2) - mu_y**2 + sigma_xy = self.sig_xy_pool(x * y) - mu_x * mu_y + + SSIM_n = (2 * mu_x * mu_y + self.C1) * (2 * sigma_xy + self.C2) + SSIM_d = (mu_x**2 + mu_y**2 + self.C1) * (sigma_x + sigma_y + self.C2) + + return paddle.clip((1 - SSIM_n / SSIM_d) / 2, 0, 1) + + +class ResNetMultiImageInput(ResNet): + """Constructs a resnet model with varying number of input images. + Adapted from https://github.com/pypaddle/vision/blob/master/paddlevision/models/resnet.py + """ + def __init__(self, block, depth, layers, num_input_images=1): + super(ResNetMultiImageInput, self).__init__(block, depth) + self.inplanes = 64 + self.conv1 = nn.Conv2D(num_input_images * 3, + 64, + kernel_size=7, + stride=2, + padding=3, + bias_attr=False) + self.bn1 = nn.BatchNorm2D(64) + self.relu = nn.ReLU() + self.maxpool = nn.MaxPool2D(kernel_size=3, stride=2, padding=1) + self.layer1 = self._make_layer(block, 64, layers[0]) + self.layer2 = self._make_layer(block, 128, layers[1], stride=2) + self.layer3 = self._make_layer(block, 256, layers[2], stride=2) + self.layer4 = self._make_layer(block, 512, layers[3], stride=2) + + def init_weights(self): + for layer in self.sublayers(include_self=True): + if isinstance(layer, nn.Conv2D): + kaiming_normal_(layer.weight, + mode='fan_out', + nonlinearity='relu') + elif isinstance(layer, nn.BatchNorm2D): + ones_(layer.weight) + zeros_(layer.bias) + + +class ConvBNLayer(nn.Layer): + """Conv2D and BatchNorm2D layer. + + Args: + in_channels (int): Number of channels for the input. + out_channels (int): Number of channels for the output. + kernel_size (int): Kernel size. + stride (int): Stride in the Conv2D layer. Default: 1. + groups (int): Groups in the Conv2D, Default: 1. + act (str): Indicate activation after BatchNorm2D layer. + name (str): the name of an instance of ConvBNLayer. + + Note: weight and bias initialization include initialize values + and name the restored parameters, values initialization + are explicit declared in the ```init_weights``` method. + + """ + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + act=None, + name=None): + super(ConvBNLayer, self).__init__() + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + bias_attr=False) + + self._act = act + + self._batch_norm = BatchNorm2D(out_channels) + + def forward(self, inputs): + y = self._conv(inputs) + y = self._batch_norm(y) + if self._act: + y = getattr(paddle.nn.functional, self._act)(y) + return y + + +class BasicBlock(nn.Layer): + expansion = 1 + + def __init__(self, + inplanes, + planes, + stride=1, + downsample=None, + groups=1, + base_width=64, + dilation=1, + norm_layer=None): + super(BasicBlock, self).__init__() + if norm_layer is None: + norm_layer = nn.BatchNorm2D + if groups != 1 or base_width != 64: + raise ValueError( + 'BasicBlock only supports groups=1 and base_width=64') + if dilation > 1: + raise NotImplementedError( + "Dilation > 1 not supported in BasicBlock") + # Both self.conv1 and self.downsample layers downsample the input when stride != 1 + self.conv1 = conv3x3(inplanes, planes, stride) + self.bn1 = norm_layer(planes) + self.relu = nn.ReLU() + self.conv2 = conv3x3(planes, planes) + self.bn2 = norm_layer(planes) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + identity = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + + if self.downsample is not None: + identity = self.downsample(x) + + out += identity + out = self.relu(out) + + return out + + +class Bottleneck(nn.Layer): + # Bottleneck in torchvision places the stride for downsampling at 3x3 convolution(self.conv2) + # while original implementation places the stride at the first 1x1 convolution(self.conv1) + # according to "Deep residual learning for image recognition"https://arxiv.org/abs/1512.03385. + # This variant is also known as ResNet V1.5 and improves accuracy according to + # https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch. + + expansion = 4 + + def __init__(self, + inplanes, + planes, + stride=1, + downsample=None, + groups=1, + base_width=64, + dilation=1, + norm_layer=None): + super(Bottleneck, self).__init__() + if norm_layer is None: + norm_layer = nn.BatchNorm2D + width = int(planes * (base_width / 64.)) * groups + + self.conv1 = conv1x1(inplanes, width) + self.bn1 = norm_layer(width) + self.conv2 = conv3x3(width, width, stride, groups, dilation) + self.bn2 = norm_layer(width) + self.conv3 = conv1x1(width, planes * self.expansion) + self.bn3 = norm_layer(planes * self.expansion) + self.relu = nn.ReLU() + self.downsample = downsample + self.stride = stride + + def forward(self, x): + identity = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + identity = self.downsample(x) + + out += identity + out = self.relu(out) + + return out + + +class DepthDecoder(nn.Layer): + def __init__(self, + num_ch_enc, + scales=range(4), + num_output_channels=1, + use_skips=True): + super(DepthDecoder, self).__init__() + + self.num_output_channels = num_output_channels + self.use_skips = use_skips + self.upsample_mode = 'nearest' + self.scales = scales + + self.num_ch_enc = num_ch_enc + self.num_ch_dec = np.array([16, 32, 64, 128, 256]) + + # decoder + self.convs = OrderedDict() + for i in range(4, -1, -1): + # upconv_0 + num_ch_in = self.num_ch_enc[-1] if i == 4 else self.num_ch_dec[i + + 1] + num_ch_out = self.num_ch_dec[i] + self.convs[("upconv", i, 0)] = ConvBlock(num_ch_in, num_ch_out) + + # upconv_1 + num_ch_in = self.num_ch_dec[i] + if self.use_skips and i > 0: + num_ch_in += self.num_ch_enc[i - 1] + num_ch_out = self.num_ch_dec[i] + self.convs[("upconv", i, 1)] = ConvBlock(num_ch_in, num_ch_out) + + for s in self.scales: + self.convs[("dispconv", s)] = Conv3x3(self.num_ch_dec[s], + self.num_output_channels) + + self.decoder = nn.LayerList(list(self.convs.values())) + self.sigmoid = nn.Sigmoid() + + def forward(self, input_features): + outputs = {} + + # decoder + x = input_features[-1] + for i in range(4, -1, -1): + x = self.convs[("upconv", i, 0)](x) + x = [upsample(x)] + if self.use_skips and i > 0: + x += [input_features[i - 1]] + x = paddle.concat(x, 1) + x = self.convs[("upconv", i, 1)](x) + if i in self.scales: + outputs[("disp", i)] = self.sigmoid(self.convs[("dispconv", + i)](x)) + return outputs + + +class PoseDecoder(nn.Layer): + def __init__(self, + num_ch_enc, + num_input_features, + num_frames_to_predict_for=None, + stride=1): + super(PoseDecoder, self).__init__() + + self.num_ch_enc = num_ch_enc + self.num_input_features = num_input_features + + if num_frames_to_predict_for is None: + num_frames_to_predict_for = num_input_features - 1 + self.num_frames_to_predict_for = num_frames_to_predict_for + + self.convs = OrderedDict() + self.convs[("squeeze")] = nn.Conv2D(self.num_ch_enc[-1], 256, 1) + self.convs[("pose", 0)] = nn.Conv2D(num_input_features * 256, 256, 3, + stride, 1) + self.convs[("pose", 1)] = nn.Conv2D(256, 256, 3, stride, 1) + self.convs[("pose", 2)] = nn.Conv2D(256, 6 * num_frames_to_predict_for, + 1) + + self.relu = nn.ReLU() + + self.net = nn.LayerList(list(self.convs.values())) + + def forward(self, input_features): + last_features = [f[-1] for f in input_features] + + cat_features = [ + self.relu(self.convs["squeeze"](f)) for f in last_features + ] + cat_features = paddle.concat(cat_features, 1) + + out = cat_features + for i in range(3): + out = self.convs[("pose", i)](out) + if i != 2: + out = self.relu(out) + + out = out.mean(3).mean(2) + + out = 0.01 * out.reshape([-1, self.num_frames_to_predict_for, 1, 6]) + + axisangle = out[..., :3] + translation = out[..., 3:] + + return axisangle, translation + + +class ResnetEncoder(nn.Layer): + """Pypaddle module for a resnet encoder + """ + def __init__(self, num_layers, pretrained=False, num_input_images=1): + super(ResnetEncoder, self).__init__() + + self.num_ch_enc = np.array([64, 64, 128, 256, 512]) + + resnets = { + 18: paddle.vision.models.resnet18, + 34: paddle.vision.models.resnet34, + 50: paddle.vision.models.resnet50, + 101: paddle.vision.models.resnet101, + 152: paddle.vision.models.resnet152 + } + + if num_layers not in resnets: + raise ValueError( + "{} is not a valid number of resnet layers".format(num_layers)) + + if num_input_images > 1: + self.encoder = resnet_multiimage_input(num_layers, pretrained, + num_input_images) + else: + self.encoder = resnets[num_layers](pretrained) + + if num_layers > 34: + self.num_ch_enc[1:] *= 4 + + ###################################### + # night public first conv + ###################################### + self.conv1 = nn.Conv2D(3, + 64, + kernel_size=7, + stride=2, + padding=3, + bias_attr=False) + self.bn1 = nn.BatchNorm2D(64) + self.relu = nn.ReLU() # NOTE + + self.conv_shared = nn.Conv2D(512, 64, kernel_size=1) + + ########################################## + # private source encoder, day + ########################################## + self.encoder_day = resnets[num_layers](pretrained) + self.conv_diff_day = nn.Conv2D( + 512, 64, kernel_size=1) # no bn after conv, so bias=true + + ########################################## + # private target encoder, night + ########################################## + self.encoder_night = resnets[num_layers](pretrained) + self.conv_diff_night = nn.Conv2D(512, 64, kernel_size=1) + + ###################################### + # shared decoder (small decoder), use a simple de-conv to upsample the features with no skip connection + ###################################### + self.convt5 = convt_bn_relu(in_channels=512, + out_channels=256, + kernel_size=3, + stride=2, + padding=1, + output_padding=1) + self.convt4 = convt_bn_relu(in_channels=256, + out_channels=128, + kernel_size=3, + stride=2, + padding=1, + output_padding=1) + self.convt3 = convt_bn_relu(in_channels=128, + out_channels=64, + kernel_size=3, + stride=2, + padding=1, + output_padding=1) + self.convt2 = convt_bn_relu(in_channels=64, + out_channels=64, + kernel_size=3, + stride=2, + padding=1, + output_padding=1) + self.convt1 = convt_bn_relu(in_channels=64, + out_channels=64, + kernel_size=3, + stride=2, + padding=1, + output_padding=1) + self.convtf = nn.Conv2D(64, 3, kernel_size=1, stride=1, padding=0) + + def forward(self, input_image, is_night): + if self.training: + result = [] + input_data = (input_image - 0.45) / 0.225 + if is_night == 'day': + # source private encoder, day + private_feature = self.encoder_day.conv1(input_data) + private_feature = self.encoder_day.bn1(private_feature) + private_feature = self.encoder_day.relu(private_feature) + private_feature = self.encoder_day.maxpool(private_feature) + private_feature = self.encoder_day.layer1(private_feature) + private_feature = self.encoder_day.layer2(private_feature) + private_feature = self.encoder_day.layer3(private_feature) + private_feature = self.encoder_day.layer4(private_feature) + private_code = self.conv_diff_day(private_feature) + private_gram = gram_matrix(private_feature) + result.append(private_code) + result.append(private_gram) + + elif is_night == 'night': + # target private encoder, night + private_feature = self.encoder_night.conv1(input_data) + private_feature = self.encoder_night.bn1(private_feature) + private_feature = self.encoder_night.relu(private_feature) + private_feature = self.encoder_night.maxpool(private_feature) + private_feature = self.encoder_night.layer1(private_feature) + private_feature = self.encoder_night.layer2(private_feature) + private_feature = self.encoder_night.layer3(private_feature) + private_feature = self.encoder_night.layer4(private_feature) + private_code = self.conv_diff_night(private_feature) + + private_gram = gram_matrix(private_feature) + result.append(private_code) + result.append(private_gram) + + # shared encoder + self.features = [] + x = (input_image - 0.45) / 0.225 + if is_night == 'day': + x = self.encoder.conv1(x) + x = self.encoder.bn1(x) + self.features.append(self.encoder.relu(x)) + else: + x = self.conv1(x) + x = self.bn1(x) + self.features.append(self.relu(x)) + + self.features.append( + self.encoder.layer1(self.encoder.maxpool(self.features[-1]))) + self.features.append(self.encoder.layer2(self.features[-1])) + self.features.append(self.encoder.layer3(self.features[-1])) + self.features.append(self.encoder.layer4(self.features[-1])) + + if self.training: + shared_code = self.conv_shared(self.features[-1]) + shared_gram = gram_matrix(self.features[-1]) + result.append(shared_code) # use this to calculate loss of diff + result.append(shared_gram) + result.append( + self.features[-1]) # use this to calculate loss of similarity + + union_code = private_feature + self.features[-1] + rec_code = self.convt5(union_code) + rec_code = self.convt4(rec_code) + rec_code = self.convt3(rec_code) + rec_code = self.convt2(rec_code) + rec_code = self.convt1(rec_code) + rec_code = self.convtf(rec_code) + result.append(rec_code) + + return self.features, result + else: + return self.features + + +class ResnetEncoder_pose(nn.Layer): + """Pypaddle module for a resnet encoder + """ + def __init__(self, num_layers, pretrained=False, num_input_images=1): + super(ResnetEncoder_pose, self).__init__() + + self.num_ch_enc = np.array([64, 64, 128, 256, 512]) + resnets = { + 18: paddle.vision.models.resnet18, + 34: paddle.vision.models.resnet34, + 50: paddle.vision.models.resnet50, + 101: paddle.vision.models.resnet101, + 152: paddle.vision.models.resnet152 + } + + if num_layers not in resnets: + raise ValueError( + "{} is not a valid number of resnet layers".format(num_layers)) + + if num_input_images > 1: + self.encoder = resnet_multiimage_input(num_layers, num_input_images) + else: + self.encoder = resnets[num_layers](pretrained) + + if num_layers > 34: + self.num_ch_enc[1:] *= 4 + + def forward(self, input_image): + features = [] + x = (input_image - 0.45) / 0.225 + x = self.encoder.conv1(x) + x = self.encoder.bn1(x) + features.append(self.encoder.relu(x)) + features.append(self.encoder.layer1(self.encoder.maxpool(features[-1]))) + features.append(self.encoder.layer2(features[-1])) + features.append(self.encoder.layer3(features[-1])) + features.append(self.encoder.layer4(features[-1])) + + return features + + +@BACKBONES.register() +class ADDS_DepthNet(nn.Layer): + def __init__(self, + num_layers=18, + frame_ids=[0, -1, 1], + height=256, + width=512, + batch_size=6, + pose_model_input="pairs", + use_stereo=False, + only_depth_encoder=False, + pretrained=None, + scales=[0, 1, 2, 3], + min_depth=0.1, + max_depth=100.0, + pose_model_type='separate_resnet', + v1_multiscale=False, + predictive_mask=False, + disable_automasking=False): + super(ADDS_DepthNet, self).__init__() + self.num_layers = num_layers + self.height = height + self.width = width + self.batch_size = batch_size + self.frame_ids = frame_ids + self.pose_model_input = pose_model_input + self.use_stereo = use_stereo + self.only_depth_encoder = only_depth_encoder + self.pretrained = pretrained + self.scales = scales + self.pose_model_type = pose_model_type + self.predictive_mask = predictive_mask + self.disable_automasking = disable_automasking + self.v1_multiscale = v1_multiscale + self.min_depth = min_depth + self.max_depth = max_depth + + self.num_input_frames = len(self.frame_ids) + self.num_pose_frames = 2 if self.pose_model_input == "pairs" else self.num_input_frames + + assert self.frame_ids[0] == 0, "frame_ids must start with 0" + + self.use_pose_net = not (self.use_stereo and self.frame_ids == [0]) + + self.encoder = ResnetEncoder(self.num_layers) + if not self.only_depth_encoder: + self.depth = DepthDecoder(self.encoder.num_ch_enc, self.scales) + if self.use_pose_net and not self.only_depth_encoder: + if self.pose_model_type == "separate_resnet": + self.pose_encoder = ResnetEncoder_pose( + self.num_layers, num_input_images=self.num_pose_frames) + self.pose = PoseDecoder(self.pose_encoder.num_ch_enc, + num_input_features=1, + num_frames_to_predict_for=2) + + self.backproject_depth = {} + self.project_3d = {} + for scale in self.scales: + h = self.height // (2**scale) + w = self.width // (2**scale) + + self.backproject_depth[scale] = BackprojectDepth( + self.batch_size, h, w) + self.project_3d[scale] = Project3D(batch_size, h, w) + + def init_weights(self): + """First init model's weight""" + for m in self.sublayers(include_self=True): + if isinstance(m, nn.Conv2D): + kaiming_normal_(m.weight, a=math.sqrt(5)) + if m.bias is not None: + fan_in, _ = _calculate_fan_in_and_fan_out(m.weight) + bound = 1 / math.sqrt(fan_in) + uniform_ = paddle.nn.initializer.Uniform(-bound, bound) + uniform_(m.bias) + """Second, if provide pretrained ckpt, load it""" + if self.pretrained: # load pretrained weights + load_ckpt(self, self.pretrained) + + def forward(self, inputs, day_or_night='day'): + if self.training: + features, result = self.encoder(inputs["color_aug", 0, 0], 'day') + features_night, result_night = self.encoder( + inputs[("color_n_aug", 0, 0)], 'night') + + outputs = self.depth(features) + outputs_night = self.depth(features_night) + if self.use_pose_net and not self.only_depth_encoder: + outputs.update(self.predict_poses(inputs, 'day')) + outputs_night.update(self.predict_poses(inputs, 'night')) + + self.generate_images_pred(inputs, outputs, 'day') + self.generate_images_pred(inputs, outputs_night, 'night') + + outputs['frame_ids'] = self.frame_ids + outputs['scales'] = self.scales + outputs['result'] = result + outputs['result_night'] = result_night + outputs_night['frame_ids'] = self.frame_ids + outputs_night['scales'] = self.scales + outputs['outputs_night'] = outputs_night + else: + if isinstance(inputs, dict): + input_color = inputs[("color", 0, 0)] + features = self.encoder(input_color, day_or_night[0]) + outputs = self.depth(features) + + pred_disp, _ = disp_to_depth(outputs[("disp", 0)], + self.min_depth, self.max_depth) + + pred_disp = pred_disp[:, 0].numpy() + + outputs['pred_disp'] = np.squeeze(pred_disp) + + outputs['gt'] = np.squeeze(inputs['depth_gt'].numpy()) + else: + input_color = inputs + features = self.encoder(input_color, day_or_night) + outputs = self.depth(features) + + pred_disp, _ = disp_to_depth(outputs[("disp", 0)], + self.min_depth, self.max_depth) + + pred_disp = pred_disp[:, 0] + outputs = paddle.squeeze(pred_disp) + return outputs + + def predict_poses(self, inputs, is_night): + """Predict poses between input frames for monocular sequences. + """ + outputs = {} + if self.num_pose_frames == 2: + if is_night: + pose_feats = { + f_i: inputs["color_n_aug", f_i, 0] + for f_i in self.frame_ids + } + else: + pose_feats = { + f_i: inputs["color_aug", f_i, 0] + for f_i in self.frame_ids + } + + for f_i in self.frame_ids[1:]: + if f_i != "s": + if f_i < 0: + pose_inputs = [pose_feats[f_i], pose_feats[0]] + else: + pose_inputs = [pose_feats[0], pose_feats[f_i]] + + if self.pose_model_type == "separate_resnet": + pose_inputs = [ + self.pose_encoder(paddle.concat(pose_inputs, + axis=1)) + ] + + axisangle, translation = self.pose(pose_inputs) + outputs[("axisangle", 0, f_i)] = axisangle + outputs[("translation", 0, f_i)] = translation + + # Invert the matrix if the frame id is negative + outputs[("cam_T_cam", 0, + f_i)] = transformation_from_parameters( + axisangle[:, 0], + translation[:, 0], + invert=(f_i < 0)) + return outputs + + def generate_images_pred(self, inputs, outputs, is_night): + """Generate the warped (reprojected) color images for a minibatch. + Generated images are saved into the `outputs` dictionary. + """ + _, _, height, width = inputs['color', 0, 0].shape + for scale in self.scales: + disp = outputs[("disp", scale)] + if self.v1_multiscale: + source_scale = scale + else: + disp = F.interpolate(disp, [height, width], + mode="bilinear", + align_corners=False) + source_scale = 0 + + _, depth = disp_to_depth(disp, self.min_depth, self.max_depth) + + outputs[("depth", 0, scale)] = depth + for i, frame_id in enumerate(self.frame_ids[1:]): + + T = outputs[("cam_T_cam", 0, frame_id)] + + cam_points = self.backproject_depth[source_scale]( + depth, inputs[("inv_K", source_scale)]) + pix_coords = self.project_3d[source_scale]( + cam_points, inputs[("K", source_scale)], T) + + outputs[("sample", frame_id, scale)] = pix_coords + + if is_night: + inputs[("color_n", frame_id, + source_scale)].stop_gradient = False + outputs[("color", frame_id, + scale)] = paddle.nn.functional.grid_sample( + inputs[("color_n", frame_id, source_scale)], + outputs[("sample", frame_id, scale)], + padding_mode="border", + align_corners=False) + + else: + inputs[("color", frame_id, + source_scale)].stop_gradient = False + outputs[("color", frame_id, + scale)] = paddle.nn.functional.grid_sample( + inputs[("color", frame_id, source_scale)], + outputs[("sample", frame_id, scale)], + padding_mode="border", + align_corners=False) + + if not self.disable_automasking: + if is_night: + outputs[("color_identity", frame_id, scale)] = \ + inputs[("color_n", frame_id, source_scale)] + else: + outputs[("color_identity", frame_id, scale)] = \ + inputs[("color", frame_id, source_scale)] diff --git a/paddlevideo/modeling/backbones/agcn.py b/paddlevideo/modeling/backbones/agcn.py new file mode 100644 index 0000000000000000000000000000000000000000..9f870c66b9bf0e14fbcf76c91ce14bd2e93e2685 --- /dev/null +++ b/paddlevideo/modeling/backbones/agcn.py @@ -0,0 +1,128 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from ..registry import BACKBONES + + +class GCN(nn.Layer): + def __init__(self, in_channels, out_channels, vertex_nums=25, stride=1): + super(GCN, self).__init__() + self.conv1 = nn.Conv2D(in_channels=in_channels, + out_channels=3 * out_channels, + kernel_size=1, + stride=1) + self.conv2 = nn.Conv2D(in_channels=vertex_nums * 3, + out_channels=vertex_nums, + kernel_size=1) + + def forward(self, x): + # x --- N,C,T,V + x = self.conv1(x) # N,3C,T,V + N, C, T, V = x.shape + x = paddle.reshape(x, [N, C // 3, 3, T, V]) # N,C,3,T,V + x = paddle.transpose(x, perm=[0, 1, 2, 4, 3]) # N,C,3,V,T + x = paddle.reshape(x, [N, C // 3, 3 * V, T]) # N,C,3V,T + x = paddle.transpose(x, perm=[0, 2, 1, 3]) # N,3V,C,T + x = self.conv2(x) # N,V,C,T + x = paddle.transpose(x, perm=[0, 2, 3, 1]) # N,C,T,V + return x + + +class Block(paddle.nn.Layer): + def __init__(self, + in_channels, + out_channels, + vertex_nums=25, + temporal_size=9, + stride=1, + residual=True): + super(Block, self).__init__() + self.residual = residual + self.out_channels = out_channels + + self.bn_res = nn.BatchNorm2D(out_channels) + self.conv_res = nn.Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=(stride, 1)) + self.gcn = GCN(in_channels=in_channels, + out_channels=out_channels, + vertex_nums=vertex_nums) + self.tcn = nn.Sequential( + nn.BatchNorm2D(out_channels), + nn.ReLU(), + nn.Conv2D(in_channels=out_channels, + out_channels=out_channels, + kernel_size=(temporal_size, 1), + padding=((temporal_size - 1) // 2, 0), + stride=(stride, 1)), + nn.BatchNorm2D(out_channels), + ) + + def forward(self, x): + if self.residual: + y = self.conv_res(x) + y = self.bn_res(y) + x = self.gcn(x) + x = self.tcn(x) + out = x + y if self.residual else x + out = F.relu(out) + return out + + +@BACKBONES.register() +class AGCN(nn.Layer): + """ + AGCN model improves the performance of ST-GCN using + Adaptive Graph Convolutional Networks. + Args: + in_channels: int, channels of vertex coordinate. 2 for (x,y), 3 for (x,y,z). Default 2. + """ + def __init__(self, in_channels=2, **kwargs): + super(AGCN, self).__init__() + + self.data_bn = nn.BatchNorm1D(25 * 2) + self.agcn = nn.Sequential( + Block(in_channels=in_channels, + out_channels=64, + residual=False, + **kwargs), Block(in_channels=64, out_channels=64, **kwargs), + Block(in_channels=64, out_channels=64, **kwargs), + Block(in_channels=64, out_channels=64, **kwargs), + Block(in_channels=64, out_channels=128, stride=2, **kwargs), + Block(in_channels=128, out_channels=128, **kwargs), + Block(in_channels=128, out_channels=128, **kwargs), + Block(in_channels=128, out_channels=256, stride=2, **kwargs), + Block(in_channels=256, out_channels=256, **kwargs), + Block(in_channels=256, out_channels=256, **kwargs)) + + self.pool = nn.AdaptiveAvgPool2D(output_size=(1, 1)) + + def forward(self, x): + # data normalization + N, C, T, V, M = x.shape + + x = x.transpose((0, 4, 1, 2, 3)) # N, M, C, T, V + x = x.reshape((N * M, C, T, V)) + + x = self.agcn(x) + + x = self.pool(x) # NM,C,T,V --> NM,C,1,1 + C = x.shape[1] + x = paddle.reshape(x, (N, M, C, 1, 1)).mean(axis=1) # N,C,1,1 + + return x diff --git a/paddlevideo/modeling/backbones/asrf.py b/paddlevideo/modeling/backbones/asrf.py new file mode 100644 index 0000000000000000000000000000000000000000..37437b3edc47b1fc745176fa3254865acfe5efb2 --- /dev/null +++ b/paddlevideo/modeling/backbones/asrf.py @@ -0,0 +1,75 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://github.com/yabufarha/ms-tcn/blob/master/model.py +# https://github.com/yiskw713/asrf/libs/models/tcn.py + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np +import copy +import random +import math + +from paddle import ParamAttr +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from .ms_tcn import DilatedResidualLayer +from ..framework.segmenters.utils import init_bias, KaimingUniform_like_torch + + +@BACKBONES.register() +class ASRF(nn.Layer): + + def __init__(self, in_channel, num_features, num_classes, num_stages, + num_layers): + super().__init__() + self.in_channel = in_channel + self.num_features = num_features + self.num_classes = num_classes + self.num_stages = num_stages + self.num_layers = num_layers + + # define layers + self.conv_in = nn.Conv1D(self.in_channel, self.num_features, 1) + + shared_layers = [ + DilatedResidualLayer(2**i, self.num_features, self.num_features) + for i in range(self.num_layers) + ] + self.shared_layers = nn.LayerList(shared_layers) + + self.init_weights() + + def init_weights(self): + """ + initialize model layers' weight + """ + # init weight + for layer in self.sublayers(): + if isinstance(layer, nn.Conv1D): + layer.weight.set_value( + KaimingUniform_like_torch(layer.weight).astype('float32')) + if layer.bias is not None: + layer.bias.set_value( + init_bias(layer.weight, layer.bias).astype('float32')) + + def forward(self, x): + """ ASRF forward + """ + out = self.conv_in(x) + for layer in self.shared_layers: + out = layer(out) + return out diff --git a/paddlevideo/modeling/backbones/bmn.py b/paddlevideo/modeling/backbones/bmn.py new file mode 100644 index 0000000000000000000000000000000000000000..200d1920a4afad43c25cb384ee0c6870022925f3 --- /dev/null +++ b/paddlevideo/modeling/backbones/bmn.py @@ -0,0 +1,290 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import numpy as np +import paddle +from paddle import ParamAttr +from ..registry import BACKBONES + + +def _get_interp1d_bin_mask(seg_xmin, seg_xmax, tscale, num_sample, + num_sample_perbin): + """ generate sample mask for a boundary-matching pair """ + plen = float(seg_xmax - seg_xmin) + plen_sample = plen / (num_sample * num_sample_perbin - 1.0) + total_samples = [ + seg_xmin + plen_sample * ii + for ii in range(num_sample * num_sample_perbin) + ] + p_mask = [] + for idx in range(num_sample): + bin_samples = total_samples[idx * num_sample_perbin:(idx + 1) * + num_sample_perbin] + bin_vector = np.zeros([tscale]) + for sample in bin_samples: + sample_upper = math.ceil(sample) + sample_decimal, sample_down = math.modf(sample) + if (tscale - 1) >= int(sample_down) >= 0: + bin_vector[int(sample_down)] += 1 - sample_decimal + if (tscale - 1) >= int(sample_upper) >= 0: + bin_vector[int(sample_upper)] += sample_decimal + bin_vector = 1.0 / num_sample_perbin * bin_vector + p_mask.append(bin_vector) + p_mask = np.stack(p_mask, axis=1) + return p_mask + + +def get_interp1d_mask(tscale, dscale, prop_boundary_ratio, num_sample, + num_sample_perbin): + """ generate sample mask for each point in Boundary-Matching Map """ + mask_mat = [] + for start_index in range(tscale): + mask_mat_vector = [] + for duration_index in range(dscale): + if start_index + duration_index < tscale: + p_xmin = start_index + p_xmax = start_index + duration_index + center_len = float(p_xmax - p_xmin) + 1 + sample_xmin = p_xmin - center_len * prop_boundary_ratio + sample_xmax = p_xmax + center_len * prop_boundary_ratio + p_mask = _get_interp1d_bin_mask(sample_xmin, sample_xmax, + tscale, num_sample, + num_sample_perbin) + else: + p_mask = np.zeros([tscale, num_sample]) + mask_mat_vector.append(p_mask) + mask_mat_vector = np.stack(mask_mat_vector, axis=2) + mask_mat.append(mask_mat_vector) + mask_mat = np.stack(mask_mat, axis=3) + mask_mat = mask_mat.astype(np.float32) + + sample_mask = np.reshape(mask_mat, [tscale, -1]) + return sample_mask + + +def init_params(name, in_channels, kernel_size): + fan_in = in_channels * kernel_size * 1 + k = 1. / math.sqrt(fan_in) + param_attr = ParamAttr(name=name, + initializer=paddle.nn.initializer.Uniform(low=-k, + high=k)) + return param_attr + + +@BACKBONES.register() +class BMN(paddle.nn.Layer): + """BMN model from + `"BMN: Boundary-Matching Network for Temporal Action Proposal Generation" `_ + Args: + tscale (int): sequence length, default 100. + dscale (int): max duration length, default 100. + prop_boundary_ratio (float): ratio of expanded temporal region in proposal boundary, default 0.5. + num_sample (int): number of samples betweent starting boundary and ending boundary of each propoasl, default 32. + num_sample_perbin (int): number of selected points in each sample, default 3. + """ + + def __init__( + self, + tscale, + dscale, + prop_boundary_ratio, + num_sample, + num_sample_perbin, + feat_dim=400, + ): + super(BMN, self).__init__() + + #init config + self.feat_dim = feat_dim + self.tscale = tscale + self.dscale = dscale + self.prop_boundary_ratio = prop_boundary_ratio + self.num_sample = num_sample + self.num_sample_perbin = num_sample_perbin + + self.hidden_dim_1d = 256 + self.hidden_dim_2d = 128 + self.hidden_dim_3d = 512 + + # Base Module + self.b_conv1 = paddle.nn.Conv1D( + in_channels=self.feat_dim, + out_channels=self.hidden_dim_1d, + kernel_size=3, + padding=1, + groups=4, + weight_attr=init_params('Base_1_w', self.feat_dim, 3), + bias_attr=init_params('Base_1_b', self.feat_dim, 3)) + self.b_conv1_act = paddle.nn.ReLU() + + self.b_conv2 = paddle.nn.Conv1D( + in_channels=self.hidden_dim_1d, + out_channels=self.hidden_dim_1d, + kernel_size=3, + padding=1, + groups=4, + weight_attr=init_params('Base_2_w', self.hidden_dim_1d, 3), + bias_attr=init_params('Base_2_b', self.hidden_dim_1d, 3)) + self.b_conv2_act = paddle.nn.ReLU() + + # Temporal Evaluation Module + self.ts_conv1 = paddle.nn.Conv1D( + in_channels=self.hidden_dim_1d, + out_channels=self.hidden_dim_1d, + kernel_size=3, + padding=1, + groups=4, + weight_attr=init_params('TEM_s1_w', self.hidden_dim_1d, 3), + bias_attr=init_params('TEM_s1_b', self.hidden_dim_1d, 3)) + self.ts_conv1_act = paddle.nn.ReLU() + + self.ts_conv2 = paddle.nn.Conv1D( + in_channels=self.hidden_dim_1d, + out_channels=1, + kernel_size=1, + padding=0, + groups=1, + weight_attr=init_params('TEM_s2_w', self.hidden_dim_1d, 1), + bias_attr=init_params('TEM_s2_b', self.hidden_dim_1d, 1)) + self.ts_conv2_act = paddle.nn.Sigmoid() + + self.te_conv1 = paddle.nn.Conv1D( + in_channels=self.hidden_dim_1d, + out_channels=self.hidden_dim_1d, + kernel_size=3, + padding=1, + groups=4, + weight_attr=init_params('TEM_e1_w', self.hidden_dim_1d, 3), + bias_attr=init_params('TEM_e1_b', self.hidden_dim_1d, 3)) + self.te_conv1_act = paddle.nn.ReLU() + self.te_conv2 = paddle.nn.Conv1D( + in_channels=self.hidden_dim_1d, + out_channels=1, + kernel_size=1, + padding=0, + groups=1, + weight_attr=init_params('TEM_e2_w', self.hidden_dim_1d, 1), + bias_attr=init_params('TEM_e2_b', self.hidden_dim_1d, 1)) + self.te_conv2_act = paddle.nn.Sigmoid() + + #Proposal Evaluation Module + self.p_conv1 = paddle.nn.Conv1D( + in_channels=self.hidden_dim_1d, + out_channels=self.hidden_dim_2d, + kernel_size=3, + padding=1, + groups=1, + weight_attr=init_params('PEM_1d_w', self.hidden_dim_1d, 3), + bias_attr=init_params('PEM_1d_b', self.hidden_dim_1d, 3)) + self.p_conv1_act = paddle.nn.ReLU() + + # init to speed up + sample_mask = get_interp1d_mask(self.tscale, self.dscale, + self.prop_boundary_ratio, + self.num_sample, self.num_sample_perbin) + self.sample_mask = paddle.to_tensor(sample_mask) + self.sample_mask.stop_gradient = True + + self.p_conv3d1 = paddle.nn.Conv3D( + in_channels=128, + out_channels=self.hidden_dim_3d, + kernel_size=(self.num_sample, 1, 1), + stride=(self.num_sample, 1, 1), + padding=0, + weight_attr=ParamAttr(name="PEM_3d1_w"), + bias_attr=ParamAttr(name="PEM_3d1_b")) + self.p_conv3d1_act = paddle.nn.ReLU() + + self.p_conv2d1 = paddle.nn.Conv2D( + in_channels=512, + out_channels=self.hidden_dim_2d, + kernel_size=1, + stride=1, + padding=0, + weight_attr=ParamAttr(name="PEM_2d1_w"), + bias_attr=ParamAttr(name="PEM_2d1_b")) + self.p_conv2d1_act = paddle.nn.ReLU() + + self.p_conv2d2 = paddle.nn.Conv2D( + in_channels=128, + out_channels=self.hidden_dim_2d, + kernel_size=3, + stride=1, + padding=1, + weight_attr=ParamAttr(name="PEM_2d2_w"), + bias_attr=ParamAttr(name="PEM_2d2_b")) + self.p_conv2d2_act = paddle.nn.ReLU() + + self.p_conv2d3 = paddle.nn.Conv2D( + in_channels=128, + out_channels=self.hidden_dim_2d, + kernel_size=3, + stride=1, + padding=1, + weight_attr=ParamAttr(name="PEM_2d3_w"), + bias_attr=ParamAttr(name="PEM_2d3_b")) + self.p_conv2d3_act = paddle.nn.ReLU() + + self.p_conv2d4 = paddle.nn.Conv2D( + in_channels=128, + out_channels=2, + kernel_size=1, + stride=1, + padding=0, + weight_attr=ParamAttr(name="PEM_2d4_w"), + bias_attr=ParamAttr(name="PEM_2d4_b")) + self.p_conv2d4_act = paddle.nn.Sigmoid() + + def init_weights(self): + pass + + def forward(self, x): + #Base Module + x = self.b_conv1(x) + x = self.b_conv1_act(x) + x = self.b_conv2(x) + x = self.b_conv2_act(x) + + #TEM + xs = self.ts_conv1(x) + xs = self.ts_conv1_act(xs) + xs = self.ts_conv2(xs) + xs = self.ts_conv2_act(xs) + xs = paddle.squeeze(xs, axis=[1]) + xe = self.te_conv1(x) + xe = self.te_conv1_act(xe) + xe = self.te_conv2(xe) + xe = self.te_conv2_act(xe) + xe = paddle.squeeze(xe, axis=[1]) + + #PEM + xp = self.p_conv1(x) + xp = self.p_conv1_act(xp) + #BM layer + xp = paddle.matmul(xp, self.sample_mask) + xp = paddle.reshape(xp, shape=[0, 0, -1, self.dscale, self.tscale]) + + xp = self.p_conv3d1(xp) + xp = self.p_conv3d1_act(xp) + xp = paddle.squeeze(xp, axis=[2]) + xp = self.p_conv2d1(xp) + xp = self.p_conv2d1_act(xp) + xp = self.p_conv2d2(xp) + xp = self.p_conv2d2_act(xp) + xp = self.p_conv2d3(xp) + xp = self.p_conv2d3_act(xp) + xp = self.p_conv2d4(xp) + xp = self.p_conv2d4_act(xp) + return xp, xs, xe diff --git a/paddlevideo/modeling/backbones/cfbi.py b/paddlevideo/modeling/backbones/cfbi.py new file mode 100644 index 0000000000000000000000000000000000000000..5fbf044b7fdf796ea5c91cbbf538dbb8d160a942 --- /dev/null +++ b/paddlevideo/modeling/backbones/cfbi.py @@ -0,0 +1,88 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from ..registry import BACKBONES +from .deeplab import DeepLab + + +class FPN(nn.Layer): + """FPN Layer""" + def __init__(self, in_dim_4x, in_dim_8x, in_dim_16x, out_dim): + super(FPN, self).__init__() + self.toplayer = self._make_layer(in_dim_16x, out_dim) + self.latlayer1 = self._make_layer(in_dim_8x, out_dim) + self.latlayer2 = self._make_layer(in_dim_4x, out_dim) + + self.smooth1 = self._make_layer(out_dim, + out_dim, + kernel_size=3, + padding=1) + self.smooth2 = self._make_layer(out_dim, + out_dim, + kernel_size=3, + padding=1) + + def _make_layer(self, in_dim, out_dim, kernel_size=1, padding=0): + return nn.Sequential( + nn.Conv2D(in_dim, + out_dim, + kernel_size=kernel_size, + stride=1, + padding=padding, + bias_attr=False), + nn.GroupNorm(num_groups=32, num_channels=out_dim)) + + def forward(self, x_4x, x_8x, x_16x): + """ forward function""" + x_16x = self.toplayer(x_16x) + x_8x = self.latlayer1(x_8x) + x_4x = self.latlayer2(x_4x) + + x_8x = x_8x + F.interpolate( + x_16x, size=x_8x.shape[-2:], mode='bilinear', align_corners=True) + x_4x = x_4x + F.interpolate( + x_8x, size=x_4x.shape[-2:], mode='bilinear', align_corners=True) + + x_8x = self.smooth1(x_8x) + x_4x = self.smooth2(x_4x) + + return F.relu(x_4x), F.relu(x_8x), F.relu(x_16x) + + +@BACKBONES.register() +class CFBI(nn.Layer): + """CFBI plus backbone""" + def __init__(self, + backbone='resnet', + freeze_bn=True, + model_aspp_outdim=256, + in_dim_8x=512, + model_semantic_embedding_dim=256): #,epsilon=1e-05): + super(CFBI, self).__init__() + #self.epsilon = epsilon + self.feature_extracter = DeepLab(backbone=backbone, freeze_bn=freeze_bn) + self.fpn = FPN(in_dim_4x=model_aspp_outdim, + in_dim_8x=in_dim_8x, + in_dim_16x=model_aspp_outdim, + out_dim=model_semantic_embedding_dim) + + def forward(self, x): + """forward function""" + x, aspp_x, low_level, mid_level = self.feature_extracter(x, True) + x_4x, x_8x, x_16x = self.fpn(x, mid_level, aspp_x) + return x_4x, x_8x, x_16x, low_level diff --git a/paddlevideo/modeling/backbones/ctrgcn.py b/paddlevideo/modeling/backbones/ctrgcn.py new file mode 100644 index 0000000000000000000000000000000000000000..9d645f4e98fb231eb09b2a3248884c4068acfd7f --- /dev/null +++ b/paddlevideo/modeling/backbones/ctrgcn.py @@ -0,0 +1,514 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np +from ..registry import BACKBONES +from ..weight_init import weight_init_ + + +def conv_init(conv): + if conv.weight is not None: + weight_init_(conv.weight, 'kaiming_normal_', mode='fan_in') + if conv.bias is not None: + nn.initializer.Constant(value=0.0)(conv.bias) + + +def bn_init(bn, scale): + nn.initializer.Constant(value=float(scale))(bn.weight) + nn.initializer.Constant(value=0.0)(bn.bias) + + +def einsum(x1, x3): + """paddle.einsum only support in dynamic graph mode. + x1 : n c u v + x2 : n c t v + """ + n, c, u, v1 = x1.shape + n, c, t, v3 = x3.shape + assert (v1 == v3), "Args of einsum not match!" + x1 = paddle.transpose(x1, perm=[0, 1, 3, 2]) # n c v u + y = paddle.matmul(x3, x1) + # out: n c t u + return y + + +class CTRGC(nn.Layer): + + def __init__(self, + in_channels, + out_channels, + rel_reduction=8, + mid_reduction=1): + super(CTRGC, self).__init__() + self.in_channels = in_channels + self.out_channels = out_channels + if in_channels == 3 or in_channels == 9: + self.rel_channels = 8 + self.mid_channels = 16 + else: + self.rel_channels = in_channels // rel_reduction + self.mid_channels = in_channels // mid_reduction + self.conv1 = nn.Conv2D(self.in_channels, + self.rel_channels, + kernel_size=1) + self.conv2 = nn.Conv2D(self.in_channels, + self.rel_channels, + kernel_size=1) + self.conv3 = nn.Conv2D(self.in_channels, + self.out_channels, + kernel_size=1) + self.conv4 = nn.Conv2D(self.rel_channels, + self.out_channels, + kernel_size=1) + self.tanh = nn.Tanh() + + def init_weights(self): + """Initiate the parameters. + """ + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + conv_init(m) + elif isinstance(m, nn.BatchNorm2D): + bn_init(m, 1) + + def forward(self, x, A=None, alpha=1): + x1, x2, x3 = self.conv1(x).mean(-2), self.conv2(x).mean(-2), self.conv3( + x) + x1 = self.tanh(x1.unsqueeze(-1) - x2.unsqueeze(-2)) + x1 = self.conv4(x1) * alpha + ( + A.unsqueeze(0).unsqueeze(0) if A is not None else 0) # N,C,V,V + # We only support 'paddle.einsum()' in dynamic graph mode, if use in infer model please implement self. + # x1 = paddle.einsum('ncuv,nctv->nctu', x1, x3) + x1 = einsum(x1, x3) + return x1 + + +class TemporalConv(nn.Layer): + + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + dilation=1): + super(TemporalConv, self).__init__() + pad = (kernel_size + (kernel_size - 1) * (dilation - 1) - 1) // 2 + self.conv = nn.Conv2D(in_channels, + out_channels, + kernel_size=(kernel_size, 1), + padding=(pad, 0), + stride=(stride, 1), + dilation=(dilation, 1)) + + self.bn = nn.BatchNorm2D(out_channels) + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + return x + + +class MultiScale_TemporalConv(nn.Layer): + + def __init__(self, + in_channels, + out_channels, + kernel_size=3, + stride=1, + dilations=[1, 2, 3, 4], + residual=True, + residual_kernel_size=1): + + super(MultiScale_TemporalConv, self).__init__() + assert out_channels % ( + len(dilations) + + 2) == 0, '# out channels should be multiples of # branches' + + # Multiple branches of temporal convolution + self.num_branches = len(dilations) + 2 + branch_channels = out_channels // self.num_branches + if type(kernel_size) == list: + assert len(kernel_size) == len(dilations) + else: + kernel_size = [kernel_size] * len(dilations) + # Temporal Convolution branches + self.branches = nn.LayerList([ + nn.Sequential( + nn.Conv2D(in_channels, + branch_channels, + kernel_size=1, + padding=0), + nn.BatchNorm2D(branch_channels), + nn.ReLU(), + TemporalConv(branch_channels, + branch_channels, + kernel_size=ks, + stride=stride, + dilation=dilation), + ) for ks, dilation in zip(kernel_size, dilations) + ]) + + # Additional Max & 1x1 branch + self.branches.append( + nn.Sequential( + nn.Conv2D(in_channels, + branch_channels, + kernel_size=1, + padding=0), nn.BatchNorm2D(branch_channels), + nn.ReLU(), + nn.MaxPool2D(kernel_size=(3, 1), + stride=(stride, 1), + padding=(1, 0)), nn.BatchNorm2D(branch_channels))) + + self.branches.append( + nn.Sequential( + nn.Conv2D(in_channels, + branch_channels, + kernel_size=1, + padding=0, + stride=(stride, 1)), nn.BatchNorm2D(branch_channels))) + + # Residual connection + if not residual: + self.residual = lambda x: 0 + elif (in_channels == out_channels) and (stride == 1): + self.residual = lambda x: x + else: + self.residual = TemporalConv(in_channels, + out_channels, + kernel_size=residual_kernel_size, + stride=stride) + + def init_weights(self): + """Initiate the parameters. + """ + # initialize + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + conv_init(m) + elif isinstance(m, nn.BatchNorm2D): + weight_init_(m.weight, 'Normal', std=0.02, mean=1.0) + nn.initializer.Constant(value=0.0)(m.bias) + + def forward(self, x): + # Input dim: (N,C,T,V) + res = self.residual(x) + branch_outs = [] + for tempconv in self.branches: + out = tempconv(x) + branch_outs.append(out) + + out = paddle.concat(branch_outs, axis=1) + out += res + return out + + +class unit_tcn(nn.Layer): + + def __init__(self, in_channels, out_channels, kernel_size=9, stride=1): + super(unit_tcn, self).__init__() + pad = int((kernel_size - 1) / 2) + self.conv = nn.Conv2D(in_channels, + out_channels, + kernel_size=(kernel_size, 1), + padding=(pad, 0), + stride=(stride, 1)) + + self.bn = nn.BatchNorm2D(out_channels) + self.relu = nn.ReLU() + conv_init(self.conv) + bn_init(self.bn, 1) + + def forward(self, x): + x = self.bn(self.conv(x)) + return x + + +class unit_gcn(nn.Layer): + + def __init__(self, + in_channels, + out_channels, + A, + coff_embedding=4, + adaptive=True, + residual=True): + super(unit_gcn, self).__init__() + inter_channels = out_channels // coff_embedding + self.inter_c = inter_channels + self.out_c = out_channels + self.in_c = in_channels + self.adaptive = adaptive + self.num_subset = A.shape[0] + self.convs = nn.LayerList() + + for i in range(self.num_subset): + self.convs.append(CTRGC(in_channels, out_channels)) + + if residual: + if in_channels != out_channels: + self.down = nn.Sequential( + nn.Conv2D(in_channels, out_channels, 1), + nn.BatchNorm2D(out_channels)) + else: + self.down = lambda x: x + else: + self.down = lambda x: 0 + if self.adaptive: + pa_param = paddle.ParamAttr( + initializer=paddle.nn.initializer.Assign(A.astype(np.float32))) + self.PA = paddle.create_parameter(shape=A.shape, + dtype='float32', + attr=pa_param) + else: + A_tensor = paddle.to_tensor(A, dtype="float32") + self.A = paddle.create_parameter( + shape=A_tensor.shape, + dtype='float32', + default_initializer=paddle.nn.initializer.Assign(A_tensor)) + self.A.stop_gradient = True + alpha_tensor = paddle.to_tensor(np.zeros(1), dtype="float32") + self.alpha = paddle.create_parameter( + shape=alpha_tensor.shape, + dtype='float32', + default_initializer=paddle.nn.initializer.Assign(alpha_tensor)) + self.bn = nn.BatchNorm2D(out_channels) + self.soft = nn.Softmax(-2) + self.relu = nn.ReLU() + + def init_weights(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + conv_init(m) + elif isinstance(m, nn.BatchNorm2D): + bn_init(m, 1) + bn_init(self.bn, 1e-6) + + def forward(self, x): + y = None + if self.adaptive: + A = self.PA + else: + A = self.A.cuda(x.get_device()) + for i in range(self.num_subset): + z = self.convs[i](x, A[i], self.alpha) + y = z + y if y is not None else z + y = self.bn(y) + y += self.down(x) + y = self.relu(y) + return y + + +class TCN_GCN_unit(nn.Layer): + + def __init__(self, + in_channels, + out_channels, + A, + stride=1, + residual=True, + adaptive=True, + kernel_size=5, + dilations=[1, 2]): + super(TCN_GCN_unit, self).__init__() + self.gcn1 = unit_gcn(in_channels, out_channels, A, adaptive=adaptive) + self.tcn1 = MultiScale_TemporalConv(out_channels, + out_channels, + kernel_size=kernel_size, + stride=stride, + dilations=dilations, + residual=False) + self.relu = nn.ReLU() + if not residual: + self.residual = lambda x: 0 + + elif (in_channels == out_channels) and (stride == 1): + self.residual = lambda x: x + + else: + self.residual = unit_tcn(in_channels, + out_channels, + kernel_size=1, + stride=stride) + + def forward(self, x): + y = self.relu(self.tcn1(self.gcn1(x)) + self.residual(x)) + return y + + +class NTUDGraph: + + def __init__(self, labeling_mode='spatial'): + num_node = 25 + self_link = [(i, i) for i in range(num_node)] + inward_ori_index = [(1, 2), (2, 21), (3, 21), (4, 3), (5, 21), (6, 5), + (7, 6), (8, 7), (9, 21), (10, 9), (11, 10), + (12, 11), (13, 1), (14, 13), (15, 14), (16, 15), + (17, 1), (18, 17), (19, 18), (20, 19), (22, 23), + (23, 8), (24, 25), (25, 12)] + inward = [(i - 1, j - 1) for (i, j) in inward_ori_index] + outward = [(j, i) for (i, j) in inward] + neighbor = inward + outward + + self.num_node = num_node + self.self_link = self_link + self.inward = inward + self.outward = outward + self.neighbor = neighbor + self.A = self.get_adjacency_matrix(labeling_mode) + + def edge2mat(self, link, num_node): + A = np.zeros((num_node, num_node)) + for i, j in link: + A[j, i] = 1 + return A + + def normalize_digraph(self, A): + Dl = np.sum(A, 0) + h, w = A.shape + Dn = np.zeros((w, w)) + for i in range(w): + if Dl[i] > 0: + Dn[i, i] = Dl[i]**(-1) + AD = np.dot(A, Dn) + return AD + + def get_spatial_graph(self, num_node, self_link, inward, outward): + I = self.edge2mat(self_link, num_node) + In = self.normalize_digraph(self.edge2mat(inward, num_node)) + Out = self.normalize_digraph(self.edge2mat(outward, num_node)) + A = np.stack((I, In, Out)) + return A + + def get_adjacency_matrix(self, labeling_mode=None): + if labeling_mode is None: + return self.A + if labeling_mode == 'spatial': + A = self.get_spatial_graph(self.num_node, self.self_link, + self.inward, self.outward) + else: + raise ValueError() + return A + + +@BACKBONES.register() +class CTRGCN(nn.Layer): + """ + CTR-GCN model from: + `"Channel-wise Topology Refinement Graph Convolution for Skeleton-Based Action Recognition" `_ + Args: + num_point: int, numbers of sketeton point. + num_person: int, numbers of person. + base_channel: int, model's hidden dim. + graph: str, sketeton adjacency matrix name. + graph_args: dict, sketeton adjacency graph class args. + in_channels: int, channels of vertex coordinate. 2 for (x,y), 3 for (x,y,z). Default 3. + adaptive: bool, if adjacency matrix can adaptive. + """ + + def __init__(self, + num_point=25, + num_person=2, + base_channel=64, + graph='ntu_rgb_d', + graph_args=dict(), + in_channels=3, + adaptive=True): + super(CTRGCN, self).__init__() + + if graph == 'ntu_rgb_d': + self.graph = NTUDGraph(**graph_args) + else: + raise ValueError() + + A = self.graph.A # 3,25,25 + + self.num_point = num_point + self.data_bn = nn.BatchNorm1D(num_person * in_channels * num_point) + self.base_channel = base_channel + + self.l1 = TCN_GCN_unit(in_channels, + self.base_channel, + A, + residual=False, + adaptive=adaptive) + self.l2 = TCN_GCN_unit(self.base_channel, + self.base_channel, + A, + adaptive=adaptive) + self.l3 = TCN_GCN_unit(self.base_channel, + self.base_channel, + A, + adaptive=adaptive) + self.l4 = TCN_GCN_unit(self.base_channel, + self.base_channel, + A, + adaptive=adaptive) + self.l5 = TCN_GCN_unit(self.base_channel, + self.base_channel * 2, + A, + stride=2, + adaptive=adaptive) + self.l6 = TCN_GCN_unit(self.base_channel * 2, + self.base_channel * 2, + A, + adaptive=adaptive) + self.l7 = TCN_GCN_unit(self.base_channel * 2, + self.base_channel * 2, + A, + adaptive=adaptive) + self.l8 = TCN_GCN_unit(self.base_channel * 2, + self.base_channel * 4, + A, + stride=2, + adaptive=adaptive) + self.l9 = TCN_GCN_unit(self.base_channel * 4, + self.base_channel * 4, + A, + adaptive=adaptive) + self.l10 = TCN_GCN_unit(self.base_channel * 4, + self.base_channel * 4, + A, + adaptive=adaptive) + + def init_weights(self): + bn_init(self.data_bn, 1) + + def forward(self, x): + N, C, T, V, M = x.shape + x = paddle.transpose(x, perm=[0, 4, 3, 1, 2]) + x = paddle.reshape(x, (N, M * V * C, T)) + + x = self.data_bn(x) + + x = paddle.reshape(x, (N, M, V, C, T)) + x = paddle.transpose(x, perm=(0, 1, 3, 4, 2)) + + x = paddle.reshape(x, (N * M, C, T, V)) + + x = self.l1(x) + x = self.l2(x) + x = self.l3(x) + x = self.l4(x) + x = self.l5(x) + x = self.l6(x) + x = self.l7(x) + x = self.l8(x) + x = self.l9(x) + x = self.l10(x) + + return x, N, M diff --git a/paddlevideo/modeling/backbones/deeplab.py b/paddlevideo/modeling/backbones/deeplab.py new file mode 100644 index 0000000000000000000000000000000000000000..c566205ac8bf12b35b5bbf0c4f77e13fbe4f4097 --- /dev/null +++ b/paddlevideo/modeling/backbones/deeplab.py @@ -0,0 +1,454 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import copy + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from ..registry import BACKBONES + + +class FrozenBatchNorm2D(nn.Layer): + """ + BatchNorm2D where the batch statistics and the affine parameters + are fixed + """ + def __init__(self, n, epsilon=1e-5): + super(FrozenBatchNorm2D, self).__init__() + x1 = paddle.ones([n]) + x2 = paddle.zeros([n]) + weight = self.create_parameter( + shape=x1.shape, default_initializer=nn.initializer.Assign(x1)) + bias = self.create_parameter( + shape=x2.shape, default_initializer=nn.initializer.Assign(x2)) + running_mean = self.create_parameter( + shape=x2.shape, default_initializer=nn.initializer.Assign(x2)) + running_var = self.create_parameter( + shape=x1.shape, default_initializer=nn.initializer.Assign(x1)) + self.add_parameter('weight', weight) + self.add_parameter('bias', bias) + self.add_parameter('running_mean', running_mean) + self.add_parameter('running_var', running_var) + self.epsilon = epsilon + + def forward(self, x): + scale = self.weight * paddle.rsqrt((self.running_var + self.epsilon)) + bias = self.bias - self.running_mean * scale + scale = paddle.reshape(scale, [1, -1, 1, 1]) + bias = paddle.reshape(bias, [1, -1, 1, 1]) + return x * scale + bias + + +class Bottleneck(nn.Layer): + expansion = 4 + + def __init__(self, + inplanes, + planes, + stride=1, + dilation=1, + downsample=None, + BatchNorm=None): + super(Bottleneck, self).__init__() + self.conv1 = nn.Conv2D(inplanes, planes, kernel_size=1, bias_attr=False) + self.bn1 = BatchNorm(planes) + self.conv2 = nn.Conv2D(planes, + planes, + kernel_size=3, + stride=stride, + dilation=dilation, + padding=dilation, + bias_attr=False) + self.bn2 = BatchNorm(planes) + self.conv3 = nn.Conv2D(planes, + planes * 4, + kernel_size=1, + bias_attr=False) + self.bn3 = BatchNorm(planes * 4) + self.relu = nn.ReLU() + self.downsample = downsample + self.stride = stride + self.dilation = dilation + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class ResNet(nn.Layer): + def __init__(self, + block, + layers, + output_stride, + BatchNorm, + pretrained=False): + self.inplanes = 64 + super(ResNet, self).__init__() + blocks = [1, 2, 4] + if output_stride == 16: + strides = [1, 2, 2, 1] + dilations = [1, 1, 1, 2] + elif output_stride == 8: + strides = [1, 2, 1, 1] + dilations = [1, 1, 2, 4] + else: + raise NotImplementedError + + # Modules + self.conv1 = nn.Conv2D(3, + 64, + kernel_size=7, + stride=2, + padding=3, + bias_attr=False) + self.bn1 = BatchNorm(64) + self.relu = nn.ReLU() + self.maxpool = nn.MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.layer1 = self._make_layer(block, + 64, + layers[0], + stride=strides[0], + dilation=dilations[0], + BatchNorm=BatchNorm) + self.layer2 = self._make_layer(block, + 128, + layers[1], + stride=strides[1], + dilation=dilations[1], + BatchNorm=BatchNorm) + self.layer3 = self._make_layer(block, + 256, + layers[2], + stride=strides[2], + dilation=dilations[2], + BatchNorm=BatchNorm) + self.layer4 = self._make_MG_unit(block, + 512, + blocks=blocks, + stride=strides[3], + dilation=dilations[3], + BatchNorm=BatchNorm) + self._init_weight() + + def _make_layer(self, + block, + planes, + blocks, + stride=1, + dilation=1, + BatchNorm=None): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D(self.inplanes, + planes * block.expansion, + kernel_size=1, + stride=stride, + bias_attr=False), + BatchNorm(planes * block.expansion), + ) + + layers = [] + layers.append( + block(self.inplanes, planes, stride, dilation, downsample, + BatchNorm)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append( + block(self.inplanes, + planes, + dilation=dilation, + BatchNorm=BatchNorm)) + + return nn.Sequential(*layers) + + def _make_MG_unit(self, + block, + planes, + blocks, + stride=1, + dilation=1, + BatchNorm=None): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2D(self.inplanes, + planes * block.expansion, + kernel_size=1, + stride=stride, + bias_attr=False), + BatchNorm(planes * block.expansion), + ) + + layers = [] + layers.append( + block(self.inplanes, + planes, + stride, + dilation=blocks[0] * dilation, + downsample=downsample, + BatchNorm=BatchNorm)) + self.inplanes = planes * block.expansion + for i in range(1, len(blocks)): + layers.append( + block(self.inplanes, + planes, + stride=1, + dilation=blocks[i] * dilation, + BatchNorm=BatchNorm)) + + return nn.Sequential(*layers) + + def forward(self, input, return_mid_level=False): + x = self.conv1(input) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + + x = self.layer1(x) + low_level_feat = x + x = self.layer2(x) + mid_level_feat = x + x = self.layer3(x) + x = self.layer4(x) + if return_mid_level: + return x, low_level_feat, mid_level_feat + else: + return x, low_level_feat + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + nn.initializer.KaimingNormal() + elif isinstance(m, nn.GroupNorm): + m.weight.data = nn.initializer.Constant(1) + m.bias.data = nn.initializer.Constant(0) + + +class _ASPPModule(nn.Layer): + def __init__(self, inplanes, planes, kernel_size, padding, dilation, + BatchNorm): + super(_ASPPModule, self).__init__() + self.atrous_conv = nn.Conv2D(inplanes, + planes, + kernel_size=kernel_size, + stride=1, + padding=padding, + dilation=dilation, + bias_attr=False) + self.bn = BatchNorm(planes) + self.relu = nn.ReLU() + + self._init_weight() + + def forward(self, x): + x = self.atrous_conv(x) + x = self.bn(x) + + return self.relu(x) + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + m.weight_attr = nn.initializer.KaimingNormal() + elif isinstance(m, nn.BatchNorm2D): + m.weight.data.fill_(1) + m.bias.data.zero_() + + +class ASPP(nn.Layer): + def __init__(self, backbone, output_stride, BatchNorm): + super(ASPP, self).__init__() + if backbone == 'drn': + inplanes = 512 + elif backbone == 'mobilenet': + inplanes = 320 + else: + inplanes = 2048 + if output_stride == 16: + dilations = [1, 6, 12, 18] + elif output_stride == 8: + dilations = [1, 12, 24, 36] + else: + raise NotImplementedError + + self.aspp1 = _ASPPModule(inplanes, + 256, + 1, + padding=0, + dilation=dilations[0], + BatchNorm=BatchNorm) + self.aspp2 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[1], + dilation=dilations[1], + BatchNorm=BatchNorm) + self.aspp3 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[2], + dilation=dilations[2], + BatchNorm=BatchNorm) + self.aspp4 = _ASPPModule(inplanes, + 256, + 3, + padding=dilations[3], + dilation=dilations[3], + BatchNorm=BatchNorm) + + self.global_avg_pool = nn.Sequential( + nn.AdaptiveAvgPool2D((1, 1)), + nn.Conv2D(inplanes, 256, 1, stride=1, bias_attr=False), + BatchNorm(256), nn.ReLU()) + self.conv1 = nn.Conv2D(1280, 256, 1, bias_attr=False) + self.bn1 = BatchNorm(256) + self.relu = nn.ReLU() + self.dropout = nn.Dropout(0.1) + self._init_weight() + + def forward(self, x): + x1 = self.aspp1(x) + x2 = self.aspp2(x) + x3 = self.aspp3(x) + x4 = self.aspp4(x) + x5 = self.global_avg_pool(x) + x5 = F.interpolate(x5, + size=x4.shape[2:], + mode='bilinear', + align_corners=True) + x = paddle.concat(x=[x1, x2, x3, x4, x5], axis=1) + + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + return self.dropout(x) + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + nn.initializer.KaimingNormal() + elif isinstance(m, nn.GroupNorm): + m.weight.data = nn.initializer.Constant(1) + m.bias.data = nn.initializer.Constant(0) + + +class Decoder(nn.Layer): + def __init__(self, backbone, BatchNorm): + super(Decoder, self).__init__() + if backbone == 'resnet': + low_level_inplanes = 256 + elif backbone == 'mobilenet': + raise NotImplementedError + else: + raise NotImplementedError + + self.conv1 = nn.Conv2D(low_level_inplanes, 48, 1, bias_attr=False) + self.bn1 = BatchNorm(48) + self.relu = nn.ReLU() + + self.last_conv = nn.Sequential( + nn.Conv2D(304, + 256, + kernel_size=3, + stride=1, + padding=1, + bias_attr=False), BatchNorm(256), nn.ReLU(), + nn.Sequential(), + nn.Conv2D(256, + 256, + kernel_size=3, + stride=1, + padding=1, + bias_attr=False), BatchNorm(256), nn.ReLU(), + nn.Sequential()) + + self._init_weight() + + def forward(self, x, low_level_feat): + low_level_feat = self.conv1(low_level_feat) + low_level_feat = self.bn1(low_level_feat) + low_level_feat = self.relu(low_level_feat) + + x = F.interpolate(x, + size=low_level_feat.shape[2:], + mode='bilinear', + align_corners=True) + x = paddle.concat(x=[x, low_level_feat], axis=1) + x = self.last_conv(x) + + return x + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + nn.initializer.KaimingNormal() + elif isinstance(m, nn.GroupNorm): + m.weight.data = nn.initializer.Constant(1) + m.bias.data = nn.initializer.Constant(0) + + +class DeepLab(nn.Layer): + """DeepLab model for segmentation""" + def __init__(self, backbone='resnet', output_stride=16, freeze_bn=True): + super(DeepLab, self).__init__() + + if freeze_bn == True: + print("Use frozen BN in DeepLab!") + BatchNorm = FrozenBatchNorm2D + else: + BatchNorm = nn.BatchNorm2D + + self.backbone = ResNet(Bottleneck, [3, 4, 23, 3], + output_stride, + BatchNorm, + pretrained=True) + self.aspp = ASPP(backbone, output_stride, BatchNorm) + self.decoder = Decoder(backbone, BatchNorm) + + def forward(self, input, return_aspp=False): + """forward function""" + if return_aspp: + x, low_level_feat, mid_level_feat = self.backbone(input, True) + else: + x, low_level_feat = self.backbone(input) + aspp_x = self.aspp(x) + x = self.decoder(aspp_x, low_level_feat) + + if return_aspp: + return x, aspp_x, low_level_feat, mid_level_feat + else: + return x, low_level_feat diff --git a/paddlevideo/modeling/backbones/movinet.py b/paddlevideo/modeling/backbones/movinet.py new file mode 100644 index 0000000000000000000000000000000000000000..cb6d4fddfa997d7cdc0095c04322f51656323c72 --- /dev/null +++ b/paddlevideo/modeling/backbones/movinet.py @@ -0,0 +1,574 @@ +import collections.abc +from itertools import repeat +from typing import Any, Callable, Optional, Tuple, Union + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from paddle.nn.layer import Identity + +from ..registry import BACKBONES +from collections import OrderedDict + +container_abcs = collections.abc +"""Model Config +""" + +A0 = {'block_num': [0, 1, 3, 3, 4, 4]} +A0['conv1'] = [3, 8, (1, 3, 3), (1, 2, 2), (0, 1, 1)] +A0['b2_l0'] = [8, 8, 24, (1, 5, 5), (1, 2, 2), (0, 2, 2), (0, 1, 1)] +A0['b3_l0'] = [8, 32, 80, (3, 3, 3), (1, 2, 2), (1, 0, 0), (0, 0, 0)] +A0['b3_l1'] = [32, 32, 80, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)] +A0['b3_l2'] = [32, 32, 80, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)] +A0['b4_l0'] = [32, 56, 184, (5, 3, 3), (1, 2, 2), (2, 0, 0), (0, 0, 0)] +A0['b4_l1'] = [56, 56, 112, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)] +A0['b4_l2'] = [56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)] +A0['b5_l0'] = [56, 56, 184, (5, 3, 3), (1, 1, 1), (2, 1, 1), (0, 1, 1)] +A0['b5_l1'] = [56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)] +A0['b5_l2'] = [56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)] +A0['b5_l3'] = [56, 56, 184, (3, 3, 3), (1, 1, 1), (1, 1, 1), (0, 1, 1)] +A0['b6_l0'] = [56, 104, 384, (5, 3, 3), (1, 2, 2), (2, 1, 1), (0, 1, 1)] +A0['b6_l1'] = [104, 104, 280, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)] +A0['b6_l2'] = [104, 104, 280, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)] +A0['b6_l3'] = [104, 104, 344, (1, 5, 5), (1, 1, 1), (0, 2, 2), (0, 1, 1)] +A0['conv7'] = [104, 480, (1, 1, 1), (1, 1, 1), (0, 0, 0)] + +MODEL_CONFIG = {'A0': A0} + + +def _ntuple(n): + def parse(x): + if isinstance(x, container_abcs.Iterable): + return x + return tuple(repeat(x, n)) + + return parse + + +def _make_divisible(v: float, + divisor: int, + min_value: Optional[int] = None) -> int: + """ + This function is taken from the original tf repo. + It ensures that all layers have a channel number that is divisible by 8. + It can be seen here: + https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py + """ + if min_value is None: + min_value = divisor + new_v = max(min_value, int(v + divisor / 2) // divisor * divisor) + # Make sure that round down does not go down by more than 10%. + if new_v < 0.9 * v: + new_v += divisor + return new_v + + +_single = _ntuple(1) +_pair = _ntuple(2) +_triple = _ntuple(3) +_quadruple = _ntuple(4) + + +class CausalModule(nn.Layer): + def __init__(self) -> None: + super().__init__() + self.activation = None + + def reset_activation(self) -> None: + self.activation = None + + +class Conv2dBNActivation(nn.Sequential): + def __init__( + self, + in_planes: int, + out_planes: int, + kernel_size: Union[int, Tuple[int, int]], + padding: Union[int, Tuple[int, int]], + stride: Union[int, Tuple[int, int]] = 1, + groups: int = 1, + norm_layer: Optional[Callable[..., nn.Layer]] = None, + activation_layer: Optional[Callable[..., nn.Layer]] = None, + **kwargs: Any, + ) -> None: + kernel_size = _pair(kernel_size) + stride = _pair(stride) + padding = _pair(padding) + if norm_layer is None: + norm_layer = Identity + if activation_layer is None: + activation_layer = Identity + self.kernel_size = kernel_size + self.stride = stride + dict_layers = (nn.Conv2D(in_planes, + out_planes, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups, + **kwargs), norm_layer(out_planes, + momentum=0.1), + activation_layer()) + + self.out_channels = out_planes + super(Conv2dBNActivation, self).__init__(dict_layers[0], dict_layers[1], + dict_layers[2]) + + +class Conv3DBNActivation(nn.Sequential): + def __init__( + self, + in_planes: int, + out_planes: int, + kernel_size: Union[int, Tuple[int, int, int]], + padding: Union[int, Tuple[int, int, int]], + stride: Union[int, Tuple[int, int, int]] = 1, + groups: int = 1, + norm_layer: Optional[Callable[..., nn.Layer]] = None, + activation_layer: Optional[Callable[..., nn.Layer]] = None, + **kwargs: Any, + ) -> None: + kernel_size = _triple(kernel_size) + stride = _triple(stride) + padding = _triple(padding) + if norm_layer is None: + norm_layer = Identity + if activation_layer is None: + activation_layer = Identity + self.kernel_size = kernel_size + self.stride = stride + + dict_layers = (nn.Conv3D(in_planes, + out_planes, + kernel_size=kernel_size, + stride=stride, + padding=padding, + groups=groups, + **kwargs), norm_layer(out_planes, + momentum=0.1), + activation_layer()) + self.out_channels = out_planes + super(Conv3DBNActivation, self).__init__(dict_layers[0], dict_layers[1], + dict_layers[2]) + + +class ConvBlock3D(CausalModule): + def __init__( + self, + in_planes: int, + out_planes: int, + kernel_size: Union[int, Tuple[int, int, int]], + causal: bool, + conv_type: str, + padding: Union[int, Tuple[int, int, int]] = 0, + stride: Union[int, Tuple[int, int, int]] = 1, + norm_layer: Optional[Callable[..., nn.Layer]] = None, + activation_layer: Optional[Callable[..., nn.Layer]] = None, + bias_attr: bool = False, + **kwargs: Any, + ) -> None: + super().__init__() + kernel_size = _triple(kernel_size) + stride = _triple(stride) + padding = _triple(padding) + self.conv_2 = None + + if causal is True: + padding = (0, padding[1], padding[2]) + if conv_type != "2plus1d" and conv_type != "3d": + raise ValueError("only 2plus2d or 3d are " + + "allowed as 3d convolutions") + + if conv_type == "2plus1d": + self.conv_1 = Conv2dBNActivation(in_planes, + out_planes, + kernel_size=(kernel_size[1], + kernel_size[2]), + padding=(padding[1], padding[2]), + stride=(stride[1], stride[2]), + activation_layer=activation_layer, + norm_layer=norm_layer, + bias_attr=bias_attr, + **kwargs) + if kernel_size[0] > 1: + self.conv_2 = Conv2dBNActivation( + in_planes, + out_planes, + kernel_size=(kernel_size[0], 1), + padding=(padding[0], 0), + stride=(stride[0], 1), + activation_layer=activation_layer, + norm_layer=norm_layer, + bias_attr=bias_attr, + **kwargs) + elif conv_type == "3d": + self.conv_1 = Conv3DBNActivation(in_planes, + out_planes, + kernel_size=kernel_size, + padding=padding, + activation_layer=activation_layer, + norm_layer=norm_layer, + stride=stride, + bias_attr=bias_attr, + **kwargs) + self.padding = padding + self.kernel_size = kernel_size + self.dim_pad = self.kernel_size[0] - 1 + self.stride = stride + self.causal = causal + self.conv_type = conv_type + + def _forward(self, x: paddle.Tensor) -> paddle.Tensor: + if self.dim_pad > 0 and self.conv_2 is None and self.causal is True: + x = self._cat_stream_buffer(x) + b, c, t, h, w = x.shape + if self.conv_type == "2plus1d": + x = paddle.transpose(x, (0, 2, 1, 3, 4)) # bcthw --> btchw + x = paddle.reshape_(x, (-1, c, h, w)) # btchw --> bt,c,h,w + x = self.conv_1(x) + if self.conv_type == "2plus1d": + b, c, h, w = x.shape + x = paddle.reshape_(x, (-1, t, c, h, w)) # bt,c,h,w --> b,t,c,h,w + x = paddle.transpose(x, (0, 2, 1, 3, 4)) # b,t,c,h,w --> b,c,t,h,w + if self.conv_2 is not None: + if self.dim_pad > 0 and self.causal is True: + x = self._cat_stream_buffer(x) + b, c, t, h, w = x.shape + x = paddle.reshape_(x, (b, c, t, h * w)) + x = self.conv_2(x) + b, c, t, _ = x.shape + x = paddle.reshape_(x, (b, c, t, h, w)) + return x + + def forward(self, x: paddle.Tensor) -> paddle.Tensor: + x = self._forward(x) + return x + + def _cat_stream_buffer(self, x: paddle.Tensor) -> paddle.Tensor: + if self.activation is None: + self._setup_activation(x.shape) + x = paddle.concat((self.activation, x), 2) + self._save_in_activation(x) + return x + + def _save_in_activation(self, x: paddle.Tensor) -> None: + assert self.dim_pad > 0 + self.activation = paddle.to_tensor(x.numpy()[:, :, -self.dim_pad:, + ...]).clone().detach() + + def _setup_activation(self, input_shape: Tuple[float, ...]) -> None: + assert self.dim_pad > 0 + self.activation = paddle.zeros(shape=[ + *input_shape[:2], # type: ignore + self.dim_pad, + *input_shape[3:] + ]) + + +class TemporalCGAvgPool3D(CausalModule): + def __init__(self, ) -> None: + super().__init__() + self.n_cumulated_values = 0 + self.register_forward_post_hook(self._detach_activation) + + def forward(self, x: paddle.Tensor) -> paddle.Tensor: + input_shape = x.shape + cumulative_sum = paddle.cumsum(x, axis=2) + if self.activation is None: + self.activation = cumulative_sum[:, :, -1:].clone() + else: + cumulative_sum += self.activation + self.activation = cumulative_sum[:, :, -1:].clone() + + noe = paddle.arange(1, input_shape[2] + 1) + axis = paddle.to_tensor([0, 1, 3, 4]) + noe = paddle.unsqueeze(noe, axis=axis) + divisor = noe.expand(x.shape) + x = cumulative_sum / (self.n_cumulated_values + divisor) + self.n_cumulated_values += input_shape[2] + return x + + @staticmethod + def _detach_activation(module: CausalModule, inputs: paddle.Tensor, + output: paddle.Tensor) -> None: + module.activation.detach() + + def reset_activation(self) -> None: + super().reset_activation() + self.n_cumulated_values = 0 + + +class SqueezeExcitation(nn.Layer): + def __init__(self, + input_channels: int, + activation_2: nn.Layer, + activation_1: nn.Layer, + conv_type: str, + causal: bool, + squeeze_factor: int = 4, + bias_attr: bool = True) -> None: + super().__init__() + self.causal = causal + se_multiplier = 2 if causal else 1 + squeeze_channels = _make_divisible( + input_channels // squeeze_factor * se_multiplier, 8) + self.temporal_cumualtive_GAvg3D = TemporalCGAvgPool3D() + self.fc1 = ConvBlock3D(input_channels * se_multiplier, + squeeze_channels, + kernel_size=(1, 1, 1), + padding=0, + causal=causal, + conv_type=conv_type, + bias_attr=bias_attr) + self.activation_1 = activation_1() + self.activation_2 = activation_2() + self.fc2 = ConvBlock3D(squeeze_channels, + input_channels, + kernel_size=(1, 1, 1), + padding=0, + causal=causal, + conv_type=conv_type, + bias_attr=bias_attr) + + def _scale(self, inputs: paddle.Tensor) -> paddle.Tensor: + if self.causal: + x_space = paddle.mean(inputs, axis=[3, 4], keepdim=True) + scale = self.temporal_cumualtive_GAvg3D(x_space) + scale = paddle.concat((scale, x_space), axis=1) + else: + scale = F.adaptive_avg_pool3d(inputs, 1) + scale = self.fc1(scale) + scale = self.activation_1(scale) + scale = self.fc2(scale) + return self.activation_2(scale) + + def forward(self, inputs: paddle.Tensor) -> paddle.Tensor: + scale = self._scale(inputs) + return scale * inputs + + +class BasicBneck(nn.Layer): + def __init__( + self, + input_channels, + out_channels, + expanded_channels, + kernel_size, + stride, + padding, + padding_avg, + causal: bool, + conv_type: str, + norm_layer: Optional[Callable[..., nn.Layer]] = None, + activation_layer: Optional[Callable[..., nn.Layer]] = None, + ) -> None: + super().__init__() + + assert type(stride) is tuple + + if (not stride[0] == 1 or not (1 <= stride[1] <= 2) + or not (1 <= stride[2] <= 2)): + raise ValueError('illegal stride value') + + self.res = None + + layers = [] + if expanded_channels != out_channels: + # expand + self.expand = ConvBlock3D(in_planes=input_channels, + out_planes=expanded_channels, + kernel_size=(1, 1, 1), + padding=(0, 0, 0), + causal=causal, + conv_type=conv_type, + norm_layer=norm_layer, + activation_layer=activation_layer) + # deepwise + self.deep = ConvBlock3D(in_planes=expanded_channels, + out_planes=expanded_channels, + kernel_size=kernel_size, + padding=padding, + stride=stride, + groups=expanded_channels, + causal=causal, + conv_type=conv_type, + norm_layer=norm_layer, + activation_layer=activation_layer) + + # SE + self.se = SqueezeExcitation( + expanded_channels, + causal=causal, + activation_1=activation_layer, + activation_2=(nn.Sigmoid if conv_type == "3d" else nn.Hardsigmoid), + conv_type=conv_type) + # project + self.project = ConvBlock3D(expanded_channels, + out_channels, + kernel_size=(1, 1, 1), + padding=(0, 0, 0), + causal=causal, + conv_type=conv_type, + norm_layer=norm_layer, + activation_layer=Identity) + + if not (stride == (1, 1, 1) and input_channels == out_channels): + if stride != (1, 1, 1): + layers.append( + nn.AvgPool3D((1, 3, 3), stride=stride, padding=padding_avg)) + layers.append( + ConvBlock3D( + in_planes=input_channels, + out_planes=out_channels, + kernel_size=(1, 1, 1), + padding=(0, 0, 0), + norm_layer=norm_layer, + activation_layer=Identity, + causal=causal, + conv_type=conv_type, + )) + self.res = nn.Sequential(*layers) + self.alpha = self.create_parameter(shape=[1], dtype="float32") + + def forward(self, inputs: paddle.Tensor) -> paddle.Tensor: + if self.res is not None: + residual = self.res(inputs) + else: + residual = inputs + if self.expand is not None: + x = self.expand(inputs) + else: + x = inputs + + x = self.deep(x) + x = self.se(x) + x = self.project(x) + result = residual + self.alpha * x + return result + + +@BACKBONES.register() +class MoViNet(nn.Layer): + def __init__( + self, + model_type: str = 'A0', + hidden_dim: int = 2048, + causal: bool = True, + num_classes: int = 400, + conv_type: str = "3d", + ) -> None: + super().__init__() + """ + causal: causal mode + num_classes: number of classes for classifcation + conv_type: type of convolution either 3d or 2plus1d + """ + blocks_dic = OrderedDict() + cfg = MODEL_CONFIG[model_type] + + norm_layer = nn.BatchNorm3D if conv_type == "3d" else nn.BatchNorm2D + activation_layer = nn.Swish if conv_type == "3d" else nn.Hardswish + + # conv1 + self.conv1 = ConvBlock3D(in_planes=cfg['conv1'][0], + out_planes=cfg['conv1'][1], + kernel_size=cfg['conv1'][2], + stride=cfg['conv1'][3], + padding=cfg['conv1'][4], + causal=causal, + conv_type=conv_type, + norm_layer=norm_layer, + activation_layer=activation_layer) + # blocks + for i in range(2, len(cfg['block_num']) + 1): + for j in range(cfg['block_num'][i - 1]): + blocks_dic[f'b{i}_l{j}'] = BasicBneck( + cfg[f'b{i}_l{j}'][0], + cfg[f'b{i}_l{j}'][1], + cfg[f'b{i}_l{j}'][2], + cfg[f'b{i}_l{j}'][3], + cfg[f'b{i}_l{j}'][4], + cfg[f'b{i}_l{j}'][5], + cfg[f'b{i}_l{j}'][6], + causal=causal, + conv_type=conv_type, + norm_layer=norm_layer, + activation_layer=activation_layer) + self.blocks = nn.Sequential(*(blocks_dic.values())) + + # conv7 + self.conv7 = ConvBlock3D(in_planes=cfg['conv7'][0], + out_planes=cfg['conv7'][1], + kernel_size=cfg['conv7'][2], + stride=cfg['conv7'][3], + padding=cfg['conv7'][4], + causal=causal, + conv_type=conv_type, + norm_layer=norm_layer, + activation_layer=activation_layer) + # pool + self.classifier = nn.Sequential( + # dense9 + ConvBlock3D(in_planes=cfg['conv7'][1], + out_planes=hidden_dim, + kernel_size=(1, 1, 1), + causal=causal, + conv_type=conv_type, + bias_attr=True), + nn.Swish(), + nn.Dropout(p=0.2), + # dense10d + ConvBlock3D(in_planes=hidden_dim, + out_planes=num_classes, + kernel_size=(1, 1, 1), + causal=causal, + conv_type=conv_type, + bias_attr=True), + ) + if causal: + self.cgap = TemporalCGAvgPool3D() + self.apply(self._weight_init) + self.causal = causal + + def avg(self, x: paddle.Tensor) -> paddle.Tensor: + if self.causal: + avg = F.adaptive_avg_pool3d(x, (x.shape[2], 1, 1)) + avg = self.cgap(avg)[:, :, -1:] + else: + avg = F.adaptive_avg_pool3d(x, 1) + return avg + + @staticmethod + def _weight_init(m): + if isinstance(m, nn.Conv3D): + nn.initializer.KaimingNormal(m.weight) + if m.bias is not None: + nn.initializer.Constant(0.0)(m.bias) + elif isinstance(m, (nn.BatchNorm3D, nn.BatchNorm2D, nn.GroupNorm)): + nn.initializer.Constant(1.0)(m.weight) + nn.initializer.Constant(0.0)(m.bias) + elif isinstance(m, nn.Linear): + nn.initializer.Normal(m.weight, 0, 0.01) + nn.initializer.Constant(0.0)(m.bias) + + def forward(self, x: paddle.Tensor) -> paddle.Tensor: + x = self.conv1(x) + x = self.blocks(x) + x = self.conv7(x) + x = self.avg(x) + x = self.classifier(x) + x = x.flatten(1) + return x + + @staticmethod + def _clean_activation_buffers(m): + if issubclass(type(m), CausalModule): + m.reset_activation() + + def clean_activation_buffers(self) -> None: + self.apply(self._clean_activation_buffers) + + +if __name__ == '__main__': + net = MoViNet(causal=False, conv_type='3d') + paddle.summary(net, input_size=(1, 3, 8, 224, 224)) diff --git a/paddlevideo/modeling/backbones/ms_tcn.py b/paddlevideo/modeling/backbones/ms_tcn.py new file mode 100644 index 0000000000000000000000000000000000000000..fb49b9c808c1707616364a6912bb9ab289771adf --- /dev/null +++ b/paddlevideo/modeling/backbones/ms_tcn.py @@ -0,0 +1,154 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np +import copy +import random +import math + +from paddle import ParamAttr +from ..registry import BACKBONES +from ..weight_init import weight_init_ + + +def _calculate_fan_in_and_fan_out(tensor): + dimensions = len(tensor.shape) + if dimensions < 2: + raise ValueError("Fan in and fan out can not be computed \ + for tensor with fewer than 2 dimensions") + + if dimensions == 2: # Linear + fan_in = tensor.shape[1] + fan_out = tensor.shape[0] + else: + num_input_fmaps = tensor.shape[1] + num_output_fmaps = tensor.shape[0] + receptive_field_size = 1 + if tensor.dim() > 2: + receptive_field_size = tensor[0][0].numel() + fan_in = num_input_fmaps * receptive_field_size + fan_out = num_output_fmaps * receptive_field_size + + return fan_in, fan_out + + +def calculate_gain(nonlinearity=None, a=None): + if nonlinearity == 'tanh': + return 5.0 / 3 + elif nonlinearity == 'relu': + return math.sqrt(2.0) + elif nonlinearity == 'leaky_relu': + if a != None: + return math.sqrt(2.0 / (1 + a**2)) + else: + return math.sqrt(2.0 / (1 + 0.01**2)) + elif nonlinearity == 'selu': + return 3.0 / 4 + else: + return 1 + + +def KaimingUniform_like_torch(weight_npy, + mode='fan_in', + nonlinearity='leaky_relu'): + fan_in, fan_out = _calculate_fan_in_and_fan_out(weight_npy) + if mode == 'fan_in': + fan_mode = fan_in + else: + fan_mode = fan_out + a = math.sqrt(5.0) + gain = calculate_gain(nonlinearity=nonlinearity, a=a) + std = gain / math.sqrt(fan_mode) + bound = math.sqrt(3.0) * std + return np.random.uniform(-bound, bound, weight_npy.shape) + + +def init_bias(weight_npy, bias_npy): + # attention this weight is not bias + fan_in, fan_out = _calculate_fan_in_and_fan_out(weight_npy) + bound = 1.0 / math.sqrt(fan_in) + return np.random.uniform(-bound, bound, bias_npy.shape) + + +class SingleStageModel(nn.Layer): + + def __init__(self, num_layers, num_f_maps, dim, num_classes): + super(SingleStageModel, self).__init__() + self.conv_in = nn.Conv1D(dim, num_f_maps, 1) + self.layers = nn.LayerList([ + copy.deepcopy(DilatedResidualLayer(2**i, num_f_maps, num_f_maps)) + for i in range(num_layers) + ]) + self.conv_out = nn.Conv1D(num_f_maps, num_classes, 1) + + def forward(self, x): + out = self.conv_in(x) + for layer in self.layers: + out = layer(out) + out = self.conv_out(out) + return out + + +class DilatedResidualLayer(nn.Layer): + + def __init__(self, dilation, in_channels, out_channels): + super(DilatedResidualLayer, self).__init__() + self.conv_dilated = nn.Conv1D(in_channels, + out_channels, + 3, + padding=dilation, + dilation=dilation) + self.conv_in = nn.Conv1D(out_channels, out_channels, 1) + self.dropout = nn.Dropout() + + def forward(self, x): + out = F.relu(self.conv_dilated(x)) + out = self.conv_in(out) + out = self.dropout(out) + return (x + out) + + +@BACKBONES.register() +class MSTCN(nn.Layer): + + def __init__(self, num_stages, num_layers, num_f_maps, dim, num_classes): + super().__init__() + self.stage1 = SingleStageModel(num_layers, num_f_maps, dim, num_classes) + self.stages = nn.LayerList([ + copy.deepcopy( + SingleStageModel(num_layers, num_f_maps, num_classes, + num_classes)) for s in range(num_stages - 1) + ]) + + def forward(self, x): + """ MSTCN forward + """ + out = self.stage1(x) + outputs = out.unsqueeze(0) + for s in self.stages: + out = s(F.softmax(out, axis=1)) + outputs = paddle.concat((outputs, out.unsqueeze(0)), axis=0) + return outputs + + def init_weights(self): + for layer in self.sublayers(): + if isinstance(layer, nn.Conv1D): + layer.weight.set_value( + KaimingUniform_like_torch(layer.weight).astype('float32')) + if layer.bias is not None: + layer.bias.set_value( + init_bias(layer.weight, layer.bias).astype('float32')) diff --git a/paddlevideo/modeling/backbones/resnet.py b/paddlevideo/modeling/backbones/resnet.py new file mode 100644 index 0000000000000000000000000000000000000000..2f07991a2349e9cecc3b17d46bf3079a1d255695 --- /dev/null +++ b/paddlevideo/modeling/backbones/resnet.py @@ -0,0 +1,283 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import math + +import paddle +import paddle.nn as nn +from paddle.nn import (Conv2D, BatchNorm2D, Linear, Dropout, MaxPool2D, + AvgPool2D) +from paddle import ParamAttr +import paddle.nn.functional as F + +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from ...utils import load_ckpt + + +class ConvBNLayer(nn.Layer): + """Conv2D and BatchNorm2D layer. + + Args: + in_channels (int): Number of channels for the input. + out_channels (int): Number of channels for the output. + kernel_size (int): Kernel size. + stride (int): Stride in the Conv2D layer. Default: 1. + groups (int): Groups in the Conv2D, Default: 1. + act (str): Indicate activation after BatchNorm2D layer. + name (str): the name of an instance of ConvBNLayer. + + Note: weight and bias initialization include initialize values and name the restored parameters, values initialization are explicit declared in the ```init_weights``` method. + + """ + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + act=None, + name=None): + super(ConvBNLayer, self).__init__() + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + weight_attr=ParamAttr(name=name + "_weights"), + bias_attr=False) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + + self._act = act + + self._batch_norm = BatchNorm2D(out_channels, + weight_attr=ParamAttr(name=bn_name + + "_scale"), + bias_attr=ParamAttr(bn_name + "_offset")) + + def forward(self, inputs): + y = self._conv(inputs) + y = self._batch_norm(y) + if self._act: + y = getattr(paddle.nn.functional, self._act)(y) + return y + + +class BottleneckBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + name=None): + super(BottleneckBlock, self).__init__() + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + act="relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act="relu", + name=name + "_branch2b") + + self.conv2 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels * 4, + kernel_size=1, + act=None, + name=name + "_branch2c") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels * 4, + kernel_size=1, + stride=stride, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + conv2 = self.conv2(conv1) + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv2) + return F.relu(y) + + +class BasicBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + name=None): + super(BasicBlock, self).__init__() + self.stride = stride + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + filter_size=3, + stride=stride, + act="relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + filter_size=3, + act=None, + name=name + "_branch2b") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + filter_size=1, + stride=stride, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(short, conv1) + y = F.relu(y) + return y + + +@BACKBONES.register() +class ResNet(nn.Layer): + """ResNet backbone. + + Args: + depth (int): Depth of resnet model. + pretrained (str): pretrained model. Default: None. + """ + def __init__(self, depth, pretrained=None): + super(ResNet, self).__init__() + self.pretrained = pretrained + self.layers = depth + + supported_layers = [18, 34, 50, 101, 152] + assert self.layers in supported_layers, \ + "supported layers are {} but input layer is {}".format( + supported_layers, self.layers) + + if self.layers == 18: + depth = [2, 2, 2, 2] + elif self.layers == 34 or self.layers == 50: + depth = [3, 4, 6, 3] + elif self.layers == 101: + depth = [3, 4, 23, 3] + elif self.layers == 152: + depth = [3, 8, 36, 3] + + in_channels = [64, 256, 512, 1024] + out_channels = [64, 128, 256, 512] + + self.conv = ConvBNLayer(in_channels=3, + out_channels=64, + kernel_size=7, + stride=2, + act="relu", + name="conv1") + self.pool2D_max = MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.block_list = [] + if self.layers >= 50: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + if self.layers in [101, 152] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + bottleneck_block = self.add_sublayer( + conv_name, + BottleneckBlock( + # NOTE: Be careful! Here is different from TSM model. + in_channels=in_channels[block] + if i == 0 else out_channels[block] * 4, + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + name=conv_name)) + + self.block_list.append(bottleneck_block) + shortcut = True + else: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + conv_name = "res" + str(block + 2) + chr(97 + i) + basic_block = self.add_sublayer( + conv_name, + BasicBlock(in_channels=in_channels[block] + if i == 0 else out_channels[block], + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + name=conv_name)) + self.block_list.append(basic_block) + shortcut = True + + def init_weights(self): + """Initiate the parameters. + Note: + 1. when indicate pretrained loading path, will load it to initiate backbone. + 2. when not indicating pretrained loading path, will follow specific initialization initiate backbone. Always, Conv2D layer will be initiated by KaimingNormal function, and BatchNorm2d will be initiated by Constant function. + Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/kaiming/KaimingNormal_en.html + """ + #XXX: check bias!!! check pretrained!!! + + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + #XXX: no bias + weight_init_(layer, 'KaimingNormal') + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Constant', value=1) + + def forward(self, inputs): + """Define how the backbone is going to run. + + """ + #NOTE: Already merge axis 0(batches) and axis 1(channels) before extracting feature phase, + # please refer to paddlevideo/modeling/framework/recognizers/recognizer2d.py#L27 + #y = paddle.reshape( + # inputs, [-1, inputs.shape[2], inputs.shape[3], inputs.shape[4]]) + + y = self.conv(inputs) + y = self.pool2D_max(y) + for block in self.block_list: + y = block(y) + return y diff --git a/paddlevideo/modeling/backbones/resnet_slowfast.py b/paddlevideo/modeling/backbones/resnet_slowfast.py new file mode 100644 index 0000000000000000000000000000000000000000..a67915946b014172f39621bfdb0c62a65e0c8cd9 --- /dev/null +++ b/paddlevideo/modeling/backbones/resnet_slowfast.py @@ -0,0 +1,795 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn.functional as F +from paddle.nn.initializer import KaimingNormal +from ..registry import BACKBONES +from paddlevideo.utils.multigrid import get_norm +import sys +import numpy as np +import paddle.distributed as dist + +# seed random seed +paddle.framework.seed(0) + + +# get init parameters for conv layer +def get_conv_init(fan_out): + return KaimingNormal(fan_in=fan_out) + + +def get_bn_param_attr(bn_weight=1.0, coeff=0.0): + param_attr = paddle.ParamAttr( + initializer=paddle.nn.initializer.Constant(bn_weight), + regularizer=paddle.regularizer.L2Decay(coeff)) + return param_attr + + +"""Video models.""" + + +class BottleneckTransform(paddle.nn.Layer): + """ + Bottleneck transformation: Tx1x1, 1x3x3, 1x1x1, where T is the size of + temporal kernel. + """ + def __init__(self, + dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups, + stride_1x1=False, + inplace_relu=True, + eps=1e-5, + dilation=1, + norm_module=paddle.nn.BatchNorm3D): + """ + Args: + dim_in (int): the channel dimensions of the input. + dim_out (int): the channel dimension of the output. + temp_kernel_size (int): the temporal kernel sizes of the middle + convolution in the bottleneck. + stride (int): the stride of the bottleneck. + dim_inner (int): the inner dimension of the block. + num_groups (int): number of groups for the convolution. num_groups=1 + is for standard ResNet like networks, and num_groups>1 is for + ResNeXt like networks. + stride_1x1 (bool): if True, apply stride to 1x1 conv, otherwise + apply stride to the 3x3 conv. + inplace_relu (bool): if True, calculate the relu on the original + input without allocating new memory. + eps (float): epsilon for batch norm. + dilation (int): size of dilation. + """ + super(BottleneckTransform, self).__init__() + self.temp_kernel_size = temp_kernel_size + self._inplace_relu = inplace_relu + self._eps = eps + self._stride_1x1 = stride_1x1 + self.norm_module = norm_module + self._construct(dim_in, dim_out, stride, dim_inner, num_groups, + dilation) + + def _construct(self, dim_in, dim_out, stride, dim_inner, num_groups, + dilation): + str1x1, str3x3 = (stride, 1) if self._stride_1x1 else (1, stride) + + fan = (dim_inner) * (self.temp_kernel_size * 1 * 1) + initializer_tmp = get_conv_init(fan) + + self.a = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_inner, + kernel_size=[self.temp_kernel_size, 1, 1], + stride=[1, str1x1, str1x1], + padding=[int(self.temp_kernel_size // 2), 0, 0], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self.a_bn = self.norm_module(num_features=dim_inner, + epsilon=self._eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + # 1x3x3, BN, ReLU. + fan = (dim_inner) * (1 * 3 * 3) + initializer_tmp = get_conv_init(fan) + + self.b = paddle.nn.Conv3D( + in_channels=dim_inner, + out_channels=dim_inner, + kernel_size=[1, 3, 3], + stride=[1, str3x3, str3x3], + padding=[0, dilation, dilation], + groups=num_groups, + dilation=[1, dilation, dilation], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self.b_bn = self.norm_module(num_features=dim_inner, + epsilon=self._eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + # 1x1x1, BN. + fan = (dim_out) * (1 * 1 * 1) + initializer_tmp = get_conv_init(fan) + + self.c = paddle.nn.Conv3D( + in_channels=dim_inner, + out_channels=dim_out, + kernel_size=[1, 1, 1], + stride=[1, 1, 1], + padding=[0, 0, 0], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self.c_bn = self.norm_module( + num_features=dim_out, + epsilon=self._eps, + weight_attr=get_bn_param_attr(bn_weight=0.0), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + def forward(self, x): + # Branch2a. + x = self.a(x) + x = self.a_bn(x) + x = F.relu(x) + + # Branch2b. + x = self.b(x) + x = self.b_bn(x) + x = F.relu(x) + + # Branch2c + x = self.c(x) + x = self.c_bn(x) + return x + + +class ResBlock(paddle.nn.Layer): + """ + Residual block. + """ + def __init__(self, + dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups=1, + stride_1x1=False, + inplace_relu=True, + eps=1e-5, + dilation=1, + norm_module=paddle.nn.BatchNorm3D): + """ + ResBlock class constructs redisual blocks. More details can be found in: + Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun. + "Deep residual learning for image recognition." + https://arxiv.org/abs/1512.03385 + Args: + dim_in (int): the channel dimensions of the input. + dim_out (int): the channel dimension of the output. + temp_kernel_size (int): the temporal kernel sizes of the middle + convolution in the bottleneck. + stride (int): the stride of the bottleneck. + trans_func (string): transform function to be used to construct the + bottleneck. + dim_inner (int): the inner dimension of the block. + num_groups (int): number of groups for the convolution. num_groups=1 + is for standard ResNet like networks, and num_groups>1 is for + ResNeXt like networks. + stride_1x1 (bool): if True, apply stride to 1x1 conv, otherwise + apply stride to the 3x3 conv. + inplace_relu (bool): calculate the relu on the original input + without allocating new memory. + eps (float): epsilon for batch norm. + dilation (int): size of dilation. + """ + super(ResBlock, self).__init__() + self._inplace_relu = inplace_relu + self._eps = eps + self.norm_module = norm_module + self._construct( + dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups, + stride_1x1, + inplace_relu, + dilation, + ) + + def _construct( + self, + dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups, + stride_1x1, + inplace_relu, + dilation, + ): + # Use skip connection with projection if dim or res change. + if (dim_in != dim_out) or (stride != 1): + fan = (dim_out) * (1 * 1 * 1) + initializer_tmp = get_conv_init(fan) + self.branch1 = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_out, + kernel_size=1, + stride=[1, stride, stride], + padding=0, + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False, + dilation=1) + self.branch1_bn = self.norm_module( + num_features=dim_out, + epsilon=self._eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + self.branch2 = BottleneckTransform(dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups, + stride_1x1=stride_1x1, + inplace_relu=inplace_relu, + dilation=dilation, + norm_module=self.norm_module) + + def forward(self, x): + if hasattr(self, "branch1"): + x1 = self.branch1(x) + x1 = self.branch1_bn(x1) + x2 = self.branch2(x) + x = paddle.add(x=x1, y=x2) + else: + x2 = self.branch2(x) + x = paddle.add(x=x, y=x2) + + x = F.relu(x) + return x + + +class ResStage(paddle.nn.Layer): + """ + Stage of 3D ResNet. It expects to have one or more tensors as input for + multi-pathway (SlowFast) cases. More details can be found here: + + Christoph Feichtenhofer, Haoqi Fan, Jitendra Malik, and Kaiming He. + "Slowfast networks for video recognition." + https://arxiv.org/pdf/1812.03982.pdf + """ + def __init__(self, + dim_in, + dim_out, + stride, + temp_kernel_sizes, + num_blocks, + dim_inner, + num_groups, + num_block_temp_kernel, + dilation, + stride_1x1=False, + inplace_relu=True, + norm_module=paddle.nn.BatchNorm3D): + """ + The `__init__` method of any subclass should also contain these arguments. + ResStage builds p streams, where p can be greater or equal to one. + Args: + dim_in (list): list of p the channel dimensions of the input. + Different channel dimensions control the input dimension of + different pathways. + dim_out (list): list of p the channel dimensions of the output. + Different channel dimensions control the input dimension of + different pathways. + temp_kernel_sizes (list): list of the p temporal kernel sizes of the + convolution in the bottleneck. Different temp_kernel_sizes + control different pathway. + stride (list): list of the p strides of the bottleneck. Different + stride control different pathway. + num_blocks (list): list of p numbers of blocks for each of the + pathway. + dim_inner (list): list of the p inner channel dimensions of the + input. Different channel dimensions control the input dimension + of different pathways. + num_groups (list): list of number of p groups for the convolution. + num_groups=1 is for standard ResNet like networks, and + num_groups>1 is for ResNeXt like networks. + num_block_temp_kernel (list): extent the temp_kernel_sizes to + num_block_temp_kernel blocks, then fill temporal kernel size + of 1 for the rest of the layers. + dilation (list): size of dilation for each pathway. + """ + super(ResStage, self).__init__() + assert all((num_block_temp_kernel[i] <= num_blocks[i] + for i in range(len(temp_kernel_sizes)))) + self.num_blocks = num_blocks + self.temp_kernel_sizes = [ + (temp_kernel_sizes[i] * num_blocks[i])[:num_block_temp_kernel[i]] + + [1] * (num_blocks[i] - num_block_temp_kernel[i]) + for i in range(len(temp_kernel_sizes)) + ] + assert (len({ + len(dim_in), + len(dim_out), + len(temp_kernel_sizes), + len(stride), + len(num_blocks), + len(dim_inner), + len(num_groups), + len(num_block_temp_kernel), + }) == 1) + self.num_pathways = len(self.num_blocks) + self.norm_module = norm_module + self._construct( + dim_in, + dim_out, + stride, + dim_inner, + num_groups, + stride_1x1, + inplace_relu, + dilation, + ) + + def _construct( + self, + dim_in, + dim_out, + stride, + dim_inner, + num_groups, + stride_1x1, + inplace_relu, + dilation, + ): + + for pathway in range(self.num_pathways): + for i in range(self.num_blocks[pathway]): + res_block = ResBlock( + dim_in[pathway] if i == 0 else dim_out[pathway], + dim_out[pathway], + self.temp_kernel_sizes[pathway][i], + stride[pathway] if i == 0 else 1, + dim_inner[pathway], + num_groups[pathway], + stride_1x1=stride_1x1, + inplace_relu=inplace_relu, + dilation=dilation[pathway], + norm_module=self.norm_module) + self.add_sublayer("pathway{}_res{}".format(pathway, i), + res_block) + + def forward(self, inputs): + output = [] + for pathway in range(self.num_pathways): + x = inputs[pathway] + + for i in range(self.num_blocks[pathway]): + m = getattr(self, "pathway{}_res{}".format(pathway, i)) + x = m(x) + output.append(x) + + return output + + +class ResNetBasicStem(paddle.nn.Layer): + """ + ResNe(X)t 3D stem module. + Performs spatiotemporal Convolution, BN, and Relu following by a + spatiotemporal pooling. + """ + def __init__(self, + dim_in, + dim_out, + kernel, + stride, + padding, + eps=1e-5, + norm_module=paddle.nn.BatchNorm3D): + super(ResNetBasicStem, self).__init__() + self.kernel = kernel + self.stride = stride + self.padding = padding + self.eps = eps + self.norm_module = norm_module + self._construct_stem(dim_in, dim_out) + + def _construct_stem(self, dim_in, dim_out): + fan = (dim_out) * (self.kernel[0] * self.kernel[1] * self.kernel[2]) + initializer_tmp = get_conv_init(fan) + + self._conv = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_out, + kernel_size=self.kernel, + stride=self.stride, + padding=self.padding, + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self._bn = self.norm_module(num_features=dim_out, + epsilon=self.eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + def forward(self, x): + x = self._conv(x) + x = self._bn(x) + x = F.relu(x) + + x = F.max_pool3d(x=x, + kernel_size=[1, 3, 3], + stride=[1, 2, 2], + padding=[0, 1, 1], + data_format="NCDHW") + return x + + +class VideoModelStem(paddle.nn.Layer): + """ + Video 3D stem module. Provides stem operations of Conv, BN, ReLU, MaxPool + on input data tensor for slow and fast pathways. + """ + def __init__(self, + dim_in, + dim_out, + kernel, + stride, + padding, + eps=1e-5, + norm_module=paddle.nn.BatchNorm3D): + """ + Args: + dim_in (list): the list of channel dimensions of the inputs. + dim_out (list): the output dimension of the convolution in the stem + layer. + kernel (list): the kernels' size of the convolutions in the stem + layers. Temporal kernel size, height kernel size, width kernel + size in order. + stride (list): the stride sizes of the convolutions in the stem + layer. Temporal kernel stride, height kernel size, width kernel + size in order. + padding (list): the paddings' sizes of the convolutions in the stem + layer. Temporal padding size, height padding size, width padding + size in order. + eps (float): epsilon for batch norm. + """ + super(VideoModelStem, self).__init__() + + assert (len({ + len(dim_in), + len(dim_out), + len(kernel), + len(stride), + len(padding), + }) == 1), "Input pathway dimensions are not consistent." + self.num_pathways = len(dim_in) + self.kernel = kernel + self.stride = stride + self.padding = padding + self.eps = eps + self.norm_module = norm_module + self._construct_stem(dim_in, dim_out) + + def _construct_stem(self, dim_in, dim_out): + for pathway in range(len(dim_in)): + stem = ResNetBasicStem(dim_in[pathway], dim_out[pathway], + self.kernel[pathway], self.stride[pathway], + self.padding[pathway], self.eps, + self.norm_module) + self.add_sublayer("pathway{}_stem".format(pathway), stem) + + def forward(self, x): + assert (len(x) == self.num_pathways + ), "Input tensor does not contain {} pathway".format( + self.num_pathways) + + for pathway in range(len(x)): + m = getattr(self, "pathway{}_stem".format(pathway)) + x[pathway] = m(x[pathway]) + + return x + + +class FuseFastToSlow(paddle.nn.Layer): + """ + Fuses the information from the Fast pathway to the Slow pathway. Given the + tensors from Slow pathway and Fast pathway, fuse information from Fast to + Slow, then return the fused tensors from Slow and Fast pathway in order. + """ + def __init__(self, + dim_in, + fusion_conv_channel_ratio, + fusion_kernel, + alpha, + fuse_bn_relu=1, + eps=1e-5, + norm_module=paddle.nn.BatchNorm3D): + """ + Args: + dim_in (int): the channel dimension of the input. + fusion_conv_channel_ratio (int): channel ratio for the convolution + used to fuse from Fast pathway to Slow pathway. + fusion_kernel (int): kernel size of the convolution used to fuse + from Fast pathway to Slow pathway. + alpha (int): the frame rate ratio between the Fast and Slow pathway. + eps (float): epsilon for batch norm. + """ + super(FuseFastToSlow, self).__init__() + self.fuse_bn_relu = fuse_bn_relu + fan = (dim_in * fusion_conv_channel_ratio) * (fusion_kernel * 1 * 1) + initializer_tmp = get_conv_init(fan) + + self._conv_f2s = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_in * fusion_conv_channel_ratio, + kernel_size=[fusion_kernel, 1, 1], + stride=[alpha, 1, 1], + padding=[fusion_kernel // 2, 0, 0], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self._bn = norm_module(num_features=dim_in * fusion_conv_channel_ratio, + epsilon=eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + def forward(self, x): + x_s = x[0] + x_f = x[1] + fuse = self._conv_f2s(x_f) + # TODO: For AVA, set fuse_bn_relu=1, check mAP's improve. + if self.fuse_bn_relu: + fuse = self._bn(fuse) + fuse = F.relu(fuse) + x_s_fuse = paddle.concat(x=[x_s, fuse], axis=1, name=None) + + return [x_s_fuse, x_f] + + +@BACKBONES.register() +class ResNetSlowFast(paddle.nn.Layer): + """ + SlowFast model builder for SlowFast network. + + Christoph Feichtenhofer, Haoqi Fan, Jitendra Malik, and Kaiming He. + "Slowfast networks for video recognition." + https://arxiv.org/pdf/1812.03982.pdf + """ + def __init__( + self, + alpha, + beta, + bn_norm_type="batchnorm", + bn_num_splits=1, + num_pathways=2, + depth=50, + num_groups=1, + input_channel_num=[3, 3], + width_per_group=64, + fusion_conv_channel_ratio=2, + fusion_kernel_sz=7, #5? + pool_size_ratio=[[1, 1, 1], [1, 1, 1]], + fuse_bn_relu = 1, + spatial_strides = [[1, 1], [2, 2], [2, 2], [2, 2]], + use_pool_af_s2 = 1, + ): + """ + Args: + cfg (CfgNode): model building configs, details are in the + comments of the config file. + """ + super(ResNetSlowFast, self).__init__() + + self.alpha = alpha #8 + self.beta = beta #8 + self.norm_module = get_norm(bn_norm_type, bn_num_splits) + self.num_pathways = num_pathways + self.depth = depth + self.num_groups = num_groups + self.input_channel_num = input_channel_num + self.width_per_group = width_per_group + self.fusion_conv_channel_ratio = fusion_conv_channel_ratio + self.fusion_kernel_sz = fusion_kernel_sz # NOTE: modify to 7 in 8*8, 5 in old implement + self.pool_size_ratio = pool_size_ratio + self.fuse_bn_relu = fuse_bn_relu + self.spatial_strides = spatial_strides + self.use_pool_af_s2 = use_pool_af_s2 + self._construct_network() + + def _construct_network(self): + """ + Builds a SlowFast model. + The first pathway is the Slow pathway + and the second pathway is the Fast pathway. + + Args: + cfg (CfgNode): model building configs, details are in the + comments of the config file. + """ + temp_kernel = [ + [[1], [5]], # conv1 temporal kernel for slow and fast pathway. + [[1], [3]], # res2 temporal kernel for slow and fast pathway. + [[1], [3]], # res3 temporal kernel for slow and fast pathway. + [[3], [3]], # res4 temporal kernel for slow and fast pathway. + [[3], [3]], + ] # res5 temporal kernel for slow and fast pathway. + + self.s1 = VideoModelStem( + dim_in=self.input_channel_num, + dim_out=[self.width_per_group, self.width_per_group // self.beta], + kernel=[temp_kernel[0][0] + [7, 7], temp_kernel[0][1] + [7, 7]], + stride=[[1, 2, 2]] * 2, + padding=[ + [temp_kernel[0][0][0] // 2, 3, 3], + [temp_kernel[0][1][0] // 2, 3, 3], + ], + norm_module=self.norm_module) + self.s1_fuse = FuseFastToSlow( + dim_in=self.width_per_group // self.beta, + fusion_conv_channel_ratio=self.fusion_conv_channel_ratio, + fusion_kernel=self.fusion_kernel_sz, + alpha=self.alpha, + norm_module=self.norm_module, + fuse_bn_relu=self.fuse_bn_relu) + + # ResNet backbone + MODEL_STAGE_DEPTH = {50: (3, 4, 6, 3)} + (d2, d3, d4, d5) = MODEL_STAGE_DEPTH[self.depth] + + num_block_temp_kernel = [[3, 3], [4, 4], [6, 6], [3, 3]] + spatial_dilations = [[1, 1], [1, 1], [1, 1], [1, 1]] + spatial_strides = self.spatial_strides + #spatial_strides = [[1, 1], [2, 2], [2, 2], [2, 2]] + #spatial_strides = [[1, 1], [2, 2], [2, 2], [1, 1]] #TODO:check which value is FAIR's impliment + + out_dim_ratio = self.beta // self.fusion_conv_channel_ratio #4 + dim_inner = self.width_per_group * self.num_groups #64 + + self.s2 = ResStage(dim_in=[ + self.width_per_group + self.width_per_group // out_dim_ratio, + self.width_per_group // self.beta, + ], + dim_out=[ + self.width_per_group * 4, + self.width_per_group * 4 // self.beta, + ], + dim_inner=[dim_inner, dim_inner // self.beta], + temp_kernel_sizes=temp_kernel[1], + stride=spatial_strides[0], + num_blocks=[d2] * 2, + num_groups=[self.num_groups] * 2, + num_block_temp_kernel=num_block_temp_kernel[0], + dilation=spatial_dilations[0], + norm_module=self.norm_module) + + self.s2_fuse = FuseFastToSlow( + dim_in=self.width_per_group * 4 // self.beta, + fusion_conv_channel_ratio=self.fusion_conv_channel_ratio, + fusion_kernel=self.fusion_kernel_sz, + alpha=self.alpha, + norm_module=self.norm_module, + fuse_bn_relu=self.fuse_bn_relu, + ) + + self.s3 = ResStage( + dim_in=[ + self.width_per_group * 4 + + self.width_per_group * 4 // out_dim_ratio, + self.width_per_group * 4 // self.beta, + ], + dim_out=[ + self.width_per_group * 8, + self.width_per_group * 8 // self.beta, + ], + dim_inner=[dim_inner * 2, dim_inner * 2 // self.beta], + temp_kernel_sizes=temp_kernel[2], + stride=spatial_strides[1], + num_blocks=[d3] * 2, + num_groups=[self.num_groups] * 2, + num_block_temp_kernel=num_block_temp_kernel[1], + dilation=spatial_dilations[1], + norm_module=self.norm_module, + ) + + self.s3_fuse = FuseFastToSlow( + dim_in=self.width_per_group * 8 // self.beta, + fusion_conv_channel_ratio=self.fusion_conv_channel_ratio, + fusion_kernel=self.fusion_kernel_sz, + alpha=self.alpha, + norm_module=self.norm_module, + fuse_bn_relu=self.fuse_bn_relu, + ) + + self.s4 = ResStage( + dim_in=[ + self.width_per_group * 8 + + self.width_per_group * 8 // out_dim_ratio, + self.width_per_group * 8 // self.beta, + ], + dim_out=[ + self.width_per_group * 16, + self.width_per_group * 16 // self.beta, + ], + dim_inner=[dim_inner * 4, dim_inner * 4 // self.beta], + temp_kernel_sizes=temp_kernel[3], + stride=spatial_strides[2], + num_blocks=[d4] * 2, + num_groups=[self.num_groups] * 2, + num_block_temp_kernel=num_block_temp_kernel[2], + dilation=spatial_dilations[2], + norm_module=self.norm_module, + ) + + self.s4_fuse = FuseFastToSlow( + dim_in=self.width_per_group * 16 // self.beta, + fusion_conv_channel_ratio=self.fusion_conv_channel_ratio, + fusion_kernel=self.fusion_kernel_sz, + alpha=self.alpha, + norm_module=self.norm_module, + fuse_bn_relu=self.fuse_bn_relu, + ) + + self.s5 = ResStage( + dim_in=[ + self.width_per_group * 16 + + self.width_per_group * 16 // out_dim_ratio, + self.width_per_group * 16 // self.beta, + ], + dim_out=[ + self.width_per_group * 32, + self.width_per_group * 32 // self.beta, + ], + dim_inner=[dim_inner * 8, dim_inner * 8 // self.beta], + temp_kernel_sizes=temp_kernel[4], + stride=spatial_strides[3], + num_blocks=[d5] * 2, + num_groups=[self.num_groups] * 2, + num_block_temp_kernel=num_block_temp_kernel[3], + dilation=spatial_dilations[3], + norm_module=self.norm_module, + ) + + def init_weights(self): + pass + + def forward(self, x): + x = self.s1(x) #VideoModelStem + x = self.s1_fuse(x) #FuseFastToSlow + x = self.s2(x) #ResStage + x = self.s2_fuse(x) + + # TODO: For AVA, set use_pool_af_s2=1, check mAP's improve. + if self.use_pool_af_s2: + for pathway in range(self.num_pathways): + x[pathway] = F.max_pool3d(x=x[pathway], + kernel_size=self.pool_size_ratio[pathway], + stride=self.pool_size_ratio[pathway], + padding=[0, 0, 0], + data_format="NCDHW") + + x = self.s3(x) + x = self.s3_fuse(x) + x = self.s4(x) + x = self.s4_fuse(x) + x = self.s5(x) + return x diff --git a/paddlevideo/modeling/backbones/resnet_slowfast_MRI.py b/paddlevideo/modeling/backbones/resnet_slowfast_MRI.py new file mode 100644 index 0000000000000000000000000000000000000000..d348d45cf20186c9d89e666d0dcc5ac93cf66363 --- /dev/null +++ b/paddlevideo/modeling/backbones/resnet_slowfast_MRI.py @@ -0,0 +1,796 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn.functional as F +from paddle.nn.initializer import KaimingNormal +from ..registry import BACKBONES +from paddlevideo.utils.multigrid import get_norm +import sys +import numpy as np +import paddle.distributed as dist + +# seed random seed +paddle.framework.seed(0) + + +# get init parameters for conv layer +def get_conv_init(fan_out): + return KaimingNormal(fan_in=fan_out) + + +def get_bn_param_attr(bn_weight=1.0, coeff=0.0): + param_attr = paddle.ParamAttr( + initializer=paddle.nn.initializer.Constant(bn_weight), + regularizer=paddle.regularizer.L2Decay(coeff)) + return param_attr + + +"""Video models.""" + + +class BottleneckTransform(paddle.nn.Layer): + """ + Bottleneck transformation: Tx1x1, 1x3x3, 1x1x1, where T is the size of + temporal kernel. + """ + def __init__(self, + dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups, + stride_1x1=False, + inplace_relu=True, + eps=1e-5, + dilation=1, + norm_module=paddle.nn.BatchNorm3D): + """ + Args: + dim_in (int): the channel dimensions of the input. + dim_out (int): the channel dimension of the output. + temp_kernel_size (int): the temporal kernel sizes of the middle + convolution in the bottleneck. + stride (int): the stride of the bottleneck. + dim_inner (int): the inner dimension of the block. + num_groups (int): number of groups for the convolution. num_groups=1 + is for standard ResNet like networks, and num_groups>1 is for + ResNeXt like networks. + stride_1x1 (bool): if True, apply stride to 1x1 conv, otherwise + apply stride to the 3x3 conv. + inplace_relu (bool): if True, calculate the relu on the original + input without allocating new memory. + eps (float): epsilon for batch norm. + dilation (int): size of dilation. + """ + super(BottleneckTransform, self).__init__() + self.temp_kernel_size = temp_kernel_size + self._inplace_relu = inplace_relu + self._eps = eps + self._stride_1x1 = stride_1x1 + self.norm_module = norm_module + self._construct(dim_in, dim_out, stride, dim_inner, num_groups, + dilation) + + def _construct(self, dim_in, dim_out, stride, dim_inner, num_groups, + dilation): + str1x1, str3x3 = (stride, 1) if self._stride_1x1 else (1, stride) + + fan = (dim_inner) * (self.temp_kernel_size * 1 * 1) + initializer_tmp = get_conv_init(fan) + + self.a = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_inner, + kernel_size=[self.temp_kernel_size, 1, 1], + stride=[1, str1x1, str1x1], + padding=[int(self.temp_kernel_size // 2), 0, 0], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self.a_bn = self.norm_module(num_features=dim_inner, + epsilon=self._eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + # 1x3x3, BN, ReLU. + fan = (dim_inner) * (1 * 3 * 3) + initializer_tmp = get_conv_init(fan) + + self.b = paddle.nn.Conv3D( + in_channels=dim_inner, + out_channels=dim_inner, + kernel_size=[1, 3, 3], + stride=[1, str3x3, str3x3], + padding=[0, dilation, dilation], + groups=num_groups, + dilation=[1, dilation, dilation], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self.b_bn = self.norm_module(num_features=dim_inner, + epsilon=self._eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + # 1x1x1, BN. + fan = (dim_out) * (1 * 1 * 1) + initializer_tmp = get_conv_init(fan) + + self.c = paddle.nn.Conv3D( + in_channels=dim_inner, + out_channels=dim_out, + kernel_size=[1, 1, 1], + stride=[1, 1, 1], + padding=[0, 0, 0], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self.c_bn = self.norm_module( + num_features=dim_out, + epsilon=self._eps, + weight_attr=get_bn_param_attr(bn_weight=0.0), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + def forward(self, x): + # Branch2a. + x = self.a(x) + x = self.a_bn(x) + x = F.relu(x) + + # Branch2b. + x = self.b(x) + x = self.b_bn(x) + x = F.relu(x) + + # Branch2c + x = self.c(x) + x = self.c_bn(x) + return x + + +class ResBlock(paddle.nn.Layer): + """ + Residual block. + """ + def __init__(self, + dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups=1, + stride_1x1=False, + inplace_relu=True, + eps=1e-5, + dilation=1, + norm_module=paddle.nn.BatchNorm3D): + """ + ResBlock class constructs redisual blocks. More details can be found in: + Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun. + "Deep residual learning for image recognition." + https://arxiv.org/abs/1512.03385 + Args: + dim_in (int): the channel dimensions of the input. + dim_out (int): the channel dimension of the output. + temp_kernel_size (int): the temporal kernel sizes of the middle + convolution in the bottleneck. + stride (int): the stride of the bottleneck. + trans_func (string): transform function to be used to construct the + bottleneck. + dim_inner (int): the inner dimension of the block. + num_groups (int): number of groups for the convolution. num_groups=1 + is for standard ResNet like networks, and num_groups>1 is for + ResNeXt like networks. + stride_1x1 (bool): if True, apply stride to 1x1 conv, otherwise + apply stride to the 3x3 conv. + inplace_relu (bool): calculate the relu on the original input + without allocating new memory. + eps (float): epsilon for batch norm. + dilation (int): size of dilation. + """ + super(ResBlock, self).__init__() + self._inplace_relu = inplace_relu + self._eps = eps + self.norm_module = norm_module + self._construct( + dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups, + stride_1x1, + inplace_relu, + dilation, + ) + + def _construct( + self, + dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups, + stride_1x1, + inplace_relu, + dilation, + ): + # Use skip connection with projection if dim or res change. + if (dim_in != dim_out) or (stride != 1): + fan = (dim_out) * (1 * 1 * 1) + initializer_tmp = get_conv_init(fan) + self.branch1 = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_out, + kernel_size=1, + stride=[1, stride, stride], + padding=0, + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False, + dilation=1) + self.branch1_bn = self.norm_module( + num_features=dim_out, + epsilon=self._eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + self.branch2 = BottleneckTransform(dim_in, + dim_out, + temp_kernel_size, + stride, + dim_inner, + num_groups, + stride_1x1=stride_1x1, + inplace_relu=inplace_relu, + dilation=dilation, + norm_module=self.norm_module) + + def forward(self, x): + if hasattr(self, "branch1"): + x1 = self.branch1(x) + x1 = self.branch1_bn(x1) + x2 = self.branch2(x) + x = paddle.add(x=x1, y=x2) + else: + x2 = self.branch2(x) + x = paddle.add(x=x, y=x2) + + x = F.relu(x) + return x + + +class ResStage(paddle.nn.Layer): + """ + Stage of 3D ResNet. It expects to have one or more tensors as input for + multi-pathway (SlowFast) cases. More details can be found here: + + Christoph Feichtenhofer, Haoqi Fan, Jitendra Malik, and Kaiming He. + "Slowfast networks for video recognition." + https://arxiv.org/pdf/1812.03982.pdf + """ + def __init__(self, + dim_in, + dim_out, + stride, + temp_kernel_sizes, + num_blocks, + dim_inner, + num_groups, + num_block_temp_kernel, + dilation, + stride_1x1=False, + inplace_relu=True, + norm_module=paddle.nn.BatchNorm3D): + """ + The `__init__` method of any subclass should also contain these arguments. + ResStage builds p streams, where p can be greater or equal to one. + Args: + dim_in (list): list of p the channel dimensions of the input. + Different channel dimensions control the input dimension of + different pathways. + dim_out (list): list of p the channel dimensions of the output. + Different channel dimensions control the input dimension of + different pathways. + temp_kernel_sizes (list): list of the p temporal kernel sizes of the + convolution in the bottleneck. Different temp_kernel_sizes + control different pathway. + stride (list): list of the p strides of the bottleneck. Different + stride control different pathway. + num_blocks (list): list of p numbers of blocks for each of the + pathway. + dim_inner (list): list of the p inner channel dimensions of the + input. Different channel dimensions control the input dimension + of different pathways. + num_groups (list): list of number of p groups for the convolution. + num_groups=1 is for standard ResNet like networks, and + num_groups>1 is for ResNeXt like networks. + num_block_temp_kernel (list): extent the temp_kernel_sizes to + num_block_temp_kernel blocks, then fill temporal kernel size + of 1 for the rest of the layers. + dilation (list): size of dilation for each pathway. + """ + super(ResStage, self).__init__() + assert all((num_block_temp_kernel[i] <= num_blocks[i] + for i in range(len(temp_kernel_sizes)))) + self.num_blocks = num_blocks + self.temp_kernel_sizes = [ + (temp_kernel_sizes[i] * num_blocks[i])[:num_block_temp_kernel[i]] + + [1] * (num_blocks[i] - num_block_temp_kernel[i]) + for i in range(len(temp_kernel_sizes)) + ] + assert (len({ + len(dim_in), + len(dim_out), + len(temp_kernel_sizes), + len(stride), + len(num_blocks), + len(dim_inner), + len(num_groups), + len(num_block_temp_kernel), + }) == 1) + self.num_pathways = len(self.num_blocks) + self.norm_module = norm_module + self._construct( + dim_in, + dim_out, + stride, + dim_inner, + num_groups, + stride_1x1, + inplace_relu, + dilation, + ) + + def _construct( + self, + dim_in, + dim_out, + stride, + dim_inner, + num_groups, + stride_1x1, + inplace_relu, + dilation, + ): + + for pathway in range(self.num_pathways): + for i in range(self.num_blocks[pathway]): + res_block = ResBlock( + dim_in[pathway] if i == 0 else dim_out[pathway], + dim_out[pathway], + self.temp_kernel_sizes[pathway][i], + stride[pathway] if i == 0 else 1, + dim_inner[pathway], + num_groups[pathway], + stride_1x1=stride_1x1, + inplace_relu=inplace_relu, + dilation=dilation[pathway], + norm_module=self.norm_module) + self.add_sublayer("pathway{}_res{}".format(pathway, i), + res_block) + + def forward(self, inputs): + output = [] + for pathway in range(self.num_pathways): + x = inputs[pathway] + + for i in range(self.num_blocks[pathway]): + m = getattr(self, "pathway{}_res{}".format(pathway, i)) + x = m(x) + output.append(x) + + return output + + +class ResNetBasicStem(paddle.nn.Layer): + """ + ResNe(X)t 3D stem module. + Performs spatiotemporal Convolution, BN, and Relu following by a + spatiotemporal pooling. + """ + def __init__(self, + dim_in, + dim_out, + kernel, + stride, + padding, + eps=1e-5, + norm_module=paddle.nn.BatchNorm3D): + super(ResNetBasicStem, self).__init__() + self.kernel = kernel + self.stride = stride + self.padding = padding + self.eps = eps + self.norm_module = norm_module + self._construct_stem(dim_in, dim_out) + + def _construct_stem(self, dim_in, dim_out): + fan = (dim_out) * (self.kernel[0] * self.kernel[1] * self.kernel[2]) + initializer_tmp = get_conv_init(fan) + + self._conv = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_out, + kernel_size=self.kernel, + stride=self.stride, + padding=self.padding, + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self._bn = self.norm_module(num_features=dim_out, + epsilon=self.eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + def forward(self, x): + x = self._conv(x) + x = self._bn(x) + x = F.relu(x) + + x = F.max_pool3d(x=x, + kernel_size=[1, 3, 3], + stride=[1, 2, 2], + padding=[0, 1, 1], + data_format="NCDHW") + return x + + +class VideoModelStem(paddle.nn.Layer): + """ + Video 3D stem module. Provides stem operations of Conv, BN, ReLU, MaxPool + on input data tensor for slow and fast pathways. + """ + def __init__(self, + dim_in, + dim_out, + kernel, + stride, + padding, + eps=1e-5, + norm_module=paddle.nn.BatchNorm3D): + """ + Args: + dim_in (list): the list of channel dimensions of the inputs. + dim_out (list): the output dimension of the convolution in the stem + layer. + kernel (list): the kernels' size of the convolutions in the stem + layers. Temporal kernel size, height kernel size, width kernel + size in order. + stride (list): the stride sizes of the convolutions in the stem + layer. Temporal kernel stride, height kernel size, width kernel + size in order. + padding (list): the paddings' sizes of the convolutions in the stem + layer. Temporal padding size, height padding size, width padding + size in order. + eps (float): epsilon for batch norm. + """ + super(VideoModelStem, self).__init__() + + assert (len({ + len(dim_in), + len(dim_out), + len(kernel), + len(stride), + len(padding), + }) == 1), "Input pathway dimensions are not consistent." + self.num_pathways = len(dim_in) + self.kernel = kernel + self.stride = stride + self.padding = padding + self.eps = eps + self.norm_module = norm_module + self._construct_stem(dim_in, dim_out) + + def _construct_stem(self, dim_in, dim_out): + for pathway in range(len(dim_in)): + stem = ResNetBasicStem(dim_in[pathway], dim_out[pathway], + self.kernel[pathway], self.stride[pathway], + self.padding[pathway], self.eps, + self.norm_module) + self.add_sublayer("pathway{}_stem".format(pathway), stem) + + def forward(self, x): + assert (len(x) == self.num_pathways + ), "Input tensor does not contain {} pathway".format( + self.num_pathways) + + for pathway in range(len(x)): + m = getattr(self, "pathway{}_stem".format(pathway)) + x[pathway] = m(x[pathway]) + + return x + + +class FuseFastToSlow(paddle.nn.Layer): + """ + Fuses the information from the Fast pathway to the Slow pathway. Given the + tensors from Slow pathway and Fast pathway, fuse information from Fast to + Slow, then return the fused tensors from Slow and Fast pathway in order. + """ + def __init__(self, + dim_in, + fusion_conv_channel_ratio, + fusion_kernel, + alpha, + fuse_bn_relu=1, + eps=1e-5, + norm_module=paddle.nn.BatchNorm3D): + """ + Args: + dim_in (int): the channel dimension of the input. + fusion_conv_channel_ratio (int): channel ratio for the convolution + used to fuse from Fast pathway to Slow pathway. + fusion_kernel (int): kernel size of the convolution used to fuse + from Fast pathway to Slow pathway. + alpha (int): the frame rate ratio between the Fast and Slow pathway. + eps (float): epsilon for batch norm. + """ + super(FuseFastToSlow, self).__init__() + self.fuse_bn_relu = fuse_bn_relu + fan = (dim_in * fusion_conv_channel_ratio) * (fusion_kernel * 1 * 1) + initializer_tmp = get_conv_init(fan) + + self._conv_f2s = paddle.nn.Conv3D( + in_channels=dim_in, + out_channels=dim_in * fusion_conv_channel_ratio, + kernel_size=[fusion_kernel, 1, 1], + stride=[alpha, 1, 1], + padding=[fusion_kernel // 2, 0, 0], + weight_attr=paddle.ParamAttr(initializer=initializer_tmp), + bias_attr=False) + self._bn = norm_module(num_features=dim_in * fusion_conv_channel_ratio, + epsilon=eps, + weight_attr=get_bn_param_attr(), + bias_attr=get_bn_param_attr(bn_weight=0.0)) + + def forward(self, x): + x_s = x[0] + x_f = x[1] + fuse = self._conv_f2s(x_f) + # TODO: For AVA, set fuse_bn_relu=1, check mAP's improve. + if self.fuse_bn_relu: + fuse = self._bn(fuse) + fuse = F.relu(fuse) + x_s_fuse = paddle.concat(x=[x_s, fuse], axis=1, name=None) + + return [x_s_fuse, x_f] + + +@BACKBONES.register() +class ResNetSlowFast_MRI(paddle.nn.Layer): + """ + SlowFast model builder for SlowFast network. + + Christoph Feichtenhofer, Haoqi Fan, Jitendra Malik, and Kaiming He. + "Slowfast networks for video recognition." + https://arxiv.org/pdf/1812.03982.pdf + """ + def __init__( + self, + alpha, + beta, + bn_norm_type="batchnorm", + bn_num_splits=1, + num_pathways=2, + depth=50, + num_groups=1, + input_channel_num=[1, 1], + width_per_group=64, + fusion_conv_channel_ratio=2, + fusion_kernel_sz=7, #5? + pool_size_ratio=[[1, 1, 1], [1, 1, 1]], + fuse_bn_relu=1, + spatial_strides=[[1, 1], [2, 2], [2, 2], [2, 2]], + use_pool_af_s2=1, + ): + """ + Args: + cfg (CfgNode): model building configs, details are in the + comments of the config file. + """ + super(ResNetSlowFast_MRI, self).__init__() + + self.alpha = alpha #8 + self.beta = beta #8 + self.norm_module = get_norm(bn_norm_type, bn_num_splits) + self.num_pathways = num_pathways + self.depth = depth + self.num_groups = num_groups + self.input_channel_num = input_channel_num + self.width_per_group = width_per_group + self.fusion_conv_channel_ratio = fusion_conv_channel_ratio + self.fusion_kernel_sz = fusion_kernel_sz # NOTE: modify to 7 in 8*8, 5 in old implement + self.pool_size_ratio = pool_size_ratio + self.fuse_bn_relu = fuse_bn_relu + self.spatial_strides = spatial_strides + self.use_pool_af_s2 = use_pool_af_s2 + self._construct_network() + + def _construct_network(self): + """ + Builds a SlowFast model. + The first pathway is the Slow pathway + and the second pathway is the Fast pathway. + + Args: + cfg (CfgNode): model building configs, details are in the + comments of the config file. + """ + temp_kernel = [ + [[1], [5]], # conv1 temporal kernel for slow and fast pathway. + [[1], [3]], # res2 temporal kernel for slow and fast pathway. + [[1], [3]], # res3 temporal kernel for slow and fast pathway. + [[3], [3]], # res4 temporal kernel for slow and fast pathway. + [[3], [3]], + ] # res5 temporal kernel for slow and fast pathway. + + self.s1 = VideoModelStem( + dim_in=self.input_channel_num, + dim_out=[self.width_per_group, self.width_per_group // self.beta], + kernel=[temp_kernel[0][0] + [7, 7], temp_kernel[0][1] + [7, 7]], + stride=[[1, 2, 2]] * 2, + padding=[ + [temp_kernel[0][0][0] // 2, 3, 3], + [temp_kernel[0][1][0] // 2, 3, 3], + ], + norm_module=self.norm_module) + self.s1_fuse = FuseFastToSlow( + dim_in=self.width_per_group // self.beta, + fusion_conv_channel_ratio=self.fusion_conv_channel_ratio, + fusion_kernel=self.fusion_kernel_sz, + alpha=self.alpha, + norm_module=self.norm_module, + fuse_bn_relu=self.fuse_bn_relu) + + # ResNet backbone + MODEL_STAGE_DEPTH = {50: (3, 4, 6, 3)} + (d2, d3, d4, d5) = MODEL_STAGE_DEPTH[self.depth] + + num_block_temp_kernel = [[3, 3], [4, 4], [6, 6], [3, 3]] + spatial_dilations = [[1, 1], [1, 1], [1, 1], [1, 1]] + spatial_strides = self.spatial_strides + #spatial_strides = [[1, 1], [2, 2], [2, 2], [2, 2]] + #spatial_strides = [[1, 1], [2, 2], [2, 2], [1, 1]] #TODO:check which value is FAIR's impliment + + out_dim_ratio = self.beta // self.fusion_conv_channel_ratio #4 + dim_inner = self.width_per_group * self.num_groups #64 + + self.s2 = ResStage(dim_in=[ + self.width_per_group + self.width_per_group // out_dim_ratio, + self.width_per_group // self.beta, + ], + dim_out=[ + self.width_per_group * 4, + self.width_per_group * 4 // self.beta, + ], + dim_inner=[dim_inner, dim_inner // self.beta], + temp_kernel_sizes=temp_kernel[1], + stride=spatial_strides[0], + num_blocks=[d2] * 2, + num_groups=[self.num_groups] * 2, + num_block_temp_kernel=num_block_temp_kernel[0], + dilation=spatial_dilations[0], + norm_module=self.norm_module) + + self.s2_fuse = FuseFastToSlow( + dim_in=self.width_per_group * 4 // self.beta, + fusion_conv_channel_ratio=self.fusion_conv_channel_ratio, + fusion_kernel=self.fusion_kernel_sz, + alpha=self.alpha, + norm_module=self.norm_module, + fuse_bn_relu=self.fuse_bn_relu, + ) + + self.s3 = ResStage( + dim_in=[ + self.width_per_group * 4 + + self.width_per_group * 4 // out_dim_ratio, + self.width_per_group * 4 // self.beta, + ], + dim_out=[ + self.width_per_group * 8, + self.width_per_group * 8 // self.beta, + ], + dim_inner=[dim_inner * 2, dim_inner * 2 // self.beta], + temp_kernel_sizes=temp_kernel[2], + stride=spatial_strides[1], + num_blocks=[d3] * 2, + num_groups=[self.num_groups] * 2, + num_block_temp_kernel=num_block_temp_kernel[1], + dilation=spatial_dilations[1], + norm_module=self.norm_module, + ) + + self.s3_fuse = FuseFastToSlow( + dim_in=self.width_per_group * 8 // self.beta, + fusion_conv_channel_ratio=self.fusion_conv_channel_ratio, + fusion_kernel=self.fusion_kernel_sz, + alpha=self.alpha, + norm_module=self.norm_module, + fuse_bn_relu=self.fuse_bn_relu, + ) + + self.s4 = ResStage( + dim_in=[ + self.width_per_group * 8 + + self.width_per_group * 8 // out_dim_ratio, + self.width_per_group * 8 // self.beta, + ], + dim_out=[ + self.width_per_group * 16, + self.width_per_group * 16 // self.beta, + ], + dim_inner=[dim_inner * 4, dim_inner * 4 // self.beta], + temp_kernel_sizes=temp_kernel[3], + stride=spatial_strides[2], + num_blocks=[d4] * 2, + num_groups=[self.num_groups] * 2, + num_block_temp_kernel=num_block_temp_kernel[2], + dilation=spatial_dilations[2], + norm_module=self.norm_module, + ) + + self.s4_fuse = FuseFastToSlow( + dim_in=self.width_per_group * 16 // self.beta, + fusion_conv_channel_ratio=self.fusion_conv_channel_ratio, + fusion_kernel=self.fusion_kernel_sz, + alpha=self.alpha, + norm_module=self.norm_module, + fuse_bn_relu=self.fuse_bn_relu, + ) + + self.s5 = ResStage( + dim_in=[ + self.width_per_group * 16 + + self.width_per_group * 16 // out_dim_ratio, + self.width_per_group * 16 // self.beta, + ], + dim_out=[ + self.width_per_group * 32, + self.width_per_group * 32 // self.beta, + ], + dim_inner=[dim_inner * 8, dim_inner * 8 // self.beta], + temp_kernel_sizes=temp_kernel[4], + stride=spatial_strides[3], + num_blocks=[d5] * 2, + num_groups=[self.num_groups] * 2, + num_block_temp_kernel=num_block_temp_kernel[3], + dilation=spatial_dilations[3], + norm_module=self.norm_module, + ) + + def init_weights(self): + pass + + def forward(self, x): + x = self.s1(x) #VideoModelStem + x = self.s1_fuse(x) #FuseFastToSlow + x = self.s2(x) #ResStage + x = self.s2_fuse(x) + + # TODO: For AVA, set use_pool_af_s2=1, check mAP's improve. + if self.use_pool_af_s2: + for pathway in range(self.num_pathways): + x[pathway] = F.max_pool3d( + x=x[pathway], + kernel_size=self.pool_size_ratio[pathway], + stride=self.pool_size_ratio[pathway], + padding=[0, 0, 0], + data_format="NCDHW") + + x = self.s3(x) + x = self.s3_fuse(x) + x = self.s4(x) + x = self.s4_fuse(x) + x = self.s5(x) + return x diff --git a/paddlevideo/modeling/backbones/resnet_tsm.py b/paddlevideo/modeling/backbones/resnet_tsm.py new file mode 100644 index 0000000000000000000000000000000000000000..9fa5093e8021076153ed288a4a9e537911c8738a --- /dev/null +++ b/paddlevideo/modeling/backbones/resnet_tsm.py @@ -0,0 +1,340 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +from paddle.nn import (Conv2D, BatchNorm2D, Linear, Dropout, MaxPool2D, + AvgPool2D) +from paddle import ParamAttr +import paddle.nn.functional as F +from paddle.regularizer import L2Decay +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from ...utils import load_ckpt + + +class ConvBNLayer(nn.Layer): + """Conv2D and BatchNorm2D layer. + + Args: + in_channels (int): Number of channels for the input. + out_channels (int): Number of channels for the output. + kernel_size (int): Kernel size. + stride (int): Stride in the Conv2D layer. Default: 1. + groups (int): Groups in the Conv2D, Default: 1. + act (str): Indicate activation after BatchNorm2D layer. + name (str): the name of an instance of ConvBNLayer. + Note: weight and bias initialization include initialize values and name the restored parameters, values initialization are explicit declared in the ```init_weights``` method. + + """ + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + act=None, + name=None, + data_format="NCHW"): + super(ConvBNLayer, self).__init__() + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + weight_attr=ParamAttr(name=name + "_weights"), + bias_attr=False, + data_format=data_format) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + + self._act = act + + self._batch_norm = BatchNorm2D( + out_channels, + weight_attr=ParamAttr(name=bn_name + "_scale", + regularizer=L2Decay(0.0)), + bias_attr=ParamAttr(name=bn_name + "_offset", + regularizer=L2Decay(0.0)), + data_format=data_format) + + def forward(self, inputs): + y = self._conv(inputs) + y = self._batch_norm(y) + if self._act: + y = getattr(paddle.nn.functional, self._act)(y) + return y + + +class BottleneckBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + num_seg=8, + name=None, + data_format="NCHW"): + super(BottleneckBlock, self).__init__() + self.data_format = data_format + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + act="relu", + name=name + "_branch2a", + data_format=data_format) + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act="relu", + name=name + "_branch2b", + data_format=data_format) + + self.conv2 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels * 4, + kernel_size=1, + act=None, + name=name + "_branch2c", + data_format=data_format) + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels * 4, + kernel_size=1, + stride=stride, + name=name + "_branch1", + data_format=data_format) + + self.shortcut = shortcut + self.num_seg = num_seg + + def forward(self, inputs): + if paddle.fluid.core.is_compiled_with_npu(): + x = inputs + seg_num = self.num_seg + shift_ratio = 1.0 / self.num_seg + + shape = x.shape #[N*T, C, H, W] + reshape_x = x.reshape((-1, seg_num, shape[1], shape[2], shape[3])) #[N, T, C, H, W] + pad_x = paddle.fluid.layers.pad(reshape_x, [0,0,1,1,0,0,0,0,0,0,]) #[N, T+2, C, H, W] + c1 = int(shape[1] * shift_ratio) + c2 = int(shape[1] * 2 * shift_ratio) + slice1 = pad_x[:, :seg_num, :c1, :, :] + slice2 = pad_x[:, 2:seg_num+2, c1:c2, :, :] + slice3 = pad_x[:, 1:seg_num+1, c2:, :, :] + concat_x = paddle.concat([slice1, slice2, slice3], axis=2) #[N, T, C, H, W] + shifts = concat_x.reshape(shape) + else: + shifts = F.temporal_shift(inputs, + self.num_seg, + 1.0 / self.num_seg, + data_format=self.data_format) + + y = self.conv0(shifts) + conv1 = self.conv1(y) + conv2 = self.conv2(conv1) + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv2) + return F.relu(y) + + +class BasicBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + name=None, + data_format="NCHW"): + super(BasicBlock, self).__init__() + self.stride = stride + self.conv0 = ConvBNLayer( + in_channels=in_channels, + out_channels=out_channels, + filter_size=3, + stride=stride, + act="relu", + name=name + "_branch2a", + data_format=data_format, + ) + self.conv1 = ConvBNLayer( + in_channels=out_channels, + out_channels=out_channels, + filter_size=3, + act=None, + name=name + "_branch2b", + data_format=data_format, + ) + + if not shortcut: + self.short = ConvBNLayer( + in_channels=in_channels, + out_channels=out_channels, + filter_size=1, + stride=stride, + name=name + "_branch1", + data_format=data_format, + ) + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(short, conv1) + y = F.relu(y) + return y + + +@BACKBONES.register() +class ResNetTSM(nn.Layer): + """ResNet TSM backbone. + + Args: + depth (int): Depth of resnet model. + pretrained (str): pretrained model. Default: None. + """ + def __init__(self, depth, num_seg=8, data_format="NCHW", pretrained=None): + super(ResNetTSM, self).__init__() + self.pretrained = pretrained + self.layers = depth + self.num_seg = num_seg + self.data_format = data_format + + supported_layers = [18, 34, 50, 101, 152] + assert self.layers in supported_layers, \ + "supported layers are {} but input layer is {}".format( + supported_layers, self.layers) + + if self.layers == 18: + depth = [2, 2, 2, 2] + elif self.layers == 34 or self.layers == 50: + depth = [3, 4, 6, 3] + elif self.layers == 101: + depth = [3, 4, 23, 3] + elif self.layers == 152: + depth = [3, 8, 36, 3] + + in_channels = 64 + out_channels = [64, 128, 256, 512] + + self.conv = ConvBNLayer(in_channels=3, + out_channels=64, + kernel_size=7, + stride=2, + act="relu", + name="conv1", + data_format=self.data_format) + self.pool2D_max = MaxPool2D( + kernel_size=3, + stride=2, + padding=1, + data_format=self.data_format, + ) + + self.block_list = [] + if self.layers >= 50: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + if self.layers in [101, 152] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + bottleneck_block = self.add_sublayer( + conv_name, + BottleneckBlock( + in_channels=in_channels + if i == 0 else out_channels[block] * 4, + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + num_seg=self.num_seg, + shortcut=shortcut, + name=conv_name, + data_format=self.data_format)) + in_channels = out_channels[block] * 4 + self.block_list.append(bottleneck_block) + shortcut = True + else: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + conv_name = "res" + str(block + 2) + chr(97 + i) + basic_block = self.add_sublayer( + conv_name, + BasicBlock( + in_channels=in_channels[block] + if i == 0 else out_channels[block], + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + name=conv_name, + data_format=self.data_format, + )) + self.block_list.append(basic_block) + shortcut = True + + def init_weights(self): + """Initiate the parameters. + Note: + 1. when indicate pretrained loading path, will load it to initiate backbone. + 2. when not indicating pretrained loading path, will follow specific initialization initiate backbone. Always, Conv2D layer will be initiated by KaimingNormal function, and BatchNorm2d will be initiated by Constant function. + Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/kaiming/KaimingNormal_en.html + """ + #XXX: check bias!!! check pretrained!!! + + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + #XXX: no bias + weight_init_(layer, 'KaimingNormal') + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Constant', value=1) + + def forward(self, inputs): + """Define how the backbone is going to run. + + """ + #NOTE: (deprecated design) Already merge axis 0(batches) and axis 1(clips) before extracting feature phase, + # please refer to paddlevideo/modeling/framework/recognizers/recognizer2d.py#L27 + #y = paddle.reshape( + # inputs, [-1, inputs.shape[2], inputs.shape[3], inputs.shape[4]]) + + #NOTE: As paddlepaddle to_static method need a "pure" model to trim. It means from + # 1. the phase of generating data[images, label] from dataloader + # to + # 2. last layer of a model, always is FC layer + + y = self.conv(inputs) + y = self.pool2D_max(y) + for block in self.block_list: + y = block(y) + return y diff --git a/paddlevideo/modeling/backbones/resnet_tsm_MRI.py b/paddlevideo/modeling/backbones/resnet_tsm_MRI.py new file mode 100644 index 0000000000000000000000000000000000000000..ae4fdc70f99ddeee44adaeb06e8b9dbccff0768e --- /dev/null +++ b/paddlevideo/modeling/backbones/resnet_tsm_MRI.py @@ -0,0 +1,328 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import math + +import sys +import paddle +import paddle.nn as nn +from paddle.nn import (Conv2D, BatchNorm2D, Linear, Dropout, MaxPool2D, + AvgPool2D) +from paddle import ParamAttr +import paddle.nn.functional as F + +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from ...utils.save_load import load_ckpt +from paddle.regularizer import L2Decay + + +class ConvBNLayer(nn.Layer): + """Conv2D and BatchNorm2D layer. + + Args: + in_channels (int): Number of channels for the input. + out_channels (int): Number of channels for the output. + kernel_size (int): Kernel size. + stride (int): Stride in the Conv2D layer. Default: 1. + groups (int): Groups in the Conv2D, Default: 1. + is_tweaks_mode (bool): switch for tweaks. Default: False. + act (str): Indicate activation after BatchNorm2D layer. + name (str): the name of an instance of ConvBNLayer. + + Note: weight and bias initialization include initialize values and name the restored parameters, values initialization are explicit declared in the ```init_weights``` method. + + """ + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + is_tweaks_mode=False, + act=None, + name=None): + super(ConvBNLayer, self).__init__() + self.is_tweaks_mode = is_tweaks_mode + #ResNet-D 1/2:add a 2×2 average pooling layer with a stride of 2 before the convolution, + # whose stride is changed to 1, works well in practice. + self._pool2d_avg = AvgPool2D(kernel_size=2, + stride=2, + padding=0, + ceil_mode=True) + + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + weight_attr=ParamAttr(name=name + "_weights"), + bias_attr=False) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + + self._act = act + + self._batch_norm = BatchNorm2D( + out_channels, + weight_attr=ParamAttr(name=bn_name + "_scale", + regularizer=L2Decay(0.0)), + bias_attr=ParamAttr(bn_name + "_offset", regularizer=L2Decay(0.0))) + + def forward(self, inputs): + if self.is_tweaks_mode: + inputs = self._pool2d_avg(inputs) + y = self._conv(inputs) + y = self._batch_norm(y) + if self._act: + y = getattr(paddle.nn.functional, self._act)(y) + return y + + +class BottleneckBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + if_first=False, + num_seg=8, + name=None): + super(BottleneckBlock, self).__init__() + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + act="leaky_relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act="leaky_relu", + name=name + "_branch2b") + + self.conv2 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels * 4, + kernel_size=1, + act=None, + name=name + "_branch2c") + + if not shortcut: + self.short = ConvBNLayer( + in_channels=in_channels, + out_channels=out_channels * 4, + kernel_size=1, + stride= + 1, #ResNet-D 2/2:add a 2×2 average pooling layer with a stride of 2 before the convolution, + # whose stride is changed to 1, works well in practice. + is_tweaks_mode=False if if_first else True, + name=name + "_branch1") + + self.shortcut = shortcut + self.num_seg = num_seg + + def forward(self, inputs): + shifts = paddle.fluid.layers.temporal_shift(inputs, self.num_seg, + 1.0 / self.num_seg) + y = self.conv0(shifts) + conv1 = self.conv1(y) + conv2 = self.conv2(conv1) + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv2) + return F.leaky_relu(y) + + +class BasicBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + name=None): + super(BasicBlock, self).__init__() + self.stride = stride + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + filter_size=3, + stride=stride, + act="leaky_relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + filter_size=3, + act=None, + name=name + "_branch2b") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + filter_size=1, + stride=stride, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(short, conv1) + y = F.leaky_relu(y) + return y + + +@BACKBONES.register() +class ResNetTSM_MRI(nn.Layer): + """ResNet TSM backbone. + + Args: + depth (int): Depth of resnet model. + pretrained (str): pretrained model. Default: None. + """ + def __init__(self, depth, num_seg=8, pretrained=None, in_channels=1): + super(ResNetTSM_MRI, self).__init__() + self.pretrained = pretrained + self.layers = depth + self.num_seg = num_seg + self.in_channels = in_channels + + supported_layers = [18, 34, 50, 101, 152] + assert self.layers in supported_layers, \ + "supported layers are {} but input layer is {}".format( + supported_layers, self.layers) + + if self.layers == 18: + depth = [2, 2, 2, 2] + elif self.layers == 34 or self.layers == 50: + depth = [3, 4, 6, 3] + elif self.layers == 101: + depth = [3, 4, 23, 3] + elif self.layers == 152: + depth = [3, 8, 36, 3] + + in_channels = 64 + out_channels = [64, 128, 256, 512] + + #ResNet-C: use three 3x3 conv, replace, one 7x7 conv + self.conv1_1 = ConvBNLayer(in_channels=self.in_channels, + out_channels=32, + kernel_size=3, + stride=2, + act='leaky_relu', + name="conv1_1") + self.conv1_2 = ConvBNLayer(in_channels=32, + out_channels=32, + kernel_size=3, + stride=1, + act='leaky_relu', + name="conv1_2") + self.conv1_3 = ConvBNLayer(in_channels=32, + out_channels=64, + kernel_size=3, + stride=1, + act='leaky_relu', + name="conv1_3") + self.pool2D_max = MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.block_list = [] + if self.layers >= 50: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + if self.layers in [101, 152] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + bottleneck_block = self.add_sublayer( + 'bb_%d_%d' % + (block, i), #same with PaddleClas, for loading pretrain + BottleneckBlock( + in_channels=in_channels + if i == 0 else out_channels[block] * 4, + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + num_seg=self.num_seg, + shortcut=shortcut, + if_first=block == i == 0, + name=conv_name)) + in_channels = out_channels[block] * 4 + self.block_list.append(bottleneck_block) + shortcut = True + else: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + conv_name = "res" + str(block + 2) + chr(97 + i) + basic_block = self.add_sublayer( + conv_name, + BasicBlock(in_channels=in_channels[block] + if i == 0 else out_channels[block], + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + name=conv_name)) + self.block_list.append(basic_block) + shortcut = True + + def init_weights(self): + """Initiate the parameters. + Note: + 1. when indicate pretrained loading path, will load it to initiate backbone. + 2. when not indicating pretrained loading path, will follow specific initialization initiate backbone. Always, Conv2D layer will be initiated by KaimingNormal function, and BatchNorm2d will be initiated by Constant function. + Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/kaiming/KaimingNormal_en.html + """ + #XXX: check bias!!! check pretrained!!! + + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + #XXX: no bias + weight_init_(layer, 'KaimingNormal') + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Constant', value=1) + + def forward(self, inputs): + """Define how the backbone is going to run. + + """ + #NOTE: Already merge axis 0(batches) and axis 1(channels) before extracting feature phase, + # please refer to paddlevideo/modeling/framework/recognizers/recognizer2d.py#L27 + #y = paddle.reshape( + # inputs, [-1, inputs.shape[2], inputs.shape[3], inputs.shape[4]]) + + ####ResNet-C: use three 3x3 conv, replace, one 7x7 conv + y = self.conv1_1(inputs) + y = self.conv1_2(y) + y = self.conv1_3(y) + + y = self.pool2D_max(y) + for block in self.block_list: + y = block(y) + return y diff --git a/paddlevideo/modeling/backbones/resnet_tsn_MRI.py b/paddlevideo/modeling/backbones/resnet_tsn_MRI.py new file mode 100644 index 0000000000000000000000000000000000000000..439a0eff84a36dafb46f68fd529b183e7b7760be --- /dev/null +++ b/paddlevideo/modeling/backbones/resnet_tsn_MRI.py @@ -0,0 +1,331 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import paddle +from paddle import ParamAttr +import paddle.nn as nn +import paddle.nn.functional as F +from paddle.regularizer import L2Decay +from paddle.nn import Conv2D, BatchNorm +from paddle.nn import MaxPool2D, AvgPool2D + +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from ...utils import load_ckpt + +__all__ = ["ResNetTSN_MRI"] + + +class ConvBNLayer(nn.Layer): + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + is_tweaks_mode=False, + act=None, + lr_mult=1.0, + name=None): + super(ConvBNLayer, self).__init__() + self.is_tweaks_mode = is_tweaks_mode + self._pool2d_avg = AvgPool2D(kernel_size=2, + stride=2, + padding=0, + ceil_mode=True) + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + weight_attr=ParamAttr(name=name + "_weights", + learning_rate=lr_mult), + bias_attr=False) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + self._batch_norm = BatchNorm( + out_channels, + act=act, + param_attr=ParamAttr(name=bn_name + '_scale', + learning_rate=lr_mult, + regularizer=L2Decay(0.0)), + bias_attr=ParamAttr(bn_name + '_offset', + learning_rate=lr_mult, + regularizer=L2Decay(0.0)), + moving_mean_name=bn_name + '_mean', + moving_variance_name=bn_name + '_variance') + + def forward(self, inputs): + if self.is_tweaks_mode: + inputs = self._pool2d_avg(inputs) + y = self._conv(inputs) + y = self._batch_norm(y) + return y + + +class BottleneckBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + if_first=False, + lr_mult=1.0, + name=None): + super(BottleneckBlock, self).__init__() + + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + act='relu', + lr_mult=lr_mult, + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act='relu', + lr_mult=lr_mult, + name=name + "_branch2b") + self.conv2 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels * 4, + kernel_size=1, + act=None, + lr_mult=lr_mult, + name=name + "_branch2c") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels * 4, + kernel_size=1, + stride=1, + is_tweaks_mode=False if if_first else True, + lr_mult=lr_mult, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + conv2 = self.conv2(conv1) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv2) + y = F.relu(y) + return y + + +class BasicBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + if_first=False, + lr_mult=1.0, + name=None): + super(BasicBlock, self).__init__() + self.stride = stride + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act='relu', + lr_mult=lr_mult, + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + act=None, + lr_mult=lr_mult, + name=name + "_branch2b") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + is_tweaks_mode=False if if_first else True, + lr_mult=lr_mult, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv1) + y = F.relu(y) + return y + + +@BACKBONES.register() +class ResNetTSN_MRI(nn.Layer): + """ResNetTweaksTSN backbone. + + Args: + depth (int): Depth of resnet model. + pretrained (str): pretrained model. Default: None. + """ + def __init__(self, + layers=50, + pretrained=None, + lr_mult_list=[1.0, 1.0, 1.0, 1.0, 1.0], + in_channels=1): + super(ResNetTSN_MRI, self).__init__() + + self.pretrained = pretrained + self.layers = layers + supported_layers = [18, 34, 50, 101, 152, 200] + assert layers in supported_layers, \ + "supported layers are {} but input layer is {}".format( + supported_layers, layers) + + self.lr_mult_list = lr_mult_list + self.in_channels = in_channels + assert isinstance( + self.lr_mult_list, + (list, tuple + )), "lr_mult_list should be in (list, tuple) but got {}".format( + type(self.lr_mult_list)) + assert len( + self.lr_mult_list + ) == 5, "lr_mult_list length should should be 5 but got {}".format( + len(self.lr_mult_list)) + + if layers == 18: + depth = [2, 2, 2, 2] + elif layers == 34 or layers == 50: + depth = [3, 4, 6, 3] + elif layers == 101: + depth = [3, 4, 23, 3] + elif layers == 152: + depth = [3, 8, 36, 3] + elif layers == 200: + depth = [3, 12, 48, 3] + num_channels = [64, 256, 512, 1024 + ] if layers >= 50 else [64, 64, 128, 256] + num_filters = [64, 128, 256, 512] + + self.conv1_1 = ConvBNLayer(in_channels=self.in_channels, + out_channels=32, + kernel_size=3, + stride=2, + act='relu', + lr_mult=self.lr_mult_list[0], + name="conv1_1") + self.conv1_2 = ConvBNLayer(in_channels=32, + out_channels=32, + kernel_size=3, + stride=1, + act='relu', + lr_mult=self.lr_mult_list[0], + name="conv1_2") + self.conv1_3 = ConvBNLayer(in_channels=32, + out_channels=64, + kernel_size=3, + stride=1, + act='relu', + lr_mult=self.lr_mult_list[0], + name="conv1_3") + self.pool2d_max = MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.block_list = [] + if layers >= 50: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + if layers in [101, 152, 200] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + bottleneck_block = self.add_sublayer( + 'bb_%d_%d' % (block, i), + BottleneckBlock( + in_channels=num_channels[block] + if i == 0 else num_filters[block] * 4, + out_channels=num_filters[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + if_first=block == i == 0, + lr_mult=self.lr_mult_list[block + 1], + name=conv_name)) + self.block_list.append(bottleneck_block) + shortcut = True + else: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + conv_name = "res" + str(block + 2) + chr(97 + i) + basic_block = self.add_sublayer( + 'bb_%d_%d' % (block, i), + BasicBlock(in_channels=num_channels[block] + if i == 0 else num_filters[block], + out_channels=num_filters[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + if_first=block == i == 0, + name=conv_name, + lr_mult=self.lr_mult_list[block + 1])) + self.block_list.append(basic_block) + shortcut = True + + def init_weights(self): + """Initiate the parameters. + Note: + 1. when indicate pretrained loading path, will load it to initiate backbone. + 2. when not indicating pretrained loading path, will follow specific initialization initiate backbone. Always, Conv2D layer will be + initiated by KaimingNormal function, and BatchNorm2d will be initiated by Constant function. + Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/kaiming/KaimingNormal_en.html + """ + # XXX: check bias!!! check pretrained!!! + + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + # XXX: no bias + weight_init_(layer, 'KaimingNormal') + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Constant', value=1) + + def forward(self, inputs): + + y = self.conv1_1(inputs) + y = self.conv1_2(y) + y = self.conv1_3(y) + y = self.pool2d_max(y) + for block in self.block_list: + y = block(y) + return y diff --git a/paddlevideo/modeling/backbones/resnet_tweaks_tsm.py b/paddlevideo/modeling/backbones/resnet_tweaks_tsm.py new file mode 100644 index 0000000000000000000000000000000000000000..f2ed947c57d6f2cb7ec616ccc31731e2ffba9c55 --- /dev/null +++ b/paddlevideo/modeling/backbones/resnet_tweaks_tsm.py @@ -0,0 +1,344 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import math + +import sys +import paddle +import paddle.nn as nn +from paddle.nn import (Conv2D, BatchNorm2D, Linear, Dropout, MaxPool2D, + AvgPool2D) +from paddle import ParamAttr +import paddle.nn.functional as F + +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from ...utils.save_load import load_ckpt +from paddle.regularizer import L2Decay + + +class ConvBNLayer(nn.Layer): + """Conv2D and BatchNorm2D layer. + + Args: + in_channels (int): Number of channels for the input. + out_channels (int): Number of channels for the output. + kernel_size (int): Kernel size. + stride (int): Stride in the Conv2D layer. Default: 1. + groups (int): Groups in the Conv2D, Default: 1. + is_tweaks_mode (bool): switch for tweaks. Default: False. + act (str): Indicate activation after BatchNorm2D layer. + name (str): the name of an instance of ConvBNLayer. + + Note: weight and bias initialization include initialize values and name the restored parameters, values initialization are explicit declared in the ```init_weights``` method. + + """ + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + is_tweaks_mode=False, + act=None, + name=None): + super(ConvBNLayer, self).__init__() + self.is_tweaks_mode = is_tweaks_mode + #ResNet-D 1/2:add a 2×2 average pooling layer with a stride of 2 before the convolution, + # whose stride is changed to 1, works well in practice. + self._pool2d_avg = AvgPool2D(kernel_size=2, + stride=2, + padding=0, + ceil_mode=True) + + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + weight_attr=ParamAttr(name=name + "_weights"), + bias_attr=False) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + + self._act = act + + self._batch_norm = BatchNorm2D( + out_channels, + weight_attr=ParamAttr(name=bn_name + "_scale", + regularizer=L2Decay(0.0)), + bias_attr=ParamAttr(bn_name + "_offset", regularizer=L2Decay(0.0))) + + def forward(self, inputs): + if self.is_tweaks_mode: + inputs = self._pool2d_avg(inputs) + y = self._conv(inputs) + y = self._batch_norm(y) + if self._act: + y = getattr(paddle.nn.functional, self._act)(y) + return y + + +class BottleneckBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + if_first=False, + num_seg=8, + name=None): + super(BottleneckBlock, self).__init__() + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + act="leaky_relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act="leaky_relu", + name=name + "_branch2b") + + self.conv2 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels * 4, + kernel_size=1, + act=None, + name=name + "_branch2c") + + if not shortcut: + self.short = ConvBNLayer( + in_channels=in_channels, + out_channels=out_channels * 4, + kernel_size=1, + stride= + 1, #ResNet-D 2/2:add a 2×2 average pooling layer with a stride of 2 before the convolution, + # whose stride is changed to 1, works well in practice. + is_tweaks_mode=False if if_first else True, + name=name + "_branch1") + + self.shortcut = shortcut + self.num_seg = num_seg + + def forward(self, inputs): + if paddle.fluid.core.is_compiled_with_npu(): + x = inputs + seg_num = self.num_seg + shift_ratio = 1.0 / self.num_seg + + shape = x.shape #[N*T, C, H, W] + reshape_x = x.reshape((-1, seg_num, shape[1], shape[2], shape[3])) #[N, T, C, H, W] + pad_x = paddle.fluid.layers.pad(reshape_x, [0,0,1,1,0,0,0,0,0,0,]) #[N, T+2, C, H, W] + c1 = int(shape[1] * shift_ratio) + c2 = int(shape[1] * 2 * shift_ratio) + slice1 = pad_x[:, :seg_num, :c1, :, :] + slice2 = pad_x[:, 2:seg_num+2, c1:c2, :, :] + slice3 = pad_x[:, 1:seg_num+1, c2:, :, :] + concat_x = paddle.concat([slice1, slice2, slice3], axis=2) #[N, T, C, H, W] + shifts = concat_x.reshape(shape) + else: + shifts = paddle.fluid.layers.temporal_shift(inputs, self.num_seg, + 1.0 / self.num_seg) + + y = self.conv0(shifts) + conv1 = self.conv1(y) + conv2 = self.conv2(conv1) + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv2) + return F.leaky_relu(y) + + +class BasicBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + name=None): + super(BasicBlock, self).__init__() + self.stride = stride + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + filter_size=3, + stride=stride, + act="leaky_relu", + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + filter_size=3, + act=None, + name=name + "_branch2b") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + filter_size=1, + stride=stride, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(short, conv1) + y = F.leaky_relu(y) + return y + + +@BACKBONES.register() +class ResNetTweaksTSM(nn.Layer): + """ResNet TSM backbone. + + Args: + depth (int): Depth of resnet model. + pretrained (str): pretrained model. Default: None. + """ + def __init__(self, depth, num_seg=8, pretrained=None): + super(ResNetTweaksTSM, self).__init__() + self.pretrained = pretrained + self.layers = depth + self.num_seg = num_seg + + supported_layers = [18, 34, 50, 101, 152] + assert self.layers in supported_layers, \ + "supported layers are {} but input layer is {}".format( + supported_layers, self.layers) + + if self.layers == 18: + depth = [2, 2, 2, 2] + elif self.layers == 34 or self.layers == 50: + depth = [3, 4, 6, 3] + elif self.layers == 101: + depth = [3, 4, 23, 3] + elif self.layers == 152: + depth = [3, 8, 36, 3] + + in_channels = 64 + out_channels = [64, 128, 256, 512] + + #ResNet-C: use three 3x3 conv, replace, one 7x7 conv + self.conv1_1 = ConvBNLayer(in_channels=3, + out_channels=32, + kernel_size=3, + stride=2, + act='leaky_relu', + name="conv1_1") + self.conv1_2 = ConvBNLayer(in_channels=32, + out_channels=32, + kernel_size=3, + stride=1, + act='leaky_relu', + name="conv1_2") + self.conv1_3 = ConvBNLayer(in_channels=32, + out_channels=64, + kernel_size=3, + stride=1, + act='leaky_relu', + name="conv1_3") + self.pool2D_max = MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.block_list = [] + if self.layers >= 50: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + if self.layers in [101, 152] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + bottleneck_block = self.add_sublayer( + 'bb_%d_%d' % + (block, i), #same with PaddleClas, for loading pretrain + BottleneckBlock( + in_channels=in_channels + if i == 0 else out_channels[block] * 4, + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + num_seg=self.num_seg, + shortcut=shortcut, + if_first=block == i == 0, + name=conv_name)) + in_channels = out_channels[block] * 4 + self.block_list.append(bottleneck_block) + shortcut = True + else: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + conv_name = "res" + str(block + 2) + chr(97 + i) + basic_block = self.add_sublayer( + conv_name, + BasicBlock(in_channels=in_channels[block] + if i == 0 else out_channels[block], + out_channels=out_channels[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + name=conv_name)) + self.block_list.append(basic_block) + shortcut = True + + def init_weights(self): + """Initiate the parameters. + Note: + 1. when indicate pretrained loading path, will load it to initiate backbone. + 2. when not indicating pretrained loading path, will follow specific initialization initiate backbone. Always, Conv2D layer will be initiated by KaimingNormal function, and BatchNorm2d will be initiated by Constant function. + Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/kaiming/KaimingNormal_en.html + """ + #XXX: check bias!!! check pretrained!!! + + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + #XXX: no bias + weight_init_(layer, 'KaimingNormal') + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Constant', value=1) + + def forward(self, inputs): + """Define how the backbone is going to run. + + """ + #NOTE: Already merge axis 0(batches) and axis 1(channels) before extracting feature phase, + # please refer to paddlevideo/modeling/framework/recognizers/recognizer2d.py#L27 + #y = paddle.reshape( + # inputs, [-1, inputs.shape[2], inputs.shape[3], inputs.shape[4]]) + + ####ResNet-C: use three 3x3 conv, replace, one 7x7 conv + y = self.conv1_1(inputs) + y = self.conv1_2(y) + y = self.conv1_3(y) + + y = self.pool2D_max(y) + for block in self.block_list: + y = block(y) + return y diff --git a/paddlevideo/modeling/backbones/resnet_tweaks_tsn.py b/paddlevideo/modeling/backbones/resnet_tweaks_tsn.py new file mode 100644 index 0000000000000000000000000000000000000000..36b33073f76506a310a250b80cfb49df07b4e613 --- /dev/null +++ b/paddlevideo/modeling/backbones/resnet_tweaks_tsn.py @@ -0,0 +1,328 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import paddle +from paddle import ParamAttr +import paddle.nn as nn +import paddle.nn.functional as F +from paddle.regularizer import L2Decay +from paddle.nn import Conv2D, BatchNorm +from paddle.nn import MaxPool2D, AvgPool2D + +from ..registry import BACKBONES +from ..weight_init import weight_init_ +from ...utils import load_ckpt + +__all__ = ["ResNetTweaksTSN"] + + +class ConvBNLayer(nn.Layer): + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + groups=1, + is_tweaks_mode=False, + act=None, + lr_mult=1.0, + name=None): + super(ConvBNLayer, self).__init__() + self.is_tweaks_mode = is_tweaks_mode + self._pool2d_avg = AvgPool2D(kernel_size=2, + stride=2, + padding=0, + ceil_mode=True) + self._conv = Conv2D(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=(kernel_size - 1) // 2, + groups=groups, + weight_attr=ParamAttr(name=name + "_weights", + learning_rate=lr_mult), + bias_attr=False) + if name == "conv1": + bn_name = "bn_" + name + else: + bn_name = "bn" + name[3:] + self._batch_norm = BatchNorm( + out_channels, + act=act, + param_attr=ParamAttr(name=bn_name + '_scale', + learning_rate=lr_mult, + regularizer=L2Decay(0.0)), + bias_attr=ParamAttr(bn_name + '_offset', + learning_rate=lr_mult, + regularizer=L2Decay(0.0)), + moving_mean_name=bn_name + '_mean', + moving_variance_name=bn_name + '_variance') + + def forward(self, inputs): + if self.is_tweaks_mode: + inputs = self._pool2d_avg(inputs) + y = self._conv(inputs) + y = self._batch_norm(y) + return y + + +class BottleneckBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + if_first=False, + lr_mult=1.0, + name=None): + super(BottleneckBlock, self).__init__() + + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + act='relu', + lr_mult=lr_mult, + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act='relu', + lr_mult=lr_mult, + name=name + "_branch2b") + self.conv2 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels * 4, + kernel_size=1, + act=None, + lr_mult=lr_mult, + name=name + "_branch2c") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels * 4, + kernel_size=1, + stride=1, + is_tweaks_mode=False if if_first else True, + lr_mult=lr_mult, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + conv2 = self.conv2(conv1) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv2) + y = F.relu(y) + return y + + +class BasicBlock(nn.Layer): + def __init__(self, + in_channels, + out_channels, + stride, + shortcut=True, + if_first=False, + lr_mult=1.0, + name=None): + super(BasicBlock, self).__init__() + self.stride = stride + self.conv0 = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + act='relu', + lr_mult=lr_mult, + name=name + "_branch2a") + self.conv1 = ConvBNLayer(in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + act=None, + lr_mult=lr_mult, + name=name + "_branch2b") + + if not shortcut: + self.short = ConvBNLayer(in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + is_tweaks_mode=False if if_first else True, + lr_mult=lr_mult, + name=name + "_branch1") + + self.shortcut = shortcut + + def forward(self, inputs): + y = self.conv0(inputs) + conv1 = self.conv1(y) + + if self.shortcut: + short = inputs + else: + short = self.short(inputs) + y = paddle.add(x=short, y=conv1) + y = F.relu(y) + return y + + +@BACKBONES.register() +class ResNetTweaksTSN(nn.Layer): + """ResNetTweaksTSN backbone. + + Args: + depth (int): Depth of resnet model. + pretrained (str): pretrained model. Default: None. + """ + def __init__(self, + layers=50, + pretrained=None, + lr_mult_list=[1.0, 1.0, 1.0, 1.0, 1.0]): + super(ResNetTweaksTSN, self).__init__() + + self.pretrained = pretrained + self.layers = layers + supported_layers = [18, 34, 50, 101, 152, 200] + assert layers in supported_layers, \ + "supported layers are {} but input layer is {}".format( + supported_layers, layers) + + self.lr_mult_list = lr_mult_list + assert isinstance( + self.lr_mult_list, + (list, tuple + )), "lr_mult_list should be in (list, tuple) but got {}".format( + type(self.lr_mult_list)) + assert len( + self.lr_mult_list + ) == 5, "lr_mult_list length should should be 5 but got {}".format( + len(self.lr_mult_list)) + + if layers == 18: + depth = [2, 2, 2, 2] + elif layers == 34 or layers == 50: + depth = [3, 4, 6, 3] + elif layers == 101: + depth = [3, 4, 23, 3] + elif layers == 152: + depth = [3, 8, 36, 3] + elif layers == 200: + depth = [3, 12, 48, 3] + num_channels = [64, 256, 512, 1024 + ] if layers >= 50 else [64, 64, 128, 256] + num_filters = [64, 128, 256, 512] + + self.conv1_1 = ConvBNLayer(in_channels=3, + out_channels=32, + kernel_size=3, + stride=2, + act='relu', + lr_mult=self.lr_mult_list[0], + name="conv1_1") + self.conv1_2 = ConvBNLayer(in_channels=32, + out_channels=32, + kernel_size=3, + stride=1, + act='relu', + lr_mult=self.lr_mult_list[0], + name="conv1_2") + self.conv1_3 = ConvBNLayer(in_channels=32, + out_channels=64, + kernel_size=3, + stride=1, + act='relu', + lr_mult=self.lr_mult_list[0], + name="conv1_3") + self.pool2d_max = MaxPool2D(kernel_size=3, stride=2, padding=1) + + self.block_list = [] + if layers >= 50: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + if layers in [101, 152, 200] and block == 2: + if i == 0: + conv_name = "res" + str(block + 2) + "a" + else: + conv_name = "res" + str(block + 2) + "b" + str(i) + else: + conv_name = "res" + str(block + 2) + chr(97 + i) + bottleneck_block = self.add_sublayer( + 'bb_%d_%d' % (block, i), + BottleneckBlock( + in_channels=num_channels[block] + if i == 0 else num_filters[block] * 4, + out_channels=num_filters[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + if_first=block == i == 0, + lr_mult=self.lr_mult_list[block + 1], + name=conv_name)) + self.block_list.append(bottleneck_block) + shortcut = True + else: + for block in range(len(depth)): + shortcut = False + for i in range(depth[block]): + conv_name = "res" + str(block + 2) + chr(97 + i) + basic_block = self.add_sublayer( + 'bb_%d_%d' % (block, i), + BasicBlock(in_channels=num_channels[block] + if i == 0 else num_filters[block], + out_channels=num_filters[block], + stride=2 if i == 0 and block != 0 else 1, + shortcut=shortcut, + if_first=block == i == 0, + name=conv_name, + lr_mult=self.lr_mult_list[block + 1])) + self.block_list.append(basic_block) + shortcut = True + + def init_weights(self): + """Initiate the parameters. + Note: + 1. when indicate pretrained loading path, will load it to initiate backbone. + 2. when not indicating pretrained loading path, will follow specific initialization initiate backbone. Always, Conv2D layer will be + initiated by KaimingNormal function, and BatchNorm2d will be initiated by Constant function. + Please refer to https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/nn/initializer/kaiming/KaimingNormal_en.html + """ + # XXX: check bias!!! check pretrained!!! + + if isinstance(self.pretrained, str) and self.pretrained.strip() != "": + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + # XXX: no bias + weight_init_(layer, 'KaimingNormal') + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Constant', value=1) + + def forward(self, inputs): + y = self.conv1_1(inputs) + y = self.conv1_2(y) + y = self.conv1_3(y) + y = self.pool2d_max(y) + for block in self.block_list: + y = block(y) + return y diff --git a/paddlevideo/modeling/backbones/stgcn.py b/paddlevideo/modeling/backbones/stgcn.py new file mode 100644 index 0000000000000000000000000000000000000000..40d9d0ddaca73cbcf618eaef5778e88f257f1227 --- /dev/null +++ b/paddlevideo/modeling/backbones/stgcn.py @@ -0,0 +1,343 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np +from ..registry import BACKBONES +from ..weight_init import weight_init_ + + +def zero(x): + return 0 + + +def iden(x): + return x + + +def einsum(x, A): + """paddle.einsum will be implemented in release/2.2. + """ + x = x.transpose((0, 2, 3, 1, 4)) + n, c, t, k, v = x.shape + k2, v2, w = A.shape + assert (k == k2 and v == v2), "Args of einsum not match!" + x = x.reshape((n, c, t, k * v)) + A = A.reshape((k * v, w)) + y = paddle.matmul(x, A) + return y + + +def get_hop_distance(num_node, edge, max_hop=1): + A = np.zeros((num_node, num_node)) + for i, j in edge: + A[j, i] = 1 + A[i, j] = 1 + + # compute hop steps + hop_dis = np.zeros((num_node, num_node)) + np.inf + transfer_mat = [np.linalg.matrix_power(A, d) for d in range(max_hop + 1)] + arrive_mat = (np.stack(transfer_mat) > 0) + for d in range(max_hop, -1, -1): + hop_dis[arrive_mat[d]] = d + return hop_dis + + +def normalize_digraph(A): + Dl = np.sum(A, 0) + num_node = A.shape[0] + Dn = np.zeros((num_node, num_node)) + for i in range(num_node): + if Dl[i] > 0: + Dn[i, i] = Dl[i]**(-1) + AD = np.dot(A, Dn) + return AD + + +class Graph(): + + def __init__(self, + layout='openpose', + strategy='uniform', + max_hop=1, + dilation=1): + self.max_hop = max_hop + self.dilation = dilation + + self.get_edge(layout) + self.hop_dis = get_hop_distance(self.num_node, + self.edge, + max_hop=max_hop) + self.get_adjacency(strategy) + + def __str__(self): + return self.A + + def get_edge(self, layout): + # edge is a list of [child, parent] paris + + if layout == 'fsd10': + self.num_node = 25 + self_link = [(i, i) for i in range(self.num_node)] + neighbor_link = [(1, 8), (0, 1), (15, 0), (17, 15), (16, 0), + (18, 16), (5, 1), (6, 5), (7, 6), (2, 1), (3, 2), + (4, 3), (9, 8), (10, 9), (11, 10), (24, 11), + (22, 11), (23, 22), (12, 8), (13, 12), (14, 13), + (21, 14), (19, 14), (20, 19)] + self.edge = self_link + neighbor_link + self.center = 8 + elif layout == 'ntu-rgb+d': + self.num_node = 25 + self_link = [(i, i) for i in range(self.num_node)] + neighbor_1base = [(1, 2), (2, 21), (3, 21), (4, 3), (5, 21), (6, 5), + (7, 6), (8, 7), (9, 21), (10, 9), (11, 10), + (12, 11), (13, 1), (14, 13), (15, 14), (16, 15), + (17, 1), (18, 17), (19, 18), (20, 19), (22, 23), + (23, 8), (24, 25), (25, 12)] + neighbor_link = [(i - 1, j - 1) for (i, j) in neighbor_1base] + self.edge = self_link + neighbor_link + self.center = 21 - 1 + elif layout == 'coco_keypoint': + self.num_node = 17 + self_link = [(i, i) for i in range(self.num_node)] + neighbor_1base = [(0, 1), (0, 2), (1, 3), (2, 4), (3, 5), (4, 6), + (5, 7), (6, 8), (7, 9), (8, 10), (5, 11), (6, 12), + (11, 13), (12, 14), (13, 15), (14, 16), (11, 12)] + neighbor_link = [(i, j) for (i, j) in neighbor_1base] + self.edge = self_link + neighbor_link + self.center = 11 + else: + raise ValueError("Do Not Exist This Layout.") + + def get_adjacency(self, strategy): + valid_hop = range(0, self.max_hop + 1, self.dilation) + adjacency = np.zeros((self.num_node, self.num_node)) + for hop in valid_hop: + adjacency[self.hop_dis == hop] = 1 + normalize_adjacency = normalize_digraph(adjacency) + + if strategy == 'spatial': + A = [] + for hop in valid_hop: + a_root = np.zeros((self.num_node, self.num_node)) + a_close = np.zeros((self.num_node, self.num_node)) + a_further = np.zeros((self.num_node, self.num_node)) + for i in range(self.num_node): + for j in range(self.num_node): + if self.hop_dis[j, i] == hop: + if self.hop_dis[j, self.center] == self.hop_dis[ + i, self.center]: + a_root[j, i] = normalize_adjacency[j, i] + elif self.hop_dis[j, self.center] > self.hop_dis[ + i, self.center]: + a_close[j, i] = normalize_adjacency[j, i] + else: + a_further[j, i] = normalize_adjacency[j, i] + if hop == 0: + A.append(a_root) + else: + A.append(a_root + a_close) + A.append(a_further) + A = np.stack(A) + self.A = A + else: + raise ValueError("Do Not Exist This Strategy") + + +class ConvTemporalGraphical(nn.Layer): + + def __init__(self, + in_channels, + out_channels, + kernel_size, + t_kernel_size=1, + t_stride=1, + t_padding=0, + t_dilation=1): + super().__init__() + + self.kernel_size = kernel_size + self.conv = nn.Conv2D(in_channels, + out_channels * kernel_size, + kernel_size=(t_kernel_size, 1), + padding=(t_padding, 0), + stride=(t_stride, 1), + dilation=(t_dilation, 1)) + + def forward(self, x, A): + assert A.shape[0] == self.kernel_size + + x = self.conv(x) + n, kc, t, v = x.shape + x = x.reshape((n, self.kernel_size, kc // self.kernel_size, t, v)) + x = einsum(x, A) + + return x, A + + +class st_gcn_block(nn.Layer): + + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + dropout=0, + residual=True): + super(st_gcn_block, self).__init__() + + assert len(kernel_size) == 2 + assert kernel_size[0] % 2 == 1 + padding = ((kernel_size[0] - 1) // 2, 0) + + self.gcn = ConvTemporalGraphical(in_channels, out_channels, + kernel_size[1]) + + self.tcn = nn.Sequential( + nn.BatchNorm2D(out_channels), + nn.ReLU(), + nn.Conv2D( + out_channels, + out_channels, + (kernel_size[0], 1), + (stride, 1), + padding, + ), + nn.BatchNorm2D(out_channels), + nn.Dropout(dropout), + ) + + if not residual: + self.residual = zero + + elif (in_channels == out_channels) and (stride == 1): + self.residual = iden + + else: + self.residual = nn.Sequential( + nn.Conv2D(in_channels, + out_channels, + kernel_size=1, + stride=(stride, 1)), + nn.BatchNorm2D(out_channels), + ) + + self.relu = nn.ReLU() + + def forward(self, x, A): + res = self.residual(x) + x, A = self.gcn(x, A) + x = self.tcn(x) + res + return self.relu(x), A + + +@BACKBONES.register() +class STGCN(nn.Layer): + """ + ST-GCN model from: + `"Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition" `_ + Args: + in_channels: int, channels of vertex coordinate. 2 for (x,y), 3 for (x,y,z). Default 2. + edge_importance_weighting: bool, whether to use edge attention. Default True. + data_bn: bool, whether to use data BatchNorm. Default True. + """ + + def __init__(self, + in_channels=2, + edge_importance_weighting=True, + data_bn=True, + layout='fsd10', + strategy='spatial', + **kwargs): + super(STGCN, self).__init__() + self.data_bn = data_bn + # load graph + self.graph = Graph( + layout=layout, + strategy=strategy, + ) + A = paddle.to_tensor(self.graph.A, dtype='float32') + self.register_buffer('A', A) + + # build networks + spatial_kernel_size = A.shape[0] + temporal_kernel_size = 9 + kernel_size = (temporal_kernel_size, spatial_kernel_size) + self.data_bn = nn.BatchNorm1D(in_channels * + A.shape[1]) if self.data_bn else iden + kwargs0 = {k: v for k, v in kwargs.items() if k != 'dropout'} + self.st_gcn_networks = nn.LayerList(( + st_gcn_block(in_channels, + 64, + kernel_size, + 1, + residual=False, + **kwargs0), + st_gcn_block(64, 64, kernel_size, 1, **kwargs), + st_gcn_block(64, 64, kernel_size, 1, **kwargs), + st_gcn_block(64, 64, kernel_size, 1, **kwargs), + st_gcn_block(64, 128, kernel_size, 2, **kwargs), + st_gcn_block(128, 128, kernel_size, 1, **kwargs), + st_gcn_block(128, 128, kernel_size, 1, **kwargs), + st_gcn_block(128, 256, kernel_size, 2, **kwargs), + st_gcn_block(256, 256, kernel_size, 1, **kwargs), + st_gcn_block(256, 256, kernel_size, 1, **kwargs), + )) + + # initialize parameters for edge importance weighting + if edge_importance_weighting: + self.edge_importance = nn.ParameterList([ + self.create_parameter( + shape=self.A.shape, + default_initializer=nn.initializer.Constant(1)) + for i in self.st_gcn_networks + ]) + else: + self.edge_importance = [1] * len(self.st_gcn_networks) + + self.pool = nn.AdaptiveAvgPool2D(output_size=(1, 1)) + + def init_weights(self): + """Initiate the parameters. + """ + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + weight_init_(layer, 'Normal', mean=0.0, std=0.02) + elif isinstance(layer, nn.BatchNorm2D): + weight_init_(layer, 'Normal', mean=1.0, std=0.02) + elif isinstance(layer, nn.BatchNorm1D): + weight_init_(layer, 'Normal', mean=1.0, std=0.02) + + def forward(self, x): + # data normalization + N, C, T, V, M = x.shape + x = x.transpose((0, 4, 3, 1, 2)) # N, M, V, C, T + x = x.reshape((N * M, V * C, T)) + if self.data_bn: + x.stop_gradient = False + x = self.data_bn(x) + x = x.reshape((N, M, V, C, T)) + x = x.transpose((0, 1, 3, 4, 2)) # N, M, C, T, V + x = x.reshape((N * M, C, T, V)) + + # forward + for gcn, importance in zip(self.st_gcn_networks, self.edge_importance): + x, _ = gcn(x, paddle.multiply(self.A, importance)) + + x = self.pool(x) # NM,C,T,V --> NM,C,1,1 + C = x.shape[1] + x = paddle.reshape(x, (N, M, C, 1, 1)).mean(axis=1) # N,C,1,1 + return x diff --git a/paddlevideo/modeling/backbones/swin_transformer.py b/paddlevideo/modeling/backbones/swin_transformer.py new file mode 100644 index 0000000000000000000000000000000000000000..aaed21790919bc45a25217c3df377c81cefdb89b --- /dev/null +++ b/paddlevideo/modeling/backbones/swin_transformer.py @@ -0,0 +1,742 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from functools import lru_cache, reduce +from operator import mul + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from paddle.nn.initializer import Constant + +from ...utils import load_ckpt +from ..registry import BACKBONES +from ..weight_init import trunc_normal_ + +zeros_ = Constant(value=0.) +ones_ = Constant(value=1.) + + +def drop_path(x, drop_prob=0., training=False): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper... + # issuecomment-532968956 ... + See discussion: https://github.com/tensorflow/tpu/issues/494 + """ + if drop_prob == 0. or not training: + return x + keep_prob = paddle.to_tensor(1 - drop_prob) + shape = (paddle.shape(x)[0], ) + (1, ) * (x.ndim - 1) + random_tensor = keep_prob + paddle.rand(shape, dtype=x.dtype) + random_tensor = paddle.floor(random_tensor) # binarize + output = x.divide(keep_prob) * random_tensor + + return output + + +class DropPath(nn.Layer): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + """ + def __init__(self, drop_prob=None): + super(DropPath, self).__init__() + self.drop_prob = drop_prob + + def forward(self, x): + return drop_path(x, self.drop_prob, self.training) + + +class Mlp(nn.Layer): + """ Multilayer perceptron.""" + def __init__(self, + in_features, + hidden_features=None, + out_features=None, + act_layer=nn.GELU, + drop=0.): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features) + self.drop = nn.Dropout(drop) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x + + +def window_partition(x, window_size): + """window_partition + Args: + x (Tensor): x.shape = [B, D, H, W, C] + window_size (tuple[int]): window_size + + Returns: + Tensor: (B*num_windows, window_size*window_size, C) + """ + B, D, H, W, C = x.shape + x = x.reshape([ + B, D // window_size[0], window_size[0], H // window_size[1], + window_size[1], W // window_size[2], window_size[2], C + ]) + windows = x.transpose([0, 1, 3, 5, 2, 4, 6, + 7]).reshape([-1, reduce(mul, window_size), C]) + return windows + + +class Identity(nn.Layer): + def __init__(self): + super(Identity, self).__init__() + + def forward(self, input): + return input + + +def window_reverse(windows, window_size, B, D, H, W): + """ + Args: + windows: (B*num_windows, window_size, window_size, C) + window_size (tuple[int]): Window size + H (int): Height of image + W (int): Width of image + + Returns: + x: (B, D, H, W, C) + """ + x = windows.reshape([ + B, D // window_size[0], H // window_size[1], W // window_size[2], + window_size[0], window_size[1], window_size[2], -1 + ]) + x = x.transpose([0, 1, 4, 2, 5, 3, 6, 7]).reshape([B, D, H, W, -1]) + return x + + +def get_window_size(x_size, window_size, shift_size=None): + use_window_size = list(window_size) + if shift_size is not None: + use_shift_size = list(shift_size) + for i in range(len(x_size)): + if x_size[i] <= window_size[i]: + use_window_size[i] = x_size[i] + if shift_size is not None: + use_shift_size[i] = 0 + + if shift_size is None: + return tuple(use_window_size) + else: + return tuple(use_window_size), tuple(use_shift_size) + + +class WindowAttention3D(nn.Layer): + """ Window based multi-head self attention (W-MSA) module with relative position bias. + It supports both of shifted and non-shifted window. + Args: + dim (int): Number of input channels. + window_size (tuple[int]): The temporal length, height and width of the window. + num_heads (int): Number of attention heads. + qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True + qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set + attn_drop (float, optional): Dropout ratio of attention weight. Default: 0.0 + proj_drop (float, optional): Dropout ratio of output. Default: 0.0 + """ + def __init__(self, + dim, + window_size, + num_heads, + qkv_bias=False, + qk_scale=None, + attn_drop=0., + proj_drop=0.): + + super().__init__() + self.dim = dim + self.window_size = window_size # Wd, Wh, Ww + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim**-0.5 + + # define a parameter table of relative position bias + self.relative_position_bias_table = self.create_parameter( + shape=((2 * window_size[0] - 1) * (2 * window_size[1] - 1) * + (2 * window_size[2] - 1), num_heads), + default_initializer=zeros_, + ) # 2*Wd-1 * 2*Wh-1 * 2*Ww-1, nH + self.add_parameter("relative_position_bias_table", + self.relative_position_bias_table) + # get pair-wise relative position index for each token inside the window + coords_d = paddle.arange(self.window_size[0]) + coords_h = paddle.arange(self.window_size[1]) + coords_w = paddle.arange(self.window_size[2]) + coords = paddle.stack(paddle.meshgrid(coords_d, coords_h, + coords_w)) # 3, Wd, Wh, Ww + coords_flatten = paddle.flatten(coords, 1) # 3, Wd*Wh*Ww + + relative_coords = coords_flatten.unsqueeze( + axis=2) - coords_flatten.unsqueeze(axis=1) # 3, Wd*Wh*Ww, Wd*Wh*Ww + + # relative_coords = coords_flatten.unsqueeze(2) - coords_flatten.unsqueeze(1) # 3, Wd*Wh*Ww, Wd*Wh*Ww + relative_coords = relative_coords.transpose([1, 2, 0 + ]) # Wd*Wh*Ww, Wd*Wh*Ww, 3 + relative_coords[:, :, + 0] += self.window_size[0] - 1 # shift to start from 0 + relative_coords[:, :, 1] += self.window_size[1] - 1 + relative_coords[:, :, 2] += self.window_size[2] - 1 + + relative_coords[:, :, 0] *= (2 * self.window_size[1] - + 1) * (2 * self.window_size[2] - 1) + relative_coords[:, :, 1] *= (2 * self.window_size[2] - 1) + relative_position_index = relative_coords.sum( + axis=-1) # Wd*Wh*Ww, Wd*Wh*Ww + self.register_buffer("relative_position_index", relative_position_index) + + self.qkv = nn.Linear(dim, dim * 3, bias_attr=qkv_bias) + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(dim, dim) + self.proj_drop = nn.Dropout(proj_drop) + + trunc_normal_(self.relative_position_bias_table, std=0.02) + self.softmax = nn.Softmax(axis=-1) + + def forward(self, x, mask=None): + """ Forward function. + Args: + x: input features with shape of (num_windows*B, N, C) + mask: (0/-inf) mask with shape of (num_windows, N, N) or None + """ + B_, N, C = x.shape + qkv = self.qkv(x).reshape( + [B_, N, 3, self.num_heads, + C // self.num_heads]).transpose([2, 0, 3, 1, 4]) + q, k, v = qkv[0], qkv[1], qkv[2] # B_, nH, N, C + + q = q * self.scale + attn = q @ k.transpose([0, 1, 3, 2]) + + relative_position_bias = self.relative_position_bias_table[ + self.relative_position_index[:N, :N].reshape([-1])].reshape( + [N, N, -1]) # Wd*Wh*Ww,Wd*Wh*Ww,nH + relative_position_bias = relative_position_bias.transpose( + [2, 0, 1]) # nH, Wd*Wh*Ww, Wd*Wh*Ww + attn = attn + relative_position_bias.unsqueeze(0) # B_, nH, N, N + + if mask is not None: + nW = mask.shape[0] + attn = attn.reshape([B_ // nW, nW, self.num_heads, N, N + ]) + mask.unsqueeze(1).unsqueeze(0) + attn = attn.reshape([-1, self.num_heads, N, N]) + attn = self.softmax(attn) + else: + attn = self.softmax(attn) + + attn = self.attn_drop(attn) + + x = (attn @ v).transpose([0, 2, 1, 3]).reshape([B_, N, C]) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class SwinTransformerBlock3D(nn.Layer): + """ Swin Transformer Block. + + Args: + dim (int): Number of input channels. + num_heads (int): Number of attention heads. + window_size (tuple[int]): Window size. + shift_size (tuple[int]): Shift size for SW-MSA. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True + qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set. + drop (float, optional): Dropout rate. Default: 0.0 + attn_drop (float, optional): Attention dropout rate. Default: 0.0 + drop_path (float, optional): Stochastic depth rate. Default: 0.0 + act_layer (nn.Layer, optional): Activation layer. Default: nn.GELU + norm_layer (nn.Layer, optional): Normalization layer. Default: nn.LayerNorm + """ + def __init__(self, + dim, + num_heads, + window_size=(2, 7, 7), + shift_size=(0, 0, 0), + mlp_ratio=4., + qkv_bias=True, + qk_scale=None, + drop=0., + attn_drop=0., + drop_path=0., + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + use_checkpoint=False): + super().__init__() + self.dim = dim + self.num_heads = num_heads + self.window_size = window_size + self.shift_size = shift_size + self.mlp_ratio = mlp_ratio + # self.use_checkpoint=use_checkpoint + + assert 0 <= self.shift_size[0] < self.window_size[ + 0], "shift_size must in 0-window_size" + assert 0 <= self.shift_size[1] < self.window_size[ + 1], "shift_size must in 0-window_size" + assert 0 <= self.shift_size[2] < self.window_size[ + 2], "shift_size must in 0-window_size" + + self.norm1 = norm_layer(dim) + self.attn = WindowAttention3D(dim, + window_size=self.window_size, + num_heads=num_heads, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + attn_drop=attn_drop, + proj_drop=drop) + + self.drop_path = DropPath(drop_path) if drop_path > 0. else Identity() + self.norm2 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp(in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop) + + def forward_part1(self, x, mask_matrix): + B = paddle.shape(x)[0] + _, D, H, W, C = x.shape + window_size, shift_size = get_window_size((D, H, W), self.window_size, + self.shift_size) + + x = self.norm1(x) + # pad feature maps to multiples of window size + pad_l = pad_t = pad_d0 = 0 + pad_d1 = (window_size[0] - D % window_size[0]) % window_size[0] + pad_b = (window_size[1] - H % window_size[1]) % window_size[1] + pad_r = (window_size[2] - W % window_size[2]) % window_size[2] + x = F.pad(x, (pad_l, pad_r, pad_t, pad_b, pad_d0, pad_d1), + data_format='NDHWC') + _, Dp, Hp, Wp, _ = x.shape + # cyclic shift + if any(i > 0 for i in shift_size): + shifted_x = paddle.roll(x, + shifts=(-shift_size[0], -shift_size[1], + -shift_size[2]), + axis=(1, 2, 3)) + attn_mask = mask_matrix + else: + shifted_x = x + attn_mask = None + # partition windows + x_windows = window_partition(shifted_x, + window_size) # B*nW, Wd*Wh*Ww, C + # W-MSA/SW-MSA + attn_windows = self.attn(x_windows, mask=attn_mask) # B*nW, Wd*Wh*Ww, C + # merge windows + attn_windows = attn_windows.reshape([-1, *(window_size + (C, ))]) + shifted_x = window_reverse(attn_windows, window_size, B, Dp, Hp, + Wp) # B D' H' W' C + # reverse cyclic shift + if any(i > 0 for i in shift_size): + x = paddle.roll(shifted_x, + shifts=(shift_size[0], shift_size[1], + shift_size[2]), + axis=(1, 2, 3)) + else: + x = shifted_x + + if pad_d1 > 0 or pad_r > 0 or pad_b > 0: + x = x[:, :D, :H, :W, :] + return x + + def forward_part2(self, x): + return self.drop_path(self.mlp(self.norm2(x))) + + def forward(self, x, mask_matrix): + """ Forward function. + + Args: + x: Input feature, tensor size (B, D, H, W, C). + mask_matrix: Attention mask for cyclic shift. + """ + + shortcut = x + x = self.forward_part1(x, mask_matrix) + x = shortcut + self.drop_path(x) + x = x + self.forward_part2(x) + + return x + + +class PatchMerging(nn.Layer): + """ Patch Merging Layer + + Args: + dim (int): Number of input channels. + norm_layer (nn.Layer, optional): Normalization layer. Default: nn.LayerNorm + """ + def __init__(self, dim, norm_layer=nn.LayerNorm): + super().__init__() + self.dim = dim + self.reduction = nn.Linear(4 * dim, 2 * dim, bias_attr=False) + self.norm = norm_layer(4 * dim) + + def forward(self, x): + """ Forward function. + + Args: + x: Input feature, tensor size (B, D, H, W, C). + """ + B, D, H, W, C = x.shape + + # padding + pad_input = (H % 2 == 1) or (W % 2 == 1) + if pad_input: + x = F.pad(x, (0, W % 2, 0, H % 2, 0, 0), data_format='NDHWC') + + x0 = x[:, :, 0::2, 0::2, :] # B D H/2 W/2 C + x1 = x[:, :, 1::2, 0::2, :] # B D H/2 W/2 C + x2 = x[:, :, 0::2, 1::2, :] # B D H/2 W/2 C + x3 = x[:, :, 1::2, 1::2, :] # B D H/2 W/2 C + x = paddle.concat([x0, x1, x2, x3], -1) # B D H/2 W/2 4*C + + x = self.norm(x) + x = self.reduction(x) + + return x + + +# cache each stage results +@lru_cache() +def compute_mask(D, H, W, window_size, shift_size): + img_mask = paddle.zeros((1, D, H, W, 1)) # 1 Dp Hp Wp 1 + cnt = 0 + for d in slice(-window_size[0]), slice(-window_size[0], + -shift_size[0]), slice( + -shift_size[0], None): + for h in slice(-window_size[1]), slice(-window_size[1], + -shift_size[1]), slice( + -shift_size[1], None): + for w in slice(-window_size[2]), slice(-window_size[2], + -shift_size[2]), slice( + -shift_size[2], None): + img_mask[:, d, h, w, :] = cnt + cnt += 1 + mask_windows = window_partition(img_mask, + window_size) # nW, ws[0]*ws[1]*ws[2], 1 + mask_windows = mask_windows.squeeze(-1) # nW, ws[0]*ws[1]*ws[2] + attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) + # attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0)) + huns = -100.0 * paddle.ones_like(attn_mask) + attn_mask = huns * (attn_mask != 0).astype("float32") + return attn_mask + + +class BasicLayer(nn.Layer): + """ A basic Swin Transformer layer for one stage. + + Args: + dim (int): Number of feature channels + depth (int): Depths of this stage. + num_heads (int): Number of attention head. + window_size (tuple[int]): Local window size. Default: (1,7,7). + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4. + qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True + qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set. + drop (float, optional): Dropout rate. Default: 0.0 + attn_drop (float, optional): Attention dropout rate. Default: 0.0 + drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0 + norm_layer (nn.Layer, optional): Normalization layer. Default: nn.LayerNorm + downsample (nn.Layer | None, optional): Downsample layer at the end of the layer. Default: None + """ + def __init__(self, + dim, + depth, + num_heads, + window_size=(1, 7, 7), + mlp_ratio=4., + qkv_bias=False, + qk_scale=None, + drop=0., + attn_drop=0., + drop_path=0., + norm_layer=nn.LayerNorm, + downsample=None, + use_checkpoint=False): + super().__init__() + self.window_size = window_size + self.shift_size = tuple(i // 2 for i in window_size) + self.depth = depth + self.use_checkpoint = use_checkpoint + + # build blocks + self.blocks = nn.LayerList([ + SwinTransformerBlock3D( + dim=dim, + num_heads=num_heads, + window_size=window_size, + shift_size=(0, 0, 0) if (i % 2 == 0) else self.shift_size, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop, + attn_drop=attn_drop, + drop_path=drop_path[i] + if isinstance(drop_path, list) else drop_path, + norm_layer=norm_layer, + use_checkpoint=use_checkpoint, + ) for i in range(depth) + ]) + + self.downsample = downsample + if self.downsample is not None: + self.downsample = downsample(dim=dim, norm_layer=norm_layer) + + def forward(self, x): + """ Forward function. + + Args: + x: Input feature, tensor size (B, C, D, H, W). + """ + # calculate attention mask for SW-MSA + B = paddle.shape(x)[0] + _, C, D, H, W = x.shape + window_size, shift_size = get_window_size((D, H, W), self.window_size, + self.shift_size) + # x = rearrange(x, 'b c d h w -> b d h w c') + x = x.transpose([0, 2, 3, 4, 1]) + Dp = int(np.ceil(D / window_size[0])) * window_size[0] + Hp = int(np.ceil(H / window_size[1])) * window_size[1] + Wp = int(np.ceil(W / window_size[2])) * window_size[2] + attn_mask = compute_mask(Dp, Hp, Wp, window_size, shift_size) + for blk in self.blocks: + x = blk(x, attn_mask) + x = x.reshape([B, D, H, W, C]) + + if self.downsample is not None: + x = self.downsample(x) + x = x.transpose([0, 4, 1, 2, 3]) + return x + + +class PatchEmbed3D(nn.Layer): + """ Video to Patch Embedding. + + Args: + patch_size (int): Patch token size. Default: (2,4,4). + in_chans (int): Number of input video channels. Default: 3. + embed_dim (int): Number of linear projection output channels. Default: 96. + norm_layer (nn.Layer, optional): Normalization layer. Default: None + """ + def __init__(self, + patch_size=(2, 4, 4), + in_chans=3, + embed_dim=96, + norm_layer=None): + super().__init__() + self.patch_size = patch_size + + self.in_chans = in_chans + self.embed_dim = embed_dim + + self.proj = nn.Conv3D(in_chans, + embed_dim, + kernel_size=patch_size, + stride=patch_size) + if norm_layer is not None: + self.norm = norm_layer(embed_dim) + else: + self.norm = None + + def forward(self, x): + _, _, D, H, W = x.shape + if W % self.patch_size[2] != 0: + x = F.pad( + x, (0, self.patch_size[2] - W % self.patch_size[2], 0, 0, 0, 0), + data_format='NCDHW') + if H % self.patch_size[1] != 0: + x = F.pad( + x, (0, 0, 0, self.patch_size[1] - H % self.patch_size[1], 0, 0), + data_format='NCDHW') + if D % self.patch_size[0] != 0: + x = F.pad( + x, (0, 0, 0, 0, 0, self.patch_size[0] - D % self.patch_size[0]), + data_format='NCDHW') + + x = self.proj(x) # B C D Wh Ww + if self.norm is not None: + D, Wh, Ww = x.shape[2], x.shape[3], x.shape[4] + x = x.flatten(2).transpose([0, 2, 1]) + x = self.norm(x) + x = x.transpose([0, 2, 1]).reshape([-1, self.embed_dim, D, Wh, Ww]) + + return x + + +@BACKBONES.register() +class SwinTransformer3D(nn.Layer): + """ Swin Transformer backbone. + A Paddle impl of : `Swin Transformer: Hierarchical Vision Transformer using Shifted Windows` - + https://arxiv.org/pdf/2103.14030 + + Args: + patch_size (int | tuple(int)): Patch size. Default: (4,4,4). + in_chans (int): Number of input image channels. Default: 3. + embed_dim (int): Number of linear projection output channels. Default: 96. + depths (tuple[int]): Depths of each Swin Transformer stage. + num_heads (tuple[int]): Number of attention head of each stage. + window_size (int): Window size. Default: 7. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4. + qkv_bias (bool): If True, add a learnable bias to query, key, value. Default: Truee + qk_scale (float): Override default qk scale of head_dim ** -0.5 if set. + drop_rate (float): Dropout rate. + attn_drop_rate (float): Attention dropout rate. Default: 0. + drop_path_rate (float): Stochastic depth rate. Default: 0.2. + norm_layer: Normalization layer. Default: nn.LayerNorm. + patch_norm (bool): If True, add normalization after patch embedding. Default: False. + frozen_stages (int): Stages to be frozen (stop grad and set eval mode). + -1 means not freezing any parameters. + """ + def __init__(self, + pretrained=None, + patch_size=(4, 4, 4), + in_chans=3, + embed_dim=96, + depths=[2, 2, 6, 2], + num_heads=[3, 6, 12, 24], + window_size=(2, 7, 7), + mlp_ratio=4., + qkv_bias=True, + qk_scale=None, + drop_rate=0., + attn_drop_rate=0., + drop_path_rate=0.2, + norm_layer=nn.LayerNorm, + patch_norm=False, + frozen_stages=-1, + use_checkpoint=False): + super().__init__() + + self.pretrained = pretrained + self.num_layers = len(depths) + self.embed_dim = embed_dim + self.patch_norm = patch_norm + self.frozen_stages = frozen_stages + self.window_size = window_size + self.patch_size = patch_size + + # split image into non-overlapping patches + self.patch_embed = PatchEmbed3D( + patch_size=patch_size, + in_chans=in_chans, + embed_dim=embed_dim, + norm_layer=norm_layer if self.patch_norm else None) + + self.pos_drop = nn.Dropout(p=drop_rate) + + # stochastic depth + dpr = [ + x.item() for x in paddle.linspace(0, drop_path_rate, sum(depths)) + ] # stochastic depth decay rule + + # build layers + self.layers = nn.LayerList() + for i_layer in range(self.num_layers): + layer = BasicLayer( + dim=int(embed_dim * 2**i_layer), + depth=depths[i_layer], + num_heads=num_heads[i_layer], + window_size=window_size, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop_rate, + attn_drop=attn_drop_rate, + drop_path=dpr[sum(depths[:i_layer]):sum(depths[:i_layer + 1])], + norm_layer=norm_layer, + downsample=PatchMerging + if i_layer < self.num_layers - 1 else None, + use_checkpoint=use_checkpoint) + self.layers.append(layer) + + self.num_features = int(embed_dim * 2**(self.num_layers - 1)) + + # add a norm layer for each output + self.norm = norm_layer(self.num_features) + + self._freeze_stages() + + def _freeze_stages(self): + if self.frozen_stages >= 0: + self.patch_embed.eval() + for param in self.patch_embed.parameters(): + param.stop_gradient = True + + if self.frozen_stages >= 1: + self.pos_drop.eval() + for i in range(0, self.frozen_stages): + m = self.layers[i] + m.eval() + for param in m.parameters(): + param.stop_gradient = True + + def _init_fn(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight, std=0.02) + if m.bias is not None: + zeros_(m.bias) + elif isinstance(m, nn.LayerNorm): + zeros_(m.bias) + ones_(m.weight) + + def init_weights(self): + """Initialize the weights in backbone. + + Args: + pretrained (str, optional): Path to pre-trained weights. + Defaults to None. + """ + """First init model's weight""" + + self.apply(self._init_fn) + """Second, if provide pretrained ckpt, load it""" + if isinstance( + self.pretrained, str + ) and self.pretrained.strip() != "": # load pretrained weights + load_ckpt(self, self.pretrained) + elif self.pretrained is None or self.pretrained.strip() == "": + pass + else: + raise NotImplementedError + + def forward(self, x): + """Forward function.""" + x = self.patch_embed(x) + x = self.pos_drop(x) + + for layer in self.layers: + x = layer(x) + + x = x.transpose([0, 2, 3, 4, 1]) + x = self.norm(x) + x = x.transpose([0, 4, 1, 2, 3]) + return x + + def train(self, mode=True): + """Convert the model into training mode while keep layers freezed.""" + super(SwinTransformer3D, self).train(mode) + self._freeze_stages() diff --git a/paddlevideo/modeling/backbones/transnetv2.py b/paddlevideo/modeling/backbones/transnetv2.py new file mode 100644 index 0000000000000000000000000000000000000000..60603e2c9a544258a93a532b1e46c8bfb3a3b441 --- /dev/null +++ b/paddlevideo/modeling/backbones/transnetv2.py @@ -0,0 +1,582 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as functional +import random +from paddle import ParamAttr + +from ..registry import BACKBONES + + +class OctConv3D(nn.Layer): + def __init__(self, in_filters, filters, kernel_size=3, dilation_rate=(1, 1, 1), alpha=0.25, + use_bias=True, kernel_initializer=nn.initializer.KaimingNormal()): + super(OctConv3D, self).__init__() + + self.low_channels = int(filters * alpha) + self.high_channels = filters - self.low_channels + + self.high_to_high = nn.Conv3D(in_filters, self.high_channels, kernel_size=kernel_size, + dilation=dilation_rate, padding=(dilation_rate[0], 1, 1), + weight_attr=ParamAttr(initializer=kernel_initializer), + bias_attr=ParamAttr( + initializer=nn.initializer.Constant(value=0.)) if use_bias else use_bias) + self.high_to_low = nn.Conv3D(self.high_channels, self.low_channels, kernel_size=kernel_size, + dilation=dilation_rate, padding=(dilation_rate[0], 1, 1), + weight_attr=ParamAttr(initializer=kernel_initializer), + bias_attr=False) + self.low_to_high = nn.Conv3D(in_filters, self.high_channels, kernel_size=kernel_size, + dilation=dilation_rate, padding=(dilation_rate[0], 1, 1), + weight_attr=ParamAttr(initializer=kernel_initializer), + bias_attr=False) + self.low_to_low = nn.Conv3D(self.high_channels, self.low_channels, kernel_size=kernel_size, + dilation=dilation_rate, padding=(dilation_rate[0], 1, 1), + weight_attr=ParamAttr(initializer=kernel_initializer), + bias_attr=ParamAttr( + initializer=nn.initializer.Constant(value=0.)) if use_bias else use_bias) + self.upsampler = nn.Upsample(size=(1, 2, 2), data_format='NCDHW') + self.downsampler = nn.AvgPool3D(kernel_size=(1, 2, 2), stride=(1, 2, 2), padding=(0, 1, 1)) + + @staticmethod + def pad_to(tensor, target_shape): + shape = tensor.shape + padding = [[0, tar - curr] for curr, tar in zip(shape, target_shape)] + return functional.pad(tensor, padding, "CONSTANT", data_format='NCDHW') + + @staticmethod + def crop_to(tensor, target_width, target_height): + return tensor[:, :, :target_height, :target_width] + + def forward(self, inputs): + low_inputs, high_inputs = inputs + + high_to_high = self.high_to_high(high_inputs) + high_to_low = self.high_to_low(self.downsampler(high_inputs)) + + low_to_high = self.upsampler(self.low_to_high(low_inputs)) + low_to_low = self.low_to_low(low_inputs) + + high_output = high_to_high[:, :, :, :low_to_high.shape[3], :low_to_high.shape[4]] + low_to_high + low_output = low_to_low + high_to_low[:, :, :, :low_to_low.shape[3], :low_to_low.shape[4]] + + return low_output, high_output + + +class Conv3DConfigurable(nn.Layer): + def __init__(self, + in_filters, + filters, + dilation_rate, + separable=True, + octave=False, + use_bias=True): + super(Conv3DConfigurable, self).__init__() + assert not (separable and octave) + + if separable: + conv1 = nn.Conv3D(in_filters, 2 * filters, kernel_size=(1, 3, 3), + dilation=(1, 1, 1), padding=(0, 1, 1), + weight_attr=ParamAttr(initializer=nn.initializer.KaimingNormal()), + bias_attr=False) + conv2 = nn.Conv3D(2 * filters, filters, kernel_size=(3, 1, 1), + dilation=(dilation_rate, 1, 1), padding=(dilation_rate, 0, 0), + weight_attr=ParamAttr(initializer=nn.initializer.KaimingNormal()), + bias_attr=ParamAttr( + initializer=nn.initializer.Constant(value=0.)) if use_bias else use_bias) + self.layers = nn.LayerList([conv1, conv2]) + elif octave: + conv = OctConv3D(in_filters, filters, kernel_size=3, dilation_rate=(dilation_rate, 1, 1), + use_bias=use_bias, + kernel_initializer=nn.initializer.KaimingNormal()) + self.layers = [conv] + else: + conv = nn.Conv3D(in_filters, filters, kernel_size=3, + dilation=(dilation_rate, 1, 1), padding=(dilation_rate, 1, 1), + weight_attr=ParamAttr(initializer=nn.initializer.KaimingNormal()), + bias_attr=ParamAttr( + initializer=nn.initializer.Constant(value=0.)) if use_bias else use_bias) + self.layers = nn.LayerList([conv]) + + def forward(self, inputs): + x = inputs + for layer in self.layers: + x = layer(x) + return x + + +class DilatedDCNNV2(nn.Layer): + def __init__(self, + in_filters, + filters, + batch_norm=True, + activation=None, + octave_conv=False): + super(DilatedDCNNV2, self).__init__() + assert not (octave_conv and batch_norm) + + self.Conv3D_1 = Conv3DConfigurable(in_filters, filters, 1, use_bias=not batch_norm, octave=octave_conv) + self.Conv3D_2 = Conv3DConfigurable(in_filters, filters, 2, use_bias=not batch_norm, octave=octave_conv) + self.Conv3D_4 = Conv3DConfigurable(in_filters, filters, 4, use_bias=not batch_norm, octave=octave_conv) + self.Conv3D_8 = Conv3DConfigurable(in_filters, filters, 8, use_bias=not batch_norm, octave=octave_conv) + self.octave = octave_conv + + self.bn = nn.BatchNorm3D(filters * 4, momentum=0.99, epsilon=1e-03, + weight_attr=ParamAttr(initializer=nn.initializer.Constant(value=1.)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.)) + ) if batch_norm else None + self.activation = activation + + def forward(self, inputs): + conv1 = self.Conv3D_1(inputs) + conv2 = self.Conv3D_2(inputs) + conv3 = self.Conv3D_4(inputs) + conv4 = self.Conv3D_8(inputs) + + # shape of convi[j]/convi is [B, 3, T, H, W], concat in channel dimension + if self.octave: + x = [paddle.concat([conv1[0], conv2[0], conv3[0], conv4[0]], axis=1), + paddle.concat([conv1[1], conv2[1], conv3[1], conv4[1]], axis=1)] + else: + x = paddle.concat([conv1, conv2, conv3, conv4], axis=1) + + if self.bn is not None: + x = self.bn(x) + + if self.activation is not None: + if self.octave: + x = [self.activation(x[0]), self.activation(x[1])] + else: + x = self.activation(x) + return x + + +class StackedDDCNNV2(nn.Layer): + def __init__(self, + in_filters, + n_blocks, + filters, + shortcut=True, + use_octave_conv=False, + pool_type="avg", + stochastic_depth_drop_prob=0.0): + super(StackedDDCNNV2, self).__init__() + assert pool_type == "max" or pool_type == "avg" + if use_octave_conv and pool_type == "max": + print("WARN: Octave convolution was designed with average pooling, not max pooling.") + + self.shortcut = shortcut + self.DDCNN = nn.LayerList([ + DilatedDCNNV2(in_filters if i == 1 else filters * 4, filters, octave_conv=use_octave_conv, + activation=functional.relu if i != n_blocks else None) for i in range(1, n_blocks + 1) + ]) + self.pool = nn.MaxPool3D(kernel_size=(1, 2, 2)) if pool_type == "max" else nn.AvgPool3D(kernel_size=(1, 2, 2)) + self.octave = use_octave_conv + self.stochastic_depth_drop_prob = stochastic_depth_drop_prob + + def forward(self, inputs): + x = inputs + shortcut = None + + if self.octave: + x = [self.pool(x), x] + for block in self.DDCNN: + x = block(x) + if shortcut is None: + shortcut = x + # shape of x[i] is [B, 3, T, H, W], concat in channel dimension + if self.octave: + x = paddle.concat([x[0], self.pool(x[1])], axis=1) + + x = functional.relu(x) + + if self.shortcut is not None: + if self.stochastic_depth_drop_prob != 0.: + if self.training: + if random.random() < self.stochastic_depth_drop_prob: + x = shortcut + else: + x = x + shortcut + else: + x = (1 - self.stochastic_depth_drop_prob) * x + shortcut + else: + x += shortcut + + if not self.octave: + x = self.pool(x) + return x + + +class ResNetBlock(nn.Layer): + def __init__(self, in_filters, filters, strides=(1, 1)): + super(ResNetBlock, self).__init__() + + self.conv1 = nn.Conv2D(in_filters, filters, kernel_size=(3, 3), stride=strides, padding=(1, 1), + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=False) + self.bn1 = nn.BatchNorm2D(filters, + weight_attr=ParamAttr(initializer=nn.initializer.Constant(value=1.)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.))) + + self.conv2 = nn.Conv2D(filters, filters, kernel_size=(3, 3), padding=(1, 1), + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=False) + self.bn2 = nn.BatchNorm2D(filters, + weight_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.))) + + def forward(self, inputs): + x = self.conv1(inputs) + x = self.bn1(x) + x = functional.relu(x) + + x = self.conv2(x) + x = self.bn2(x) + + shortcut = inputs + x += shortcut + + return functional.relu(x) + + +class ResNetFeatures(nn.Layer): + def __init__(self, in_filters=3, + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225]): + super(ResNetFeatures, self).__init__() + self.conv1 = nn.Conv2D(in_channels=in_filters, out_channels=64, kernel_size=(7, 7), + stride=(2, 2), padding=(3, 3), + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=False) + self.bn1 = nn.BatchNorm2D(num_features=64, momentum=0.99, epsilon=1e-03, + weight_attr=ParamAttr(initializer=nn.initializer.Constant(value=1.)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.)) + ) + self.max_pool = nn.MaxPool2D(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1)) + + self.layer2a = ResNetBlock(64, 64) + self.layer2b = ResNetBlock(64, 64) + + self.mean = paddle.to_tensor(mean) + self.std = paddle.to_tensor(std) + + def forward(self, inputs): + shape = inputs.shape + x = paddle.reshape(inputs, [shape[0] * shape[2], shape[1], shape[3], shape[4]]) + x = (x - self.mean) / self.std + + x = self.conv1(x) + x = self.bn1(x) + x = functional.relu(x) + x = self.max_pool(x) + x = self.layer2a(x) + x = self.layer2b(x) + + new_shape = x.shape + x = paddle.reshape(x, [shape[0], new_shape[1], shape[2], new_shape[2], new_shape[3]]) + return x + + +class FrameSimilarity(nn.Layer): + def __init__(self, + in_filters, + similarity_dim=128, + lookup_window=101, + output_dim=128, + stop_gradient=False, + use_bias=False): + super(FrameSimilarity, self).__init__() + self.projection = nn.Linear(in_filters, similarity_dim, + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=use_bias) + self.fc = nn.Linear(lookup_window, output_dim, + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.))) + + self.lookup_window = lookup_window + self.stop_gradient = stop_gradient + assert lookup_window % 2 == 1, "`lookup_window` must be odd integer" + + def forward(self, inputs): + x = paddle.concat([paddle.mean(x, axis=[3, 4]) for x in inputs], axis=1) + x = paddle.transpose(x, (0, 2, 1)) + + if self.stop_gradient: + x = x.stop_gradient + + x = self.projection(x) + x = functional.normalize(x, p=2, axis=2) + batch_size = paddle.slice(x.shape, starts=[0], ends=[1], axes=[0]) if x.shape[0] == -1 else x.shape[0] + time_window = x.shape[1] + similarities = paddle.bmm(x, x.transpose([0, 2, 1])) # [batch_size, time_window, time_window] + + similarities_padded = functional.pad(similarities, + [(self.lookup_window - 1) // 2, (self.lookup_window - 1) // 2], + data_format='NCL') + + batch_indices = paddle.arange(0, batch_size).reshape([batch_size, 1, 1]) + batch_indices = paddle.tile(batch_indices, [1, time_window, self.lookup_window]) + time_indices = paddle.arange(0, time_window).reshape([1, time_window, 1]) + time_indices = paddle.tile(time_indices, [batch_size, 1, self.lookup_window]) + lookup_indices = paddle.arange(0, self.lookup_window).reshape([1, 1, self.lookup_window]) + lookup_indices = paddle.tile(lookup_indices, [batch_size, time_window, 1]) + time_indices + indices = paddle.stack([batch_indices, time_indices, lookup_indices], -1) + similarities = paddle.gather_nd(similarities_padded, indices) + return functional.relu(self.fc(similarities)) + + +class ConvexCombinationRegularization(nn.Layer): + def __init__(self, in_filters, filters=32, delta_scale=10., loss_weight=0.01): + super(ConvexCombinationRegularization, self).__init__() + + self.projection = nn.Conv3D(in_filters, filters, kernel_size=1, dilation=1, padding=(0, 0, 0), + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.))) + self.features = nn.Conv3D((filters * 3), filters * 2, + kernel_size=(3, 3, 3), dilation=1, padding=(1, 1, 1), + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.))) + self.dense = nn.Linear(64, 1, weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), bias_attr=True) + self.loss = nn.SmoothL1Loss(reduction='none') + self.delta_scale = delta_scale + self.loss_weight = loss_weight + + def forward(self, image_inputs, feature_inputs): + x = feature_inputs + x = self.projection(x) + x = functional.relu(x) + batch_size = x.shape[0] + window_size = x.shape[2] + first_frame = paddle.tile(x[:, :, :1], [1, 1, window_size, 1, 1]) + last_frame = paddle.tile(x[:, :, -1:], [1, 1, window_size, 1, 1]) + x = paddle.concat([x, first_frame, last_frame], 1) + x = self.features(x) + x = functional.relu(x) + x = paddle.mean(x, axis=[3, 4]) + x = paddle.transpose(x, (0, 2, 1)) + alpha = self.dense(x) + alpha = paddle.transpose(alpha, (0, 2, 1)) + + first_img = paddle.tile(image_inputs[:, :, :1], [1, 1, window_size, 1, 1]) + last_img = paddle.tile(image_inputs[:, :, -1:], [1, 1, window_size, 1, 1]) + + alpha_ = functional.sigmoid(alpha) + alpha_ = paddle.reshape(alpha_, [batch_size, 1, window_size, 1, 1]) + predictions_ = (alpha_ * first_img + (1 - alpha_) * last_img) + loss_ = self.loss(label=image_inputs / self.delta_scale, input=predictions_ / self.delta_scale) + loss_ = self.loss_weight * paddle.mean(loss_) + return alpha, loss_ + + +class ColorHistograms(nn.Layer): + def __init__(self, + lookup_window=101, + output_dim=None): + super(ColorHistograms, self).__init__() + + self.fc = nn.Linear(lookup_window, output_dim, + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=ParamAttr( + initializer=nn.initializer.Constant(value=0.))) if output_dim is not None else None + self.lookup_window = lookup_window + assert lookup_window % 2 == 1, "`lookup_window` must be odd integer" + + def compute_color_histograms(self, frames): + frames = frames.astype('int32') + + def get_bin(frames): + # returns 0 .. 511 + R, G, B = frames[:, :, 0], frames[:, :, 1], frames[:, :, 2] + R, G, B = R // 32, G // 32, B // 32 + return (R * 64) + (G * 8) + B + + batch_size = paddle.slice(frames.shape, starts=[0], ends=[1], axes=[0]) if frames.shape[0] == -1 else frames.shape[0] + time_window, height, width, no_channels = frames.shape[1:] + + assert no_channels == 3 or no_channels == 6 + if no_channels == 3: + frames_flatten = frames.reshape([-1, height * width, 3]) + else: + frames_flatten = frames.reshape([-1, height * width * 2, 3]) + + binned_values = get_bin(frames_flatten) + + frame_bin_prefix = (paddle.arange(0, batch_size * time_window) * 512).reshape([-1, 1]) + binned_values = (binned_values + frame_bin_prefix).reshape([-1, 1]) + histograms = paddle.zeros_like(frame_bin_prefix, dtype='int32').tile([512]).reshape([-1]) + histograms = histograms.scatter_nd_add(binned_values, paddle.ones_like(binned_values, dtype='int32').reshape([-1])) + histograms = histograms.reshape([batch_size, time_window, 512]).astype('float32') + histograms_normalized = functional.normalize(histograms, p=2, axis=2) + return histograms_normalized + + def forward(self, inputs): + x = self.compute_color_histograms(inputs) + batch_size = paddle.slice(x.shape, starts=[0], ends=[1], axes=[0]) if x.shape[0] == -1 else x.shape[0] + time_window = x.shape[1] + similarities = paddle.bmm(x, x.transpose([0, 2, 1])) # [batch_size, time_window, time_window] + similarities_padded = functional.pad(similarities, + [(self.lookup_window - 1) // 2, (self.lookup_window - 1) // 2], + data_format='NCL') + + batch_indices = paddle.arange(0, batch_size).reshape([batch_size, 1, 1]) + batch_indices = paddle.tile(batch_indices, [1, time_window, self.lookup_window]) + time_indices = paddle.arange(0, time_window).reshape([1, time_window, 1]) + time_indices = paddle.tile(time_indices, [batch_size, 1, self.lookup_window]) + lookup_indices = paddle.arange(0, self.lookup_window).reshape([1, 1, self.lookup_window]) + lookup_indices = paddle.tile(lookup_indices, [batch_size, time_window, 1]) + time_indices + + indices = paddle.stack([batch_indices, time_indices, lookup_indices], -1) + similarities = paddle.gather_nd(similarities_padded, indices) + + if self.fc is not None: + return functional.relu(self.fc(similarities)) + return similarities + + +@BACKBONES.register() +class TransNetV2(nn.Layer): + """TransNetV2 model from + `"TransNet V2: An effective deep network architecture for fast shot transition detection" `_ + """ + def __init__(self, + F=16, L=3, S=2, D=1024, + use_many_hot_targets=True, + use_frame_similarity=True, + use_color_histograms=True, + use_mean_pooling=False, + dropout_rate=0.5, + use_convex_comb_reg=False, + use_resnet_features=False, + use_resnet_like_top=False, + frame_similarity_on_last_layer=False, + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225]): + super(TransNetV2, self).__init__() + + self.mean = np.array(mean, np.float32).reshape([1, 3, 1, 1]) * 255 + self.std = np.array(std, np.float32).reshape([1, 3, 1, 1]) * 255 + + self.use_resnet_features = use_resnet_features + self.resnet_layers = ResNetFeatures(in_filters=3, mean=self.mean, std=self.std) if self.use_resnet_features else None + self.resnet_like_top = use_resnet_like_top + if self.resnet_like_top: + self.resnet_like_top_conv = nn.Conv3D(64 if self.use_resnet_features else 3, 32, kernel_size=(3, 7, 7), + stride=(1, 2, 2), + padding=(1, 3, 3), + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=False) + self.resnet_like_top_bn = nn.BatchNorm3D(32, momentum=0.99, epsilon=1e-03, + weight_attr=ParamAttr( + initializer=nn.initializer.Constant(value=1.)), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.))) + self.resnet_like_top_max_pool = nn.MaxPool3D(kernel_size=(1, 3, 3), stride=(1, 2, 2), + padding=(0, 1, 1)) + + if self.resnet_like_top: + in_filters = 32 + elif self.use_resnet_features: + in_filters = 64 + else: + in_filters = 3 + self.SDDCNN = nn.LayerList( + [StackedDDCNNV2(in_filters=in_filters, n_blocks=S, filters=F, + stochastic_depth_drop_prob=0.)] + + [StackedDDCNNV2(in_filters=(F * 2 ** (i - 1)) * 4, n_blocks=S, filters=F * 2 ** i) for i in range(1, L)] + ) + + self.frame_sim_layer = FrameSimilarity( + sum([(F * 2 ** i) * 4 for i in range(L)]), lookup_window=101, output_dim=128, similarity_dim=128, + use_bias=True + ) if use_frame_similarity else None + self.color_hist_layer = ColorHistograms( + lookup_window=101, output_dim=128 + ) if use_color_histograms else None + + self.dropout = nn.Dropout(dropout_rate) if dropout_rate is not None else None + + output_dim = ((F * 2 ** (L - 1)) * 4) * 3 * 6 # 3x6 for spatial dimensions + if use_frame_similarity: output_dim += 128 + if use_color_histograms: output_dim += 128 + + self.use_mean_pooling = use_mean_pooling + + self.has_downsample = False + if self.use_resnet_features or self.resnet_like_top or self.use_mean_pooling: + self.has_downsample = True + self.fc1 = nn.Linear(512 if self.has_downsample else output_dim, D, + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.)) + ) + self.frame_similarity_on_last_layer = frame_similarity_on_last_layer + self.cls_layer1 = nn.Linear(1152 if self.frame_similarity_on_last_layer else D, 1, + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.)) + ) + self.cls_layer2 = nn.Linear(1152 if self.frame_similarity_on_last_layer else D, 1, + weight_attr=ParamAttr(initializer=nn.initializer.XavierUniform()), + bias_attr=ParamAttr(initializer=nn.initializer.Constant(value=0.)) + ) if use_many_hot_targets else None + + self.convex_comb_reg = ConvexCombinationRegularization( + in_filters=(F * 2 ** (L - 1) * 4)) if use_convex_comb_reg else None + + def forward(self, inputs): + assert list(inputs.shape[2:]) == [27, 48, 3] and inputs.dtype == paddle.float32, \ + "incorrect input type and/or shape" + out_dict = {} + + # shape [B, T, H, W, 3] to shape [B, 3, T, H, W] + x = inputs.transpose([0, 4, 1, 2, 3]) + if self.use_resnet_features: + x = self.resnet_layers(x) + else: + x = x / 255. + inputs = inputs.clip(min=0).astype('uint8') + if self.resnet_like_top: + x = self.resnet_like_top_conv(x) + x = self.resnet_like_top_bn(x) + x = self.resnet_like_top_max_pool(x) + block_features = [] + for block in self.SDDCNN: + x = block(x) + block_features.append(x) + if self.convex_comb_reg is not None: + out_dict["alphas"], out_dict["comb_reg_loss"] = self.convex_comb_reg(inputs.transpose([0, 4, 1, 2, 3]), x) + if self.use_mean_pooling: + x = paddle.mean(x, axis=[3, 4]) + x = x.transpose([0, 2, 1]) + else: + x = x.transpose([0, 2, 3, 4, 1]) + x = x.reshape([x.shape[0], x.shape[1], x.shape[2]*x.shape[3]*x.shape[4]]) + if self.frame_sim_layer is not None: + x = paddle.concat([self.frame_sim_layer(block_features), x], 2) + if self.color_hist_layer is not None: + x = paddle.concat([self.color_hist_layer(inputs), x], 2) + x = self.fc1(x) + x = functional.relu(x) + if self.dropout is not None: + x = self.dropout(x) + if self.frame_sim_layer is not None and self.frame_similarity_on_last_layer: + x = paddle.concat([self.frame_sim_layer(block_features), x], 2) + one_hot = self.cls_layer1(x) + if self.cls_layer2 is not None: + out_dict["many_hot"] = self.cls_layer2(x) + + if len(out_dict) > 0: + return one_hot, out_dict + + return one_hot + diff --git a/paddlevideo/modeling/backbones/vit.py b/paddlevideo/modeling/backbones/vit.py new file mode 100644 index 0000000000000000000000000000000000000000..e4ecebd9191a4c2ace3f04c2b28e88a82c8ee96c --- /dev/null +++ b/paddlevideo/modeling/backbones/vit.py @@ -0,0 +1,465 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from paddle.nn.initializer import Constant + +from ...utils import load_ckpt +from ..registry import BACKBONES +from ..weight_init import trunc_normal_ + +__all__ = ['VisionTransformer'] + +zeros_ = Constant(value=0.) +ones_ = Constant(value=1.) + + +def to_2tuple(x): + return tuple([x] * 2) + + +def drop_path(x, drop_prob=0., training=False): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper... + # issuecomment-532968956 ... + See discussion: https://github.com/tensorflow/tpu/issues/494 + """ + if drop_prob == 0. or not training: + return x + keep_prob = paddle.to_tensor(1 - drop_prob) + shape = (paddle.shape(x)[0], ) + (1, ) * (x.ndim - 1) + random_tensor = keep_prob + paddle.rand(shape).astype(x.dtype) + random_tensor = paddle.floor(random_tensor) # binarize + output = x.divide(keep_prob) * random_tensor + + return output + + +class DropPath(nn.Layer): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + """ + def __init__(self, drop_prob=None): + super(DropPath, self).__init__() + self.drop_prob = drop_prob + + def forward(self, x): + return drop_path(x, self.drop_prob, self.training) + + +class Identity(nn.Layer): + def __init__(self): + super(Identity, self).__init__() + + def forward(self, input): + return input + + +class Mlp(nn.Layer): + def __init__(self, + in_features, + hidden_features=None, + out_features=None, + act_layer=nn.GELU, + drop=0.0): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features) + self.drop = nn.Dropout(drop) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x + + +class Attention(nn.Layer): + def __init__(self, + dim, + num_heads=8, + qkv_bias=False, + qk_scale=None, + attn_drop=0.0, + proj_drop=0.0): + super().__init__() + + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim**-0.5 + + self.qkv = nn.Linear(dim, dim * 3, bias_attr=qkv_bias) + self.proj = nn.Linear(dim, dim) + self.proj_drop = nn.Dropout(proj_drop) + self.attn_drop = nn.Dropout(attn_drop) + + def forward(self, x): + N, C = x.shape[1:] + qkv = self.qkv(x).reshape( + (-1, N, 3, self.num_heads, C // self.num_heads)).transpose( + (2, 0, 3, 1, 4)) + q, k, v = qkv[0], qkv[1], qkv[2] + + attn = (q.matmul(k.transpose((0, 1, 3, 2)))) * self.scale + attn = nn.functional.softmax(attn, axis=-1) + attn = self.attn_drop(attn) + + x = (attn.matmul(v)).transpose((0, 2, 1, 3)).reshape((-1, N, C)) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class Block(nn.Layer): + def __init__(self, + dim, + num_heads, + mlp_ratio=4.0, + qkv_bias=False, + qk_scale=None, + drop=0.0, + attn_drop=0.0, + drop_path=0.1, + act_layer=nn.GELU, + norm_layer='nn.LayerNorm', + epsilon=1e-5, + attention_type='divided_space_time'): + + super().__init__() + self.attention_type = attention_type + if isinstance(norm_layer, str): + self.norm1 = eval(norm_layer)(dim, epsilon=epsilon) + elif isinstance(norm_layer, Callable): + self.norm1 = norm_layer(dim, epsilon=epsilon) + else: + raise TypeError( + "The norm_layer must be str or paddle.nn.layer.Layer class") + + self.attn = Attention(dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + attn_drop=attn_drop, + proj_drop=drop) + + # Temporal Attention Parameters + if self.attention_type == 'divided_space_time': + if isinstance(norm_layer, str): + self.temporal_norm1 = eval(norm_layer)(dim, epsilon=epsilon) + elif isinstance(norm_layer, Callable): + self.temporal_norm1 = norm_layer(dim, epsilon=epsilon) + else: + raise TypeError( + "The norm_layer must be str or paddle.nn.layer.Layer class") + self.temporal_attn = Attention(dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + attn_drop=attn_drop, + proj_drop=drop) + self.temporal_fc = nn.Linear(dim, dim) + + # NOTE: drop path for stochastic depth, we shall see if this is better than dropout here + self.drop_path = DropPath(drop_path) if drop_path > 0. else Identity() + if isinstance(norm_layer, str): + self.norm2 = eval(norm_layer)(dim, epsilon=epsilon) + elif isinstance(norm_layer, Callable): + self.norm2 = norm_layer(dim, epsilon=epsilon) + else: + raise TypeError( + "The norm_layer must be str or paddle.nn.layer.Layer class") + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp(in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop) + + def forward(self, x, B, T, W): + num_spatial_tokens = (x.shape[1] - 1) // T + H = num_spatial_tokens // W + if self.attention_type in ['space_only', 'joint_space_time']: + x = x + self.drop_path(self.attn(self.norm1(x))) + x = x + self.drop_path(self.mlp(self.norm2(x))) + return x + elif self.attention_type == 'divided_space_time': + ########## Temporal ########## + xt = x[:, 1:, :] + _, _, _, _t, _m = B, H, W, T, xt.shape[-1] + xt = xt.reshape([-1, _t, _m]) + + res_temporal = self.drop_path( + self.temporal_attn(self.temporal_norm1(xt))) + + _, _h, _w, _t, _m = B, H, W, T, res_temporal.shape[-1] + res_temporal = res_temporal.reshape([-1, _h * _w * _t, _m]) + + res_temporal = self.temporal_fc(res_temporal) + xt = x[:, 1:, :] + res_temporal + + ########## Spatial ########## + init_cls_token = x[:, 0, :].unsqueeze(1) + cls_token = init_cls_token.tile((1, T, 1)) + _b, _t, _m = cls_token.shape + cls_token = cls_token.reshape([-1, _m]).unsqueeze(1) + + xs = xt + _, _h, _w, _t, _m = B, H, W, T, xs.shape[-1] + xs = xs.reshape([-1, _h, _w, _t, _m]).transpose( + (0, 3, 1, 2, 4)).reshape([-1, _h * _w, _m]) + xs = paddle.concat((cls_token, xs), axis=1) + res_spatial = self.drop_path(self.attn(self.norm1(xs))) + + # Taking care of CLS token + cls_token = res_spatial[:, 0, :] + _, _t, _m = B, T, cls_token.shape[-1] + cls_token = cls_token.reshape([-1, _t, _m]) + # averaging for every frame + cls_token = paddle.mean(cls_token, axis=1, keepdim=True) + + res_spatial = res_spatial[:, 1:, :] + _, _t, _h, _w, _m = B, T, H, W, res_spatial.shape[-1] + res_spatial = res_spatial.reshape([-1, _t, _h, _w, _m]).transpose( + (0, 2, 3, 1, 4)).reshape([-1, _h * _w * _t, _m]) + + res = res_spatial + x = xt + x = paddle.concat((init_cls_token, x), axis=1) + paddle.concat( + (cls_token, res), axis=1) + + # Mlp + x = x + self.drop_path(self.mlp(self.norm2(x))) + return x + else: + raise NotImplementedError + + +class PatchEmbed(nn.Layer): + """ Image to Patch Embedding + """ + def __init__(self, + img_size=224, + patch_size=16, + in_channels=3, + embed_dim=768): + super().__init__() + img_size = to_2tuple(img_size) + patch_size = to_2tuple(patch_size) + num_patches = (img_size[1] // patch_size[1]) * (img_size[0] // + patch_size[0]) + self.img_size = img_size + self.patch_size = patch_size + self.num_patches = num_patches + + self.proj = nn.Conv2D(in_channels, + embed_dim, + kernel_size=patch_size, + stride=patch_size) + + def forward(self, x): + B, C, T, H, W = x.shape + + assert H == self.img_size[0] and W == self.img_size[1], \ + f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})." + x = x.transpose((0, 2, 1, 3, 4)) + x = x.reshape([-1, C, H, W]) + x = self.proj(x) + W = x.shape[-1] + x = x.flatten(2).transpose((0, 2, 1)) + return x, T, W + + +@BACKBONES.register() +class VisionTransformer(nn.Layer): + """ Vision Transformer with support for patch input + """ + def __init__(self, + pretrained=None, + img_size=224, + patch_size=16, + in_channels=3, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4, + qkv_bias=False, + qk_scale=None, + drop_rate=0., + attn_drop_rate=0., + drop_path_rate=0.1, + norm_layer='nn.LayerNorm', + epsilon=1e-5, + num_seg=8, + attention_type='divided_space_time', + **args): + super().__init__() + self.pretrained = pretrained + self.num_seg = num_seg + self.attention_type = attention_type + self.num_features = self.embed_dim = embed_dim + + self.patch_embed = PatchEmbed(img_size=img_size, + patch_size=patch_size, + in_channels=in_channels, + embed_dim=embed_dim) + num_patches = self.patch_embed.num_patches + + # Positional Embeddings + self.cls_token = self.create_parameter(shape=(1, 1, embed_dim), + default_initializer=zeros_) + self.pos_embed = self.create_parameter(shape=(1, num_patches + 1, + embed_dim), + default_initializer=zeros_) + self.pos_drop = nn.Dropout(p=drop_rate) + + if self.attention_type != 'space_only': + self.time_embed = self.create_parameter(shape=(1, num_seg, + embed_dim), + default_initializer=zeros_) + self.time_drop = nn.Dropout(p=drop_rate) + + self.add_parameter("pos_embed", self.pos_embed) + self.add_parameter("cls_token", self.cls_token) + + dpr = np.linspace(0, drop_path_rate, depth) + + self.blocks = nn.LayerList([ + Block(dim=embed_dim, + num_heads=num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop_rate, + attn_drop=attn_drop_rate, + drop_path=dpr[i], + norm_layer=norm_layer, + epsilon=epsilon, + attention_type=self.attention_type) for i in range(depth) + ]) + + self.norm = eval(norm_layer)(embed_dim, epsilon=epsilon) + + def init_weights(self): + """First init model's weight""" + trunc_normal_(self.pos_embed, std=0.02) + trunc_normal_(self.cls_token, std=0.02) + self.apply(self._init_fn) + + if self.attention_type == 'divided_space_time': + i = 0 + for m in self.blocks.sublayers(include_self=True): + m_str = str(m) + if 'Block' in m_str: + if i > 0: + zeros_(m.temporal_fc.weight) + zeros_(m.temporal_fc.bias) + i += 1 + """Second, if provide pretrained ckpt, load it""" + if isinstance( + self.pretrained, str + ) and self.pretrained.strip() != "": # load pretrained weights + load_ckpt(self, + self.pretrained, + num_patches=self.patch_embed.num_patches, + num_seg=self.num_seg, + attention_type=self.attention_type) + + def _init_fn(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight) + if m.bias is not None: + zeros_(m.bias) + elif isinstance(m, nn.LayerNorm): + ones_(m.weight) + zeros_(m.bias) + + def forward_features(self, x): + # B = x.shape[0] + B = paddle.shape(x)[0] + x, T, W = self.patch_embed(x) # [BT,nH*nW,F] + cls_tokens = self.cls_token.expand((B * T, -1, -1)) # [1,1,F]->[BT,1,F] + x = paddle.concat((cls_tokens, x), axis=1) + pos_interp = (x.shape[1] != self.pos_embed.shape[1]) + if pos_interp: + pos_embed = self.pos_embed + cls_pos_embed = pos_embed[0, 0, :].unsqueeze(0).unsqueeze(1) + other_pos_embed = pos_embed[0, 1:, :].unsqueeze(0).transpose( + (0, 2, 1)) + P = int(other_pos_embed.shape[2]**0.5) + H = x.shape[1] // W + other_pos_embed = other_pos_embed.reshape([1, x.shape[2], P, P]) + new_pos_embed = F.interpolate(other_pos_embed, + size=(H, W), + mode='nearest') + new_pos_embed = new_pos_embed.flatten(2) + new_pos_embed = new_pos_embed.transpose((0, 2, 1)) + new_pos_embed = paddle.concat((cls_pos_embed, new_pos_embed), + axis=1) + x = x + new_pos_embed + else: + x = x + self.pos_embed + + x = self.pos_drop(x) + + # Time Embeddings + if self.attention_type != 'space_only': + cls_tokens = x[:B, 0, :].unsqueeze(1) if B > 0 else x.split( + T)[0].index_select(paddle.to_tensor([0]), axis=1) + x = x[:, 1:] + _, _n, _m = x.shape + _t = T + x = x.reshape([-1, _t, _n, _m]).transpose( + (0, 2, 1, 3)).reshape([-1, _t, _m]) + # Resizing time embeddings in case they don't match + time_interp = (T != self.time_embed.shape[1]) + if time_interp: # T' != T + time_embed = self.time_embed.transpose((0, 2, 1)).unsqueeze(0) + new_time_embed = F.interpolate(time_embed, + size=(T, x.shape[-1]), + mode='nearest').squeeze(0) + new_time_embed = new_time_embed.transpose((0, 2, 1)) + x = x + new_time_embed + else: + x = x + self.time_embed + + x = self.time_drop(x) + _, _t, _m = x.shape + x = x.reshape([-1, W * W * T, _m]) + x = paddle.concat((cls_tokens, x), axis=1) + + # Attention blocks + for blk in self.blocks: + x = blk(x, B, T, W) + + # Predictions for space-only baseline + if self.attention_type == 'space_only': + _, _n, _m = x.shape + _t = T + x = x.reshape([-1, _t, _n, _m]) + x = paddle.mean(x, 1) # averaging predictions for every frame + + x = self.norm(x) + return x[:, 0] # [B, embed_dim] + + def forward(self, x): + x = self.forward_features(x) + return x diff --git a/paddlevideo/modeling/backbones/vit_tweaks.py b/paddlevideo/modeling/backbones/vit_tweaks.py new file mode 100644 index 0000000000000000000000000000000000000000..a20af30f17283546f02c4e084730080f0c2f8c18 --- /dev/null +++ b/paddlevideo/modeling/backbones/vit_tweaks.py @@ -0,0 +1,515 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +from paddle import ParamAttr +from paddle.nn.initializer import Constant +from paddle.regularizer import L2Decay + +from ...utils import load_ckpt +from ..registry import BACKBONES +from ..weight_init import trunc_normal_ + +__all__ = ['VisionTransformer_tweaks'] + +zeros_ = Constant(value=0.) +ones_ = Constant(value=1.) + + +def to_2tuple(x): + return tuple([x] * 2) + + +def rand_bbox(size, lam): + """ rand_bbox """ + w = size[2] + h = size[3] + cut_rat = np.sqrt(1. - lam) + cut_w = np.int(w * cut_rat) + cut_h = np.int(h * cut_rat) + + # uniform + cx = np.random.randint(w) + cy = np.random.randint(h) + + bbx1 = np.clip(cx - cut_w // 2, 0, w) + bby1 = np.clip(cy - cut_h // 2, 0, h) + bbx2 = np.clip(cx + cut_w // 2, 0, w) + bby2 = np.clip(cy + cut_h // 2, 0, h) + + return bbx1, bby1, bbx2, bby2 + + +def drop_path(x, drop_prob=0., training=False): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper... + # issuecomment-532968956 ... + See discussion: https://github.com/tensorflow/tpu/issues/494 + """ + if drop_prob == 0. or not training: + return x + keep_prob = paddle.to_tensor(1 - drop_prob) + shape = (paddle.shape(x)[0], ) + (1, ) * (x.ndim - 1) + random_tensor = keep_prob + paddle.rand(shape, dtype=x.dtype) + random_tensor = paddle.floor(random_tensor) # binarize + output = x.divide(keep_prob) * random_tensor + + return output + + +class DropPath(nn.Layer): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + """ + def __init__(self, drop_prob=None): + super(DropPath, self).__init__() + self.drop_prob = drop_prob + + def forward(self, x): + return drop_path(x, self.drop_prob, self.training) + + +class Identity(nn.Layer): + def __init__(self): + super(Identity, self).__init__() + + def forward(self, input): + return input + + +class Mlp(nn.Layer): + def __init__(self, + in_features, + hidden_features=None, + out_features=None, + act_layer=nn.GELU, + drop=0., + wd_bias=True, + lr_mult=1.0): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features) + self.drop = nn.Dropout(drop) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x + + +class Attention(nn.Layer): + def __init__(self, + dim, + num_heads=8, + qkv_bias=False, + qk_scale=None, + attn_drop=0., + proj_drop=0., + wd_bias=True, + lr_mult=1.0): + super().__init__() + + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim**-0.5 + self.qkv = nn.Linear(dim, dim * 3, bias_attr=qkv_bias) + self.proj = nn.Linear(dim, dim) + self.proj_drop = nn.Dropout(proj_drop) + self.attn_drop = nn.Dropout(attn_drop) + + def forward(self, x): + N, C = x.shape[1:] + qkv = self.qkv(x).reshape( + (-1, N, 3, self.num_heads, C // self.num_heads)).transpose( + (2, 0, 3, 1, 4)) + q, k, v = qkv[0], qkv[1], qkv[2] + + attn = (q.matmul(k.transpose((0, 1, 3, 2)))) * self.scale + attn = nn.functional.softmax(attn, axis=-1) + attn = self.attn_drop(attn) + + x = (attn.matmul(v)).transpose((0, 2, 1, 3)).reshape((-1, N, C)) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class Block(nn.Layer): + def __init__(self, + dim, + num_heads, + mlp_ratio=4.0, + qkv_bias=False, + qk_scale=None, + drop=0.0, + attn_drop=0.0, + drop_path=0.1, + act_layer=nn.GELU, + norm_layer='nn.LayerNorm', + epsilon=1e-5, + attention_type='divided_space_time', + wd_bias=True, + lr_mult=1.0): + + super().__init__() + self.attention_type = attention_type + if isinstance(norm_layer, str): + self.norm1 = eval(norm_layer)(dim, epsilon=epsilon) + elif isinstance(norm_layer, Callable): + self.norm1 = norm_layer(dim, epsilon=epsilon) + else: + raise TypeError( + "The norm_layer must be str or paddle.nn.layer.Layer class") + + self.attn = Attention(dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + attn_drop=attn_drop, + proj_drop=drop, + wd_bias=wd_bias, + lr_mult=lr_mult) + + # Temporal Attention Parameters + if self.attention_type == 'divided_space_time': + if isinstance(norm_layer, str): + self.temporal_norm1 = eval(norm_layer)(dim, epsilon=epsilon) + elif isinstance(norm_layer, Callable): + self.temporal_norm1 = norm_layer(dim, epsilon=epsilon) + else: + raise TypeError( + "The norm_layer must be str or paddle.nn.layer.Layer class") + self.temporal_attn = Attention(dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + attn_drop=attn_drop, + proj_drop=drop, + wd_bias=wd_bias, + lr_mult=lr_mult) + self.temporal_fc = nn.Linear(dim, dim) + + # NOTE: drop path for stochastic depth, we shall see if this is better than dropout here + self.drop_path = DropPath(drop_path) if drop_path > 0. else Identity() + if isinstance(norm_layer, str): + self.norm2 = eval(norm_layer)(dim, epsilon=epsilon) + elif isinstance(norm_layer, Callable): + self.norm2 = norm_layer(dim, epsilon=epsilon) + else: + raise TypeError( + "The norm_layer must be str or paddle.nn.layer.Layer class") + + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp(in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + drop=drop, + wd_bias=wd_bias, + lr_mult=lr_mult) + + def forward(self, x, B, T, W): + num_spatial_tokens = (x.shape[1] - 1) // T + H = num_spatial_tokens // W + if self.attention_type in ['space_only', 'joint_space_time']: + x = paddle.add(x, self.drop_path(self.attn(self.norm1(x)))) + x = paddle.add(x, self.drop_path(self.mlp(self.norm2(x)))) + return x + elif self.attention_type == 'divided_space_time': + ########## Temporal ########## + xt = x[:, 1:, :] + _, _, _, _t, _m = B, H, W, T, xt.shape[-1] + xt = xt.reshape([-1, _t, _m]) + + res_temporal = self.drop_path( + self.temporal_attn(self.temporal_norm1(xt))) + + _, _h, _w, _t, _m = B, H, W, T, res_temporal.shape[-1] + res_temporal = res_temporal.reshape([-1, _h * _w * _t, _m]) + + res_temporal = self.temporal_fc(res_temporal) + xt = paddle.add(x[:, 1:, :], res_temporal) + + ########## Spatial ########## + init_cls_token = x[:, 0, :].unsqueeze(1) + cls_token = init_cls_token.tile((1, T, 1)) + _b, _t, _m = cls_token.shape + cls_token = cls_token.reshape([-1, _m]).unsqueeze(1) + + xs = xt + _, _h, _w, _t, _m = B, H, W, T, xs.shape[-1] + xs = xs.reshape([-1, _h, _w, _t, _m]).transpose( + (0, 3, 1, 2, 4)).reshape([-1, _h * _w, _m]) + xs = paddle.concat((cls_token, xs), axis=1) + res_spatial = self.drop_path(self.attn(self.norm1(xs))) + + # Taking care of CLS token + cls_token = res_spatial[:, 0, :] + _, _t, _m = B, T, cls_token.shape[-1] + cls_token = cls_token.reshape([-1, _t, _m]) + # averaging for every frame + cls_token = paddle.mean(cls_token, axis=1, keepdim=True) + + res_spatial = res_spatial[:, 1:, :] + _, _t, _h, _w, _m = B, T, H, W, res_spatial.shape[-1] + res_spatial = res_spatial.reshape([-1, _t, _h, _w, _m]).transpose( + (0, 2, 3, 1, 4)).reshape([-1, _h * _w * _t, _m]) + + res = res_spatial + x = xt + x = paddle.add(paddle.concat((init_cls_token, x), axis=1), + paddle.concat((cls_token, res), axis=1)) + # Mlp + x = paddle.add(x, self.drop_path(self.mlp(self.norm2(x)))) + return x + else: + raise NotImplementedError + + +class PatchEmbed(nn.Layer): + """ Image to Patch Embedding + """ + def __init__(self, + img_size=224, + patch_size=16, + in_channels=3, + embed_dim=768, + wd_bias=True, + lr_mult=1.0): + super().__init__() + img_size = to_2tuple(img_size) + patch_size = to_2tuple(patch_size) + num_patches = (img_size[1] // patch_size[1]) * (img_size[0] // + patch_size[0]) + self.img_size = img_size + self.patch_size = patch_size + self.num_patches = num_patches + + self.proj = nn.Conv2D(in_channels, + embed_dim, + kernel_size=patch_size, + stride=patch_size) + + def forward(self, x): + B, C, T, H, W = x.shape + + assert H == self.img_size[0] and W == self.img_size[1], \ + f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})." + x = x.transpose((0, 2, 1, 3, 4)) # [B,T,C,H,W] + x = x.reshape([-1, C, H, W]) # [BT,C,H,W] + x = self.proj(x) # [BT,F,nH,nW] + W = x.shape[-1] + x = x.flatten(2).transpose((0, 2, 1)) # [BT,F,nHnW] + return x, T, W + + +@BACKBONES.register() +class VisionTransformer_tweaks(nn.Layer): + """ Vision Transformer with support for patch input + """ + def __init__(self, + pretrained=None, + img_size=224, + patch_size=16, + in_channels=3, + embed_dim=768, + depth=12, + num_heads=12, + mlp_ratio=4, + qkv_bias=False, + qk_scale=None, + drop_rate=0., + attn_drop_rate=0., + drop_path_rate=0.1, + norm_layer='nn.LayerNorm', + epsilon=1e-5, + num_seg=8, + attention_type='divided_space_time', + wd_bias=True, + lr_mult_list=[1.0, 1.0, 1.0, 1.0, 1.0], + **args): + super().__init__() + self.pretrained = pretrained + self.num_seg = num_seg + self.attention_type = attention_type + self.lr_mult_list = lr_mult_list + self.num_features = self.embed_dim = embed_dim + + self.patch_embed = PatchEmbed(img_size=img_size, + patch_size=patch_size, + in_channels=in_channels, + embed_dim=embed_dim, + wd_bias=wd_bias, + lr_mult=self.lr_mult_list[0]) + num_patches = self.patch_embed.num_patches + + # Positional Embeddings + self.cls_token = self.create_parameter( + shape=(1, 1, embed_dim), + default_initializer=zeros_, + attr=ParamAttr(regularizer=L2Decay(0.0))) + self.pos_embed = self.create_parameter( + shape=(1, num_patches + 1, embed_dim), + default_initializer=zeros_, + attr=ParamAttr(regularizer=L2Decay(0.0))) + self.pos_drop = nn.Dropout(p=drop_rate) + + if self.attention_type != 'space_only': + self.time_embed = self.create_parameter( + shape=(1, num_seg, embed_dim), + default_initializer=zeros_, + attr=ParamAttr(regularizer=L2Decay(0.0))) + self.time_drop = nn.Dropout(p=drop_rate) + + self.add_parameter("pos_embed", self.pos_embed) + self.add_parameter("cls_token", self.cls_token) + + dpr = np.linspace(0, drop_path_rate, depth) + + self.blocks = nn.LayerList([ + Block(dim=embed_dim, + num_heads=num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop_rate, + attn_drop=attn_drop_rate, + drop_path=dpr[i], + norm_layer=norm_layer, + epsilon=epsilon, + attention_type=self.attention_type, + wd_bias=wd_bias, + lr_mult=self.lr_mult_list[(i // 4) + 1]) for i in range(depth) + ]) + + self.norm = eval(norm_layer)(embed_dim, epsilon=epsilon) + + def init_weights(self): + """First init model's weight""" + trunc_normal_(self.pos_embed, std=0.02) + trunc_normal_(self.cls_token, std=0.02) + self.apply(self._init_fn) + + if self.attention_type == 'divided_space_time': + i = 0 + for m in self.blocks.sublayers(include_self=True): + m_str = str(m) + if 'Block' in m_str: + if i > 0: + zeros_(m.temporal_fc.weight) + zeros_(m.temporal_fc.bias) + i += 1 + """Second, if provide pretrained ckpt, load it""" + if isinstance( + self.pretrained, str + ) and self.pretrained.strip() != "": # load pretrained weights + load_ckpt(self, + self.pretrained, + num_patches=self.patch_embed.num_patches, + num_seg=self.num_seg, + attention_type=self.attention_type) + elif self.pretrained is None or self.pretrained.strip() == "": + pass + else: + raise NotImplementedError + + def _init_fn(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight) + if m.bias is not None: + zeros_(m.bias) + elif isinstance(m, nn.LayerNorm): + ones_(m.weight) + zeros_(m.bias) + + def forward_features(self, x): + # B = x.shape[0] + B = paddle.shape(x)[0] + x, T, W = self.patch_embed(x) # [BT,nH*nW,F] + cls_tokens = self.cls_token.expand((B * T, -1, -1)) # [1,1,F]->[BT,1,F] + x = paddle.concat((cls_tokens, x), axis=1) + pos_interp = (x.shape[1] != self.pos_embed.shape[1]) + if pos_interp: + pos_embed = self.pos_embed + cls_pos_embed = pos_embed[0, 0, :].unsqueeze(0).unsqueeze(1) + other_pos_embed = pos_embed[0, 1:, :].unsqueeze(0).transpose( + (0, 2, 1)) + P = int(other_pos_embed.shape[2]**0.5) + H = x.shape[1] // W + other_pos_embed = other_pos_embed.reshape([1, x.shape[2], P, P]) + new_pos_embed = F.interpolate(other_pos_embed, + size=(H, W), + mode='nearest') + new_pos_embed = new_pos_embed.flatten(2) + new_pos_embed = new_pos_embed.transpose((0, 2, 1)) + new_pos_embed = paddle.concat((cls_pos_embed, new_pos_embed), + axis=1) + x = paddle.add(x, new_pos_embed) + else: + x = paddle.add(x, self.pos_embed) + + x = self.pos_drop(x) + + # Time Embeddings + if self.attention_type != 'space_only': + cls_tokens = x[:B, 0, :].unsqueeze(1) if B > 0 else x.split( + T)[0].index_select(paddle.to_tensor([0]), axis=1) + x = x[:, 1:] + _, _n, _m = x.shape + _t = T + x = x.reshape([-1, _t, _n, _m]).transpose( + (0, 2, 1, 3)).reshape([-1, _t, _m]) + # Resizing time embeddings in case they don't match + time_interp = (T != self.time_embed.shape[1]) + if time_interp: # T' != T + time_embed = self.time_embed.transpose((0, 2, 1)).unsqueeze(0) + new_time_embed = F.interpolate(time_embed, + size=(T, x.shape[-1]), + mode='nearest').squeeze(0) + new_time_embed = new_time_embed.transpose((0, 2, 1)) + x = paddle.add(x, new_time_embed) + else: + x = paddle.add(x, self.time_embed) + + x = self.time_drop(x) + _, _t, _m = x.shape + x = x.reshape([-1, W * W * T, _m]) + x = paddle.concat((cls_tokens, x), axis=1) + + # Attention blocks + for blk in self.blocks: + x = blk(x, B, T, W) + + # Predictions for space-only baseline + if self.attention_type == 'space_only': + _, _n, _m = x.shape + _t = T + x = x.reshape([-1, _t, _n, _m]) + x = paddle.mean(x, 1) # averaging predictions for every frame + + x = self.norm(x) + return x[:, 0] # [B, embed_dim] + + def forward(self, x): + x = self.forward_features(x) + return x diff --git a/paddlevideo/modeling/bbox_utils.py b/paddlevideo/modeling/bbox_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..23b4555b4f84c2cf0d4b7089a5e953acc4270c23 --- /dev/null +++ b/paddlevideo/modeling/bbox_utils.py @@ -0,0 +1,528 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import paddle +import paddle.nn.functional as F +import math +import numpy as np + + +def bbox2delta(src_boxes, tgt_boxes, weights): + src_w = src_boxes[:, 2] - src_boxes[:, 0] + src_h = src_boxes[:, 3] - src_boxes[:, 1] + src_ctr_x = src_boxes[:, 0] + 0.5 * src_w + src_ctr_y = src_boxes[:, 1] + 0.5 * src_h + + tgt_w = tgt_boxes[:, 2] - tgt_boxes[:, 0] + tgt_h = tgt_boxes[:, 3] - tgt_boxes[:, 1] + tgt_ctr_x = tgt_boxes[:, 0] + 0.5 * tgt_w + tgt_ctr_y = tgt_boxes[:, 1] + 0.5 * tgt_h + + wx, wy, ww, wh = weights + dx = wx * (tgt_ctr_x - src_ctr_x) / src_w + dy = wy * (tgt_ctr_y - src_ctr_y) / src_h + dw = ww * paddle.log(tgt_w / src_w) + dh = wh * paddle.log(tgt_h / src_h) + + deltas = paddle.stack((dx, dy, dw, dh), axis=1) + return deltas + + +def delta2bbox(deltas, boxes, weights): + clip_scale = math.log(1000.0 / 16) + + widths = boxes[:, 2] - boxes[:, 0] + heights = boxes[:, 3] - boxes[:, 1] + ctr_x = boxes[:, 0] + 0.5 * widths + ctr_y = boxes[:, 1] + 0.5 * heights + + wx, wy, ww, wh = weights + dx = deltas[:, 0::4] / wx + dy = deltas[:, 1::4] / wy + dw = deltas[:, 2::4] / ww + dh = deltas[:, 3::4] / wh + # Prevent sending too large values into paddle.exp() + dw = paddle.clip(dw, max=clip_scale) + dh = paddle.clip(dh, max=clip_scale) + + pred_ctr_x = dx * widths.unsqueeze(1) + ctr_x.unsqueeze(1) + pred_ctr_y = dy * heights.unsqueeze(1) + ctr_y.unsqueeze(1) + pred_w = paddle.exp(dw) * widths.unsqueeze(1) + pred_h = paddle.exp(dh) * heights.unsqueeze(1) + + pred_boxes = [] + pred_boxes.append(pred_ctr_x - 0.5 * pred_w) + pred_boxes.append(pred_ctr_y - 0.5 * pred_h) + pred_boxes.append(pred_ctr_x + 0.5 * pred_w) + pred_boxes.append(pred_ctr_y + 0.5 * pred_h) + pred_boxes = paddle.stack(pred_boxes, axis=-1) + + return pred_boxes + + +def expand_bbox(bboxes, scale): + w_half = (bboxes[:, 2] - bboxes[:, 0]) * .5 + h_half = (bboxes[:, 3] - bboxes[:, 1]) * .5 + x_c = (bboxes[:, 2] + bboxes[:, 0]) * .5 + y_c = (bboxes[:, 3] + bboxes[:, 1]) * .5 + + w_half *= scale + h_half *= scale + + bboxes_exp = np.zeros(bboxes.shape, dtype=np.float32) + bboxes_exp[:, 0] = x_c - w_half + bboxes_exp[:, 2] = x_c + w_half + bboxes_exp[:, 1] = y_c - h_half + bboxes_exp[:, 3] = y_c + h_half + + return bboxes_exp + + +def clip_bbox(boxes, im_shape): + h, w = im_shape[0], im_shape[1] + x1 = boxes[:, 0].clip(0, w) + y1 = boxes[:, 1].clip(0, h) + x2 = boxes[:, 2].clip(0, w) + y2 = boxes[:, 3].clip(0, h) + return paddle.stack([x1, y1, x2, y2], axis=1) + + +def nonempty_bbox(boxes, min_size=0, return_mask=False): + w = boxes[:, 2] - boxes[:, 0] + h = boxes[:, 3] - boxes[:, 1] + mask = paddle.logical_and(w > min_size, w > min_size) + if return_mask: + return mask + keep = paddle.nonzero(mask).flatten() + return keep + + +def bbox_area(boxes): + return (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) + + +def bbox_overlaps(boxes1, boxes2): + """ + Calculate overlaps between boxes1 and boxes2 + + Args: + boxes1 (Tensor): boxes with shape [M, 4] + boxes2 (Tensor): boxes with shape [N, 4] + + Return: + overlaps (Tensor): overlaps between boxes1 and boxes2 with shape [M, N] + """ + area1 = bbox_area(boxes1) + area2 = bbox_area(boxes2) + + xy_max = paddle.minimum( + paddle.unsqueeze(boxes1, 1)[:, :, 2:], boxes2[:, 2:]) + xy_min = paddle.maximum( + paddle.unsqueeze(boxes1, 1)[:, :, :2], boxes2[:, :2]) + width_height = xy_max - xy_min + width_height = width_height.clip(min=0) + inter = width_height.prod(axis=2) + + overlaps = paddle.where(inter > 0, inter / + (paddle.unsqueeze(area1, 1) + area2 - inter), + paddle.zeros_like(inter)) + return overlaps + + +def xywh2xyxy(box): + x, y, w, h = box + x1 = x - w * 0.5 + y1 = y - h * 0.5 + x2 = x + w * 0.5 + y2 = y + h * 0.5 + return [x1, y1, x2, y2] + + +def make_grid(h, w, dtype): + yv, xv = paddle.meshgrid([paddle.arange(h), paddle.arange(w)]) + return paddle.stack((xv, yv), 2).cast(dtype=dtype) + + +def decode_yolo(box, anchor, downsample_ratio): + """decode yolo box + + Args: + box (list): [x, y, w, h], all have the shape [b, na, h, w, 1] + anchor (list): anchor with the shape [na, 2] + downsample_ratio (int): downsample ratio, default 32 + scale (float): scale, default 1. + + Return: + box (list): decoded box, [x, y, w, h], all have the shape [b, na, h, w, 1] + """ + x, y, w, h = box + na, grid_h, grid_w = x.shape[1:4] + grid = make_grid(grid_h, grid_w, x.dtype).reshape((1, 1, grid_h, grid_w, 2)) + x1 = (x + grid[:, :, :, :, 0:1]) / grid_w + y1 = (y + grid[:, :, :, :, 1:2]) / grid_h + + anchor = paddle.to_tensor(anchor) + anchor = paddle.cast(anchor, x.dtype) + anchor = anchor.reshape((1, na, 1, 1, 2)) + w1 = paddle.exp(w) * anchor[:, :, :, :, 0:1] / (downsample_ratio * grid_w) + h1 = paddle.exp(h) * anchor[:, :, :, :, 1:2] / (downsample_ratio * grid_h) + + return [x1, y1, w1, h1] + + +def iou_similarity(box1, box2, eps=1e-9): + """Calculate iou of box1 and box2 + + Args: + box1 (Tensor): box with the shape [N, M1, 4] + box2 (Tensor): box with the shape [N, M2, 4] + + Return: + iou (Tensor): iou between box1 and box2 with the shape [N, M1, M2] + """ + box1 = box1.unsqueeze(2) # [N, M1, 4] -> [N, M1, 1, 4] + box2 = box2.unsqueeze(1) # [N, M2, 4] -> [N, 1, M2, 4] + px1y1, px2y2 = box1[:, :, :, 0:2], box1[:, :, :, 2:4] + gx1y1, gx2y2 = box2[:, :, :, 0:2], box2[:, :, :, 2:4] + x1y1 = paddle.maximum(px1y1, gx1y1) + x2y2 = paddle.minimum(px2y2, gx2y2) + overlap = (x2y2 - x1y1).clip(0).prod(-1) + area1 = (px2y2 - px1y1).clip(0).prod(-1) + area2 = (gx2y2 - gx1y1).clip(0).prod(-1) + union = area1 + area2 - overlap + eps + return overlap / union + + +def bbox_iou(box1, box2, giou=False, diou=False, ciou=False, eps=1e-9): + """calculate the iou of box1 and box2 + + Args: + box1 (list): [x, y, w, h], all have the shape [b, na, h, w, 1] + box2 (list): [x, y, w, h], all have the shape [b, na, h, w, 1] + giou (bool): whether use giou or not, default False + diou (bool): whether use diou or not, default False + ciou (bool): whether use ciou or not, default False + eps (float): epsilon to avoid divide by zero + + Return: + iou (Tensor): iou of box1 and box1, with the shape [b, na, h, w, 1] + """ + px1, py1, px2, py2 = box1 + gx1, gy1, gx2, gy2 = box2 + x1 = paddle.maximum(px1, gx1) + y1 = paddle.maximum(py1, gy1) + x2 = paddle.minimum(px2, gx2) + y2 = paddle.minimum(py2, gy2) + + overlap = ((x2 - x1).clip(0)) * ((y2 - y1).clip(0)) + + area1 = (px2 - px1) * (py2 - py1) + area1 = area1.clip(0) + + area2 = (gx2 - gx1) * (gy2 - gy1) + area2 = area2.clip(0) + + union = area1 + area2 - overlap + eps + iou = overlap / union + + if giou or ciou or diou: + # convex w, h + cw = paddle.maximum(px2, gx2) - paddle.minimum(px1, gx1) + ch = paddle.maximum(py2, gy2) - paddle.minimum(py1, gy1) + if giou: + c_area = cw * ch + eps + return iou - (c_area - union) / c_area + else: + # convex diagonal squared + c2 = cw**2 + ch**2 + eps + # center distance + rho2 = ((px1 + px2 - gx1 - gx2)**2 + (py1 + py2 - gy1 - gy2)**2) / 4 + if diou: + return iou - rho2 / c2 + else: + w1, h1 = px2 - px1, py2 - py1 + eps + w2, h2 = gx2 - gx1, gy2 - gy1 + eps + delta = paddle.atan(w1 / h1) - paddle.atan(w2 / h2) + v = (4 / math.pi**2) * paddle.pow(delta, 2) + alpha = v / (1 + eps - iou + v) + alpha.stop_gradient = True + return iou - (rho2 / c2 + v * alpha) + else: + return iou + + +def rect2rbox(bboxes): + """ + :param bboxes: shape (n, 4) (xmin, ymin, xmax, ymax) + :return: dbboxes: shape (n, 5) (x_ctr, y_ctr, w, h, angle) + """ + bboxes = bboxes.reshape(-1, 4) + num_boxes = bboxes.shape[0] + + x_ctr = (bboxes[:, 2] + bboxes[:, 0]) / 2.0 + y_ctr = (bboxes[:, 3] + bboxes[:, 1]) / 2.0 + edges1 = np.abs(bboxes[:, 2] - bboxes[:, 0]) + edges2 = np.abs(bboxes[:, 3] - bboxes[:, 1]) + angles = np.zeros([num_boxes], dtype=bboxes.dtype) + + inds = edges1 < edges2 + + rboxes = np.stack((x_ctr, y_ctr, edges1, edges2, angles), axis=1) + rboxes[inds, 2] = edges2[inds] + rboxes[inds, 3] = edges1[inds] + rboxes[inds, 4] = np.pi / 2.0 + return rboxes + + +def delta2rbox(Rrois, + deltas, + means=[0, 0, 0, 0, 0], + stds=[1, 1, 1, 1, 1], + wh_ratio_clip=1e-6): + """ + :param Rrois: (cx, cy, w, h, theta) + :param deltas: (dx, dy, dw, dh, dtheta) + :param means: + :param stds: + :param wh_ratio_clip: + :return: + """ + means = paddle.to_tensor(means) + stds = paddle.to_tensor(stds) + deltas = paddle.reshape(deltas, [-1, deltas.shape[-1]]) + denorm_deltas = deltas * stds + means + + dx = denorm_deltas[:, 0] + dy = denorm_deltas[:, 1] + dw = denorm_deltas[:, 2] + dh = denorm_deltas[:, 3] + dangle = denorm_deltas[:, 4] + + max_ratio = np.abs(np.log(wh_ratio_clip)) + dw = paddle.clip(dw, min=-max_ratio, max=max_ratio) + dh = paddle.clip(dh, min=-max_ratio, max=max_ratio) + + Rroi_x = Rrois[:, 0] + Rroi_y = Rrois[:, 1] + Rroi_w = Rrois[:, 2] + Rroi_h = Rrois[:, 3] + Rroi_angle = Rrois[:, 4] + + gx = dx * Rroi_w * paddle.cos(Rroi_angle) - dy * Rroi_h * paddle.sin( + Rroi_angle) + Rroi_x + gy = dx * Rroi_w * paddle.sin(Rroi_angle) + dy * Rroi_h * paddle.cos( + Rroi_angle) + Rroi_y + gw = Rroi_w * dw.exp() + gh = Rroi_h * dh.exp() + ga = np.pi * dangle + Rroi_angle + ga = (ga + np.pi / 4) % np.pi - np.pi / 4 + ga = paddle.to_tensor(ga) + + gw = paddle.to_tensor(gw, dtype='float32') + gh = paddle.to_tensor(gh, dtype='float32') + bboxes = paddle.stack([gx, gy, gw, gh, ga], axis=-1) + return bboxes + + +def rbox2delta(proposals, gt, means=[0, 0, 0, 0, 0], stds=[1, 1, 1, 1, 1]): + """ + + Args: + proposals: + gt: + means: 1x5 + stds: 1x5 + + Returns: + + """ + proposals = proposals.astype(np.float64) + + PI = np.pi + + gt_widths = gt[..., 2] + gt_heights = gt[..., 3] + gt_angle = gt[..., 4] + + proposals_widths = proposals[..., 2] + proposals_heights = proposals[..., 3] + proposals_angle = proposals[..., 4] + + coord = gt[..., 0:2] - proposals[..., 0:2] + dx = (np.cos(proposals[..., 4]) * coord[..., 0] + np.sin(proposals[..., 4]) + * coord[..., 1]) / proposals_widths + dy = (-np.sin(proposals[..., 4]) * coord[..., 0] + np.cos(proposals[..., 4]) + * coord[..., 1]) / proposals_heights + dw = np.log(gt_widths / proposals_widths) + dh = np.log(gt_heights / proposals_heights) + da = (gt_angle - proposals_angle) + + da = (da + PI / 4) % PI - PI / 4 + da /= PI + + deltas = np.stack([dx, dy, dw, dh, da], axis=-1) + means = np.array(means, dtype=deltas.dtype) + stds = np.array(stds, dtype=deltas.dtype) + deltas = (deltas - means) / stds + deltas = deltas.astype(np.float32) + return deltas + + +def bbox_decode(bbox_preds, + anchors, + means=[0, 0, 0, 0, 0], + stds=[1, 1, 1, 1, 1]): + """decode bbox from deltas + Args: + bbox_preds: [N,H,W,5] + anchors: [H*W,5] + return: + bboxes: [N,H,W,5] + """ + means = paddle.to_tensor(means) + stds = paddle.to_tensor(stds) + num_imgs, H, W, _ = bbox_preds.shape + bboxes_list = [] + for img_id in range(num_imgs): + bbox_pred = bbox_preds[img_id] + # bbox_pred.shape=[5,H,W] + bbox_delta = bbox_pred + anchors = paddle.to_tensor(anchors) + bboxes = delta2rbox( + anchors, bbox_delta, means, stds, wh_ratio_clip=1e-6) + bboxes = paddle.reshape(bboxes, [H, W, 5]) + bboxes_list.append(bboxes) + return paddle.stack(bboxes_list, axis=0) + + +def poly_to_rbox(polys): + """ + poly:[x0,y0,x1,y1,x2,y2,x3,y3] + to + rotated_boxes:[x_ctr,y_ctr,w,h,angle] + """ + rotated_boxes = [] + for poly in polys: + poly = np.array(poly[:8], dtype=np.float32) + + pt1 = (poly[0], poly[1]) + pt2 = (poly[2], poly[3]) + pt3 = (poly[4], poly[5]) + pt4 = (poly[6], poly[7]) + + edge1 = np.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[ + 1]) * (pt1[1] - pt2[1])) + edge2 = np.sqrt((pt2[0] - pt3[0]) * (pt2[0] - pt3[0]) + (pt2[1] - pt3[ + 1]) * (pt2[1] - pt3[1])) + + width = max(edge1, edge2) + height = min(edge1, edge2) + + rbox_angle = 0 + if edge1 > edge2: + rbox_angle = np.arctan2( + np.float(pt2[1] - pt1[1]), np.float(pt2[0] - pt1[0])) + elif edge2 >= edge1: + rbox_angle = np.arctan2( + np.float(pt4[1] - pt1[1]), np.float(pt4[0] - pt1[0])) + + def norm_angle(angle, range=[-np.pi / 4, np.pi]): + return (angle - range[0]) % range[1] + range[0] + + rbox_angle = norm_angle(rbox_angle) + + x_ctr = np.float(pt1[0] + pt3[0]) / 2 + y_ctr = np.float(pt1[1] + pt3[1]) / 2 + rotated_box = np.array([x_ctr, y_ctr, width, height, rbox_angle]) + rotated_boxes.append(rotated_box) + ret_rotated_boxes = np.array(rotated_boxes) + assert ret_rotated_boxes.shape[1] == 5 + return ret_rotated_boxes + + +def cal_line_length(point1, point2): + import math + return math.sqrt( + math.pow(point1[0] - point2[0], 2) + math.pow(point1[1] - point2[1], 2)) + + +def get_best_begin_point_single(coordinate): + x1, y1, x2, y2, x3, y3, x4, y4 = coordinate + xmin = min(x1, x2, x3, x4) + ymin = min(y1, y2, y3, y4) + xmax = max(x1, x2, x3, x4) + ymax = max(y1, y2, y3, y4) + combinate = [[[x1, y1], [x2, y2], [x3, y3], [x4, y4]], + [[x4, y4], [x1, y1], [x2, y2], [x3, y3]], + [[x3, y3], [x4, y4], [x1, y1], [x2, y2]], + [[x2, y2], [x3, y3], [x4, y4], [x1, y1]]] + dst_coordinate = [[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]] + force = 100000000.0 + force_flag = 0 + for i in range(4): + temp_force = cal_line_length(combinate[i][0], dst_coordinate[0]) \ + + cal_line_length(combinate[i][1], dst_coordinate[1]) \ + + cal_line_length(combinate[i][2], dst_coordinate[2]) \ + + cal_line_length(combinate[i][3], dst_coordinate[3]) + if temp_force < force: + force = temp_force + force_flag = i + if force_flag != 0: + pass + return np.array(combinate[force_flag]).reshape(8) + + +def rbox2poly_single(rrect): + """ + rrect:[x_ctr,y_ctr,w,h,angle] + to + poly:[x0,y0,x1,y1,x2,y2,x3,y3] + """ + x_ctr, y_ctr, width, height, angle = rrect[:5] + tl_x, tl_y, br_x, br_y = -width / 2, -height / 2, width / 2, height / 2 + # rect 2x4 + rect = np.array([[tl_x, br_x, br_x, tl_x], [tl_y, tl_y, br_y, br_y]]) + R = np.array([[np.cos(angle), -np.sin(angle)], + [np.sin(angle), np.cos(angle)]]) + # poly + poly = R.dot(rect) + x0, x1, x2, x3 = poly[0, :4] + x_ctr + y0, y1, y2, y3 = poly[1, :4] + y_ctr + poly = np.array([x0, y0, x1, y1, x2, y2, x3, y3], dtype=np.float32) + poly = get_best_begin_point_single(poly) + return poly + + +def rbox2poly(rrects): + """ + rrect:[x_ctr,y_ctr,w,h,angle] + to + poly:[x0,y0,x1,y1,x2,y2,x3,y3] + """ + polys = [] + for rrect in rrects: + x_ctr, y_ctr, width, height, angle = rrect[:5] + tl_x, tl_y, br_x, br_y = -width / 2, -height / 2, width / 2, height / 2 + rect = np.array([[tl_x, br_x, br_x, tl_x], [tl_y, tl_y, br_y, br_y]]) + R = np.array([[np.cos(angle), -np.sin(angle)], + [np.sin(angle), np.cos(angle)]]) + poly = R.dot(rect) + x0, x1, x2, x3 = poly[0, :4] + x_ctr + y0, y1, y2, y3 = poly[1, :4] + y_ctr + poly = np.array([x0, y0, x1, y1, x2, y2, x3, y3], dtype=np.float32) + poly = get_best_begin_point_single(poly) + polys.append(poly) + polys = np.array(polys) + return polys diff --git a/paddlevideo/modeling/builder.py b/paddlevideo/modeling/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..71503eb4d52541a89e840d67a104476a59abfeff --- /dev/null +++ b/paddlevideo/modeling/builder.py @@ -0,0 +1,127 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .registry import BACKBONES, HEADS, LOSSES, RECOGNIZERS, LOCALIZERS, ROI_EXTRACTORS, DETECTORS, BBOX_ASSIGNERS, BBOX_SAMPLERS, BBOX_CODERS, PARTITIONERS, MULTIMODAL, SEGMENT, SEGMENTERS +from ..utils import build +from .registry import (BACKBONES, BBOX_ASSIGNERS, BBOX_CODERS, BBOX_SAMPLERS, + DETECTORS, ESTIMATORS, HEADS, LOCALIZERS, LOSSES, + MULTIMODAL, PARTITIONERS, RECOGNIZERS, ROI_EXTRACTORS) + + +def build_backbone(cfg): + """Build backbone.""" + return build(cfg, BACKBONES) + + +def build_roi_extractor(cfg): + """Build roi extractor.""" + return build(cfg, ROI_EXTRACTORS) + + +def build_assigner(cfg, **default_args): + """Builder of box assigner.""" + return build(cfg, BBOX_ASSIGNERS) + + +def build_sampler(cfg, **default_args): + """Builder of box sampler.""" + return build(cfg, BBOX_SAMPLERS) + + +def build_roi_extractor(cfg): + """Build roi extractor.""" + return build(cfg, ROI_EXTRACTORS) + + +def build_assigner(cfg, **default_args): + """Builder of box assigner.""" + return build(cfg, BBOX_ASSIGNERS) + + +def build_sampler(cfg, **default_args): + """Builder of box sampler.""" + return build(cfg, BBOX_SAMPLERS) + + +def build_head(cfg): + """Build head.""" + return build(cfg, HEADS) + + +def build_loss(cfg): + """Build loss.""" + return build(cfg, LOSSES) + + +def build_recognizer(cfg): + """Build recognizer.""" + return build(cfg, RECOGNIZERS, key='framework') + + +def build_segmenter(cfg): + """Build segmenter.""" + return build(cfg, SEGMENTERS, key='framework') + + +def build_localizer(cfg): + """Build localizer.""" + return build(cfg, LOCALIZERS, key='framework') + + +def build_detector(cfg, train_cfg=None, test_cfg=None): + """Build detector.""" + return build(cfg, DETECTORS, key='framework') + + +def build_partitioner(cfg): + """Build partitioner.""" + return build(cfg, PARTITIONERS, key='framework') + + +def build_estimator(cfg): + """Build estimator.""" + return build(cfg, ESTIMATORS, key='framework') + + +def build_multimodal(cfg): + """Build multimodal.""" + return build(cfg, MULTIMODAL, key='framework') + + +def build_segment(cfg): + """Build segment.""" + return build(cfg, SEGMENT, key='framework') + + +def build_model(cfg): + cfg_copy = cfg.copy() + framework_type = cfg_copy.get('framework') + if framework_type in RECOGNIZERS: + return build_recognizer(cfg) + elif framework_type in LOCALIZERS: + return build_localizer(cfg) + elif framework_type in PARTITIONERS: + return build_partitioner(cfg) + elif framework_type in DETECTORS: + return build_detector(cfg) + elif framework_type in ESTIMATORS: + return build_estimator(cfg) + elif framework_type in MULTIMODAL: + return build_multimodal(cfg) + elif framework_type in SEGMENTERS: + return build_segmenter(cfg) + elif framework_type in SEGMENT: + return build_segment(cfg) + else: + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/__init__.py b/paddlevideo/modeling/framework/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d68fe09ac47d3160db6238279352bd1e4ed10dc4 --- /dev/null +++ b/paddlevideo/modeling/framework/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .estimators import BaseEstimator, DepthEstimator +from .localizers import BaseLocalizer, BMNLocalizer +from .partitioners import BasePartitioner, TransNetV2Partitioner +from .recognizers import BaseRecognizer, Recognizer2D +from .multimodal import ActBert, BaseMultimodal +from .segment import BaseSegment, CFBI +from .segmenters import MSTCN + +__all__ = [ + 'BaseRecognizer', 'Recognizer2D', 'BaseLocalizer', 'BMNLocalizer', + 'BasePartitioner', 'TransNetV2Partitioner', 'BaseEstimator', + 'DepthEstimator', 'BaseMultimodal', 'ActBert', 'BaseSegment', 'CFBI', + 'MSTCN' +] diff --git a/paddlevideo/modeling/framework/detectors/__init__.py b/paddlevideo/modeling/framework/detectors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..74dcac0a36e852ef331c733e543342c9e22b752d --- /dev/null +++ b/paddlevideo/modeling/framework/detectors/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from .base import BaseDetector +from .fast_rcnn import FastRCNN +from .two_stage import TwoStageDetector + +__all__ = ['BaseDetector', 'TwoStageDetector', 'FastRCNN'] diff --git a/paddlevideo/modeling/framework/detectors/base.py b/paddlevideo/modeling/framework/detectors/base.py new file mode 100644 index 0000000000000000000000000000000000000000..4d5ccb8feca0d8729e125aa6c8b13d076c742fb8 --- /dev/null +++ b/paddlevideo/modeling/framework/detectors/base.py @@ -0,0 +1,51 @@ +from abc import abstractmethod +from ... import builder +import paddle.nn as nn +from ...registry import DETECTORS + +@DETECTORS.register() +class BaseDetector(nn.Layer): + """Base class for detectors. """ + def __init__(self, backbone=None, head=None): + + super().__init__() + + def init_weights(self): + """Initialize the model network weights. """ + self.backbone.init_weights() + self.head.init_weights() + + def extract_feature(self, imgs, iter_num): + """Extract features through a backbone. """ + feature = self.backbone(imgs) + return feature + + def forward(self, data_batch, mode='infer'): + if mode == 'train': + return self.train_step(data_batch) + elif mode == 'valid': + return self.val_step(data_batch) + elif mode == 'test': + return self.test_step(data_batch) + elif mode == 'infer': + return self.infer_step(data_batch) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating step. + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Test step. + """ + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/detectors/fast_rcnn.py b/paddlevideo/modeling/framework/detectors/fast_rcnn.py new file mode 100644 index 0000000000000000000000000000000000000000..e8f912dbea0f3a1f5f1a4d1157f1d3ae01793afb --- /dev/null +++ b/paddlevideo/modeling/framework/detectors/fast_rcnn.py @@ -0,0 +1,34 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .two_stage import TwoStageDetector +from ...registry import DETECTORS + +@DETECTORS.register() +class FastRCNN(TwoStageDetector): + + def __init__(self, + backbone, + head=None, + train_cfg=None, + test_cfg=None, + neck=None, + pretrained=None): + super(FastRCNN, self).__init__( + backbone=backbone, + neck=neck, + roi_head=head, + train_cfg=train_cfg, + test_cfg=test_cfg, + pretrained=pretrained) diff --git a/paddlevideo/modeling/framework/detectors/two_stage.py b/paddlevideo/modeling/framework/detectors/two_stage.py new file mode 100644 index 0000000000000000000000000000000000000000..f9deb1d0fdd70131c17d05cda147ca6b7bdcc15a --- /dev/null +++ b/paddlevideo/modeling/framework/detectors/two_stage.py @@ -0,0 +1,186 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +from ... import builder +import paddle.distributed as dist +from ...registry import DETECTORS +from .base import BaseDetector + + +@DETECTORS.register() +class TwoStageDetector(BaseDetector): + """Base class for two-stage detectors. """ + + def __init__(self, + backbone, + neck=None, + rpn_head=None, + roi_head=None, + train_cfg=None, + test_cfg=None, + pretrained=None): + super(TwoStageDetector, self).__init__() + self.backbone = builder.build_backbone(backbone) + + if neck is not None: + self.neck = neck # useless + + if rpn_head is not None: + rpn_train_cfg = train_cfg.rpn if train_cfg is not None else None + rpn_head_ = rpn_head.copy() + rpn_head_.update(train_cfg=rpn_train_cfg, test_cfg=test_cfg.rpn) + self.rpn_head = builder.build_head(rpn_head_) + + if roi_head is not None: + self.roi_head = builder.build_head(roi_head) + + self.train_cfg = train_cfg + self.test_cfg = test_cfg + + if pretrained is not None: + self.init_weights(pretrained=pretrained) + + @property + def with_rpn(self): + """whether the detector has RPN""" + return hasattr(self, 'rpn_head') and self.rpn_head is not None + + @property + def with_roi_head(self): + """whether the detector has a RoI head""" + return hasattr(self, 'roi_head') and self.roi_head is not None + + def init_weights(self, pretrained=None): + """Initialize the weights in detector. """ + super(TwoStageDetector, self).init_weights(pretrained) + self.backbone.init_weights(pretrained=pretrained) + if self.with_rpn: + self.rpn_head.init_weights() + if self.with_roi_head: + self.roi_head.init_weights(pretrained) + + def extract_feat(self, img): + """Directly extract features from the backbone.""" + x = self.backbone(img) + return x + + def train_step(self, data, **kwargs): + img_slow = data[0] + img_fast = data[1] + proposals, gt_bboxes, gt_labels, scores, entity_ids = self.get_unpad_datas( + data) + img_shape = data[7] + img_idx = data[8] + img_metas = scores, entity_ids + x = self.extract_feat(img=[img_slow, img_fast]) + roi_losses = self.roi_head.train_step(x, img_metas, proposals, + gt_bboxes, gt_labels, **kwargs) + losses = dict() + losses.update(roi_losses) + + return losses + + def val_step(self, data, rescale=False): + img_slow = data[0] + img_fast = data[1] + proposals, gt_bboxes, gt_labels, scores, entity_ids = self.get_unpad_datas( + data) + img_shape = data[7] + img_metas = scores, entity_ids + x = self.extract_feat(img=[img_slow, img_fast]) + + return self.roi_head.simple_test(x, + proposals[0], + img_shape, + rescale=rescale) + + def test_step(self, data, rescale=False): + return self.val_step(data, rescale) + + def infer_step(self, data, rescale=False): + ''' model inference''' + + img_slow = data[0] + img_fast = data[1] + proposals = data[2] + img_shape = data[3] + + # using slowfast model to extract spatio-temporal features + x = self.extract_feat(img=[img_slow, img_fast]) + + ret = self.roi_head.simple_test(x, + proposals[0], + img_shape, + rescale=rescale) + return ret + + def get_unpad_datas(self, data): + ''' get original datas padded in dataset ''' + pad_proposals = data[2] + pad_gt_bboxes = data[3] + pad_gt_labels = data[4] + pad_scores, pad_entity_ids = data[5], data[6] + len_proposals = data[9] + len_gt_bboxes = data[10] + len_gt_labels = data[11] + len_scores = data[12] + len_entity_ids = data[13] + N = pad_proposals.shape[0] + proposals = [] + gt_bboxes = [] + gt_labels = [] + scores = [] + entity_ids = [] + for bi in range(N): + pad_proposal = pad_proposals[bi] + len_proposal = len_proposals[bi] + index_proposal = paddle.arange(len_proposal) + proposal = paddle.index_select(x=pad_proposal, + index=index_proposal, + axis=0) + proposals.append(proposal) + + pad_gt_bbox = pad_gt_bboxes[bi] + len_gt_bbox = len_gt_bboxes[bi] + index_gt_bbox = paddle.arange(len_gt_bbox) + gt_bbox = paddle.index_select(x=pad_gt_bbox, + index=index_gt_bbox, + axis=0) + gt_bboxes.append(gt_bbox) + + pad_gt_label = pad_gt_labels[bi] + len_gt_label = len_gt_labels[bi] + index_gt_label = paddle.arange(len_gt_label) + gt_label = paddle.index_select(x=pad_gt_label, + index=index_gt_label, + axis=0) + gt_labels.append(gt_label) + + pad_score = pad_scores[bi] + len_score = len_scores[bi] + index_score = paddle.arange(len_score) + score = paddle.index_select(x=pad_score, index=index_score, axis=0) + scores.append(score) + + pad_entity_id = pad_entity_ids[bi] + len_entity_id = len_entity_ids[bi] + index_entity_id = paddle.arange(len_entity_id) + entity_id = paddle.index_select(x=pad_entity_id, + index=index_entity_id, + axis=0) + entity_ids.append(entity_id) + + return proposals, gt_bboxes, gt_labels, scores, entity_ids diff --git a/paddlevideo/modeling/framework/estimators/__init__.py b/paddlevideo/modeling/framework/estimators/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e2bda935c2d8aff40f26e34e1f128ee6dafa7be1 --- /dev/null +++ b/paddlevideo/modeling/framework/estimators/__init__.py @@ -0,0 +1,4 @@ +from .base import BaseEstimator +from .depth_estimator import DepthEstimator + +__all__ = ['DepthEstimator', 'BaseEstimator'] diff --git a/paddlevideo/modeling/framework/estimators/base.py b/paddlevideo/modeling/framework/estimators/base.py new file mode 100644 index 0000000000000000000000000000000000000000..cdddd674fbabf9bba136e6421770e385dfccb656 --- /dev/null +++ b/paddlevideo/modeling/framework/estimators/base.py @@ -0,0 +1,82 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from abc import abstractmethod + +import paddle +import paddle.nn as nn +from paddlevideo.modeling.registry import ESTIMATORS +from paddlevideo.utils import get_logger + +from ... import builder + +logger = get_logger("paddlevideo") + + +@ESTIMATORS.register() +class BaseEstimator(nn.Layer): + """BaseEstimator + + """ + def __init__(self, backbone=None, head=None): + super().__init__() + if backbone is not None: + self.backbone = builder.build_backbone(backbone) + if hasattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + self.backbone = None + + if head is not None: + self.head_name = head.name + self.head = builder.build_head(head) + if hasattr(self.head, 'init_weights'): + self.head.init_weights() + else: + self.head = None + + def forward(self, data_batch, mode='infer'): + """ + 1. Define how the model is going to run, from input to output. + 2. Console of train, valid, test or infer step + """ + if mode == 'train': + return self.train_step(data_batch) + elif mode == 'valid': + return self.val_step(data_batch) + elif mode == 'test': + return self.test_step(data_batch) + elif mode == 'infer': + return self.infer_step(data_batch) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch): + """Define how the model is going to train, from input to output. + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch): + """Define how the model is going to valid, from input to output.""" + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + raise NotImplementedError + + @abstractmethod + def infer_step(self, data_batch): + """Define how the model is going to infer, from input to output.""" + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/estimators/depth_estimator.py b/paddlevideo/modeling/framework/estimators/depth_estimator.py new file mode 100644 index 0000000000000000000000000000000000000000..13ee877756b02e16a9dda231d66dcf0b1af532e7 --- /dev/null +++ b/paddlevideo/modeling/framework/estimators/depth_estimator.py @@ -0,0 +1,59 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import paddle +from paddlevideo.modeling.framework.estimators.base import BaseEstimator +from paddlevideo.modeling.registry import ESTIMATORS +from paddlevideo.utils import get_logger + +from ... import builder + +logger = get_logger("paddlevideo") + + +@ESTIMATORS.register() +class DepthEstimator(BaseEstimator): + """DepthEstimator + """ + def forward_net(self, inputs, day_or_night='day_and_night'): + if self.backbone is not None: + outputs = self.backbone(inputs, day_or_night) + else: + outputs = inputs + return outputs + + def train_step(self, data_batch): + """Define how the model is going to train, from input to output. + """ + inputs, _ = data_batch + outputs = self.forward_net(inputs, day_or_night='day_and_night') + loss_metrics = self.head.loss(inputs, outputs) + return loss_metrics + + def val_step(self, data_batch): + inputs, day_or_night = data_batch + outputs = self.forward_net(inputs, day_or_night=day_or_night) + loss_metrics = self.head.loss(inputs, outputs) + return loss_metrics + + def test_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + inputs, day_or_night = data_batch + outputs = self.forward_net(inputs, day_or_night=day_or_night) + loss_metrics = self.head.loss(inputs, outputs) + return loss_metrics + + def infer_step(self, data_batch): + """Define how the model is going to infer, from input to output.""" + inputs = data_batch[0] + outputs = self.forward_net(inputs, day_or_night='day') + return outputs diff --git a/paddlevideo/modeling/framework/localizers/__init__.py b/paddlevideo/modeling/framework/localizers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..52405444deabe42792d30d2a888b38005ee8f410 --- /dev/null +++ b/paddlevideo/modeling/framework/localizers/__init__.py @@ -0,0 +1,18 @@ +# copyright (c) 2020 paddlepaddle authors. all rights reserved. +# +# licensed under the apache license, version 2.0 (the "license" +# you may not use this file except in compliance with the license. +# you may obtain a copy of the license at +# +# http://www.apache.org/licenses/license-2.0 +# +# unless required by applicable law or agreed to in writing, software +# distributed under the license is distributed on an "as is" basis, +# without warranties or conditions of any kind, either express or implied. +# see the license for the specific language governing permissions and +# limitations under the license. + +from .base import BaseLocalizer +from .bmn_localizer import BMNLocalizer + +__all__ = ['BaseLocalizer', 'BMNLocalizer'] diff --git a/paddlevideo/modeling/framework/localizers/base.py b/paddlevideo/modeling/framework/localizers/base.py new file mode 100644 index 0000000000000000000000000000000000000000..cfd2869f6da32ba35224fe36b347f072523d2587 --- /dev/null +++ b/paddlevideo/modeling/framework/localizers/base.py @@ -0,0 +1,74 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod +import paddle.nn as nn +from ... import builder + + +class BaseLocalizer(nn.Layer): + """Base class for Localization. + All localizer should subclass it. + All subclass should overwrite: + - Methods:``train_step``, define your train step. + - Methods:``valid_step``, define your valid step, always the same as train_step. + - Methods:``test_step``, define your test step. + """ + def __init__(self, backbone, loss): + super().__init__() + self.backbone = builder.build_backbone(backbone) + self.loss = builder.build_loss(loss) + self.init_weights() + + def init_weights(self): + """Initialize the model network weights. """ + if getattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + pass + + def forward(self, data_batch, mode='infer'): + """ + 1. Define how the model is going to run, from input to output. + 2. Console of train, valid, test or infer step + 3. Set mode='infer' is used for saving inference model, refer to tools/export_model.py + """ + if mode == 'train': + return self.train_step(data_batch) + elif mode == 'valid': + return self.val_step(data_batch) + elif mode == 'test': + return self.test_step(data_batch) + elif mode == 'infer': + return self.infer_step(data_batch) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. input_data_batch -> loss_metric + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating setp. input_data_batch -> loss_metric + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Tets setp. to get acc in test data. input_data_batch -> output + """ + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/localizers/bmn_localizer.py b/paddlevideo/modeling/framework/localizers/bmn_localizer.py new file mode 100644 index 0000000000000000000000000000000000000000..5afbd3a0c1b635a299d3276cce59882aa2b0bf54 --- /dev/null +++ b/paddlevideo/modeling/framework/localizers/bmn_localizer.py @@ -0,0 +1,69 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import LOCALIZERS +from .base import BaseLocalizer + +import paddle + + +@LOCALIZERS.register() +class BMNLocalizer(BaseLocalizer): + """BMN Localization framework + """ + def forward_net(self, imgs): + """Call backbone forward. + """ + preds = self.backbone(imgs) + return preds + + def train_step(self, data_batch): + """Training step. + """ + x_data = data_batch[0] + gt_iou_map = data_batch[1] + gt_start = data_batch[2] + gt_end = data_batch[3] + gt_iou_map.stop_gradient = True + gt_start.stop_gradient = True + gt_end.stop_gradient = True + + # call Model forward + pred_bm, pred_start, pred_end = self.forward_net(x_data) + # call Loss forward + loss = self.loss(pred_bm, pred_start, pred_end, gt_iou_map, gt_start, + gt_end) + avg_loss = paddle.mean(loss) + loss_metrics = dict() + loss_metrics['loss'] = avg_loss + return loss_metrics + + def val_step(self, data_batch): + """Validating setp. + """ + return self.train_step(data_batch) + + def test_step(self, data_batch): + """Test step. + """ + x_data = data_batch[0] + pred_bm, pred_start, pred_end = self.forward_net(x_data) + return pred_bm, pred_start, pred_end + + def infer_step(self, data_batch): + """Infer step + """ + x_data = data_batch[0] + + # call Model forward + pred_bm, pred_start, pred_end = self.forward_net(x_data) + return pred_bm, pred_start, pred_end diff --git a/paddlevideo/modeling/framework/multimodal/__init__.py b/paddlevideo/modeling/framework/multimodal/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e1efec3d776857c8a09493ac1a2ee6e151e71434 --- /dev/null +++ b/paddlevideo/modeling/framework/multimodal/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from .base import BaseMultimodal +from .actbert import ActBert + +__all__ = ['BaseMultimodal', 'ActBert'] diff --git a/paddlevideo/modeling/framework/multimodal/actbert.py b/paddlevideo/modeling/framework/multimodal/actbert.py new file mode 100644 index 0000000000000000000000000000000000000000..4f2c074ff1b27b58ec70be3b3de1b532078792da --- /dev/null +++ b/paddlevideo/modeling/framework/multimodal/actbert.py @@ -0,0 +1,64 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import MULTIMODAL +from .base import BaseMultimodal +import paddle +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@MULTIMODAL.register() +class ActBert(BaseMultimodal): + """ActBert model framework.""" + def forward_net(self, text_ids, action_feat, image_feat, image_loc, + token_type_ids, text_mask, image_mask, action_mask): + pred = self.backbone(text_ids, action_feat, image_feat, image_loc, + token_type_ids, text_mask, image_mask, action_mask) + return pred + + def train_step(self, data_batch): + """For ActBert Dataset. Define how the model is going to train, from input to output. + """ + text_ids, action_feat, image_feat, image_loc, \ + token_type_ids, text_mask, image_mask, action_mask, \ + text_labels, action_label, next_sentence_label, image_label, image_target = data_batch + loss_metrics = dict() + pred = self.backbone(text_ids, action_feat, image_feat, image_loc, + token_type_ids, text_mask, image_mask, action_mask) + prediction_scores_t, prediction_scores_v, prediction_scores_a, seq_relationship_score = pred + total_loss = self.loss(prediction_scores_t, prediction_scores_v, prediction_scores_a, seq_relationship_score, \ + text_labels, image_label, image_target, action_label, next_sentence_label) + loss_metrics['loss'] = paddle.mean(total_loss) + return loss_metrics + + def val_step(self, data_batch): + """For ActBert Dataset. Define how the model is going to val, from input to output. + """ + return self.train_step(data_batch) + + def test_step(self, data_batch): + """For MSR-VTT Dataset. Define how the model is going to test, from input to output.""" + text_ids, action_feat, image_feat, image_loc, token_type_ids, text_mask, image_mask, action_mask = data_batch[: + -1] + action_feat = action_feat.squeeze(0) + image_feat = image_feat.squeeze(0) + image_loc = image_loc.squeeze(0) + image_mask = image_mask.squeeze(0) + action_mask = action_mask.squeeze(0) + prediction_scores_t, prediction_scores_v, prediction_scores_a, seq_relationship_score = self.forward_net(text_ids, \ + action_feat, image_feat, image_loc, token_type_ids, text_mask, image_mask, action_mask) + return prediction_scores_t, prediction_scores_v, prediction_scores_a, seq_relationship_score + + def infer_step(self, data_batch): + pass diff --git a/paddlevideo/modeling/framework/multimodal/base.py b/paddlevideo/modeling/framework/multimodal/base.py new file mode 100644 index 0000000000000000000000000000000000000000..bc57f9765bf247272c15c932eb319efff6c73566 --- /dev/null +++ b/paddlevideo/modeling/framework/multimodal/base.py @@ -0,0 +1,81 @@ +from abc import abstractmethod +from ... import builder +import paddle.nn as nn + + +class BaseMultimodal(nn.Layer): + """Base class for Multimodal. + + All Multimodal model should subclass it. + All subclass should overwrite: + + - Methods:``train_step``, supporting to forward when training. + - Methods:``valid_step``, supporting to forward when validating. + - Methods:``test_step``, supporting to forward when testing. + + Args: + backbone (dict): Backbone modules to extract feature. + head (dict): Head to process feature. + loss(dict): Loss function. + + """ + def __init__(self, backbone=None, head=None, loss=None): + super().__init__() + if backbone is not None: + self.backbone = builder.build_backbone(backbone) + if hasattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + self.backbone = None + if head is not None: + self.head_name = head.name + self.head = builder.build_head(head) + if hasattr(self.head, 'init_weights'): + self.head.init_weights() + else: + self.head = None + if loss is not None: + self.loss = builder.build_loss(loss) + else: + self.loss = None + + def forward(self, data_batch, mode='infer'): + """ + 1. Define how the model is going to run, from input to output. + 2. Console of train, valid, test or infer step + 3. Set mode='infer' is used for saving inference model, refer to tools/export_model.py + """ + if mode == 'train': + return self.train_step(data_batch) + elif mode == 'valid': + return self.val_step(data_batch) + elif mode == 'test': + return self.test_step(data_batch) + elif mode == 'infer': + return self.infer_step(data_batch) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating step. + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Test step. + """ + raise NotImplementedError + + @abstractmethod + def infer_step(self, data_batch, **kwargs): + """Infer step. + """ + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/partitioners/__init__.py b/paddlevideo/modeling/framework/partitioners/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0c6de50a38db12ccb166644ee82778a2a9dbebf4 --- /dev/null +++ b/paddlevideo/modeling/framework/partitioners/__init__.py @@ -0,0 +1,18 @@ +# copyright (c) 2020 paddlepaddle authors. all rights reserved. +# +# licensed under the apache license, version 2.0 (the "license" +# you may not use this file except in compliance with the license. +# you may obtain a copy of the license at +# +# http://www.apache.org/licenses/license-2.0 +# +# unless required by applicable law or agreed to in writing, software +# distributed under the license is distributed on an "as is" basis, +# without warranties or conditions of any kind, either express or implied. +# see the license for the specific language governing permissions and +# limitations under the license. + +from .base import BasePartitioner +from .transnetv2_partitioner import TransNetV2Partitioner + +__all__ = ['BasePartitioner', 'TransNetV2Partitioner'] diff --git a/paddlevideo/modeling/framework/partitioners/base.py b/paddlevideo/modeling/framework/partitioners/base.py new file mode 100644 index 0000000000000000000000000000000000000000..a7c925975a9343a69d061d30cd4e7730f68db3f2 --- /dev/null +++ b/paddlevideo/modeling/framework/partitioners/base.py @@ -0,0 +1,84 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod +import paddle.nn as nn +from ... import builder + + +class BasePartitioner(nn.Layer): + """Base class for Partition. + All partitioner should subclass it. + All subclass should overwrite: + - Methods:``train_step``, define your train step. + - Methods:``valid_step``, define your valid step, always the same as train_step. + - Methods:``test_step``, define your test step. + """ + def __init__(self, backbone=None, head=None): + super().__init__() + if backbone is not None: + self.backbone = builder.build_backbone(backbone) + if hasattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + self.backbone = None + if head is not None: + self.head_name = head.name + self.head = builder.build_head(head) + if hasattr(self.head, 'init_weights'): + self.head.init_weights() + else: + self.head = None + + def init_weights(self): + """Initialize the model network weights. """ + if getattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + pass + + def forward(self, data_batch, mode='infer'): + """ + 1. Define how the model is going to run, from input to output. + 2. Console of train, valid, test or infer step + 3. Set mode='infer' is used for saving inference model, refer to tools/export_model.py + """ + if mode == 'train': + return self.train_step(data_batch) + elif mode == 'valid': + return self.val_step(data_batch) + elif mode == 'test': + return self.test_step(data_batch) + elif mode == 'infer': + return self.infer_step(data_batch) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. input_data_batch -> loss_metric + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating setp. input_data_batch -> loss_metric + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Tets setp. to get acc in test data. input_data_batch -> output + """ + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/partitioners/transnetv2_partitioner.py b/paddlevideo/modeling/framework/partitioners/transnetv2_partitioner.py new file mode 100644 index 0000000000000000000000000000000000000000..c3295068cdb39657bcd9c1b05817417b2061f69a --- /dev/null +++ b/paddlevideo/modeling/framework/partitioners/transnetv2_partitioner.py @@ -0,0 +1,68 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import PARTITIONERS +from .base import BasePartitioner + +import paddle + + +@PARTITIONERS.register() +class TransNetV2Partitioner(BasePartitioner): + """TransNetV2 Partitioner framework + """ + def forward_net(self, imgs): + one_hot_pred = self.backbone(imgs) + return one_hot_pred + + def train_step(self, data_batch): + """Define how the model is going to train, from input to output. + """ + frame_sequence = data_batch[0] + one_hot_gt, many_hot_gt = data_batch[1:] + one_hot_pred = self.forward_net(frame_sequence) + dict_ = {} + if isinstance(one_hot_pred, tuple): + one_hot_pred, dict_ = one_hot_pred + many_hot_pred = dict_.get("many_hot", None) + comb_reg_loss = dict_.get("comb_reg_loss", None) + loss_metrics = self.head.loss(one_hot_pred, one_hot_gt, + many_hot_pred, many_hot_gt, + reg_losses={"comb_reg": comb_reg_loss}) + return loss_metrics + + def val_step(self, data_batch): + frame_sequence = data_batch[0] + one_hot_gt, many_hot_gt = data_batch[1:] + one_hot_pred = self.forward_net(frame_sequence) + dict_ = {} + if isinstance(one_hot_pred, tuple): + one_hot_pred, dict_ = one_hot_pred + many_hot_pred = dict_.get("many_hot", None) + comb_reg_loss = dict_.get("comb_reg_loss", None) + loss_metrics = self.head.loss(one_hot_pred, one_hot_gt, + many_hot_pred, many_hot_gt, + reg_losses={"comb_reg": comb_reg_loss}) + return loss_metrics + + def test_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + # NOTE: (shipping) when testing, the net won't call head.loss, we deal with the test processing in /paddlevideo/metrics + frame_sequence = data_batch[0] + one_hot_pred = self.forward_net(frame_sequence) + return one_hot_pred + + def infer_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + frame_sequence = data_batch[0] + one_hot_pred = self.forward_net(frame_sequence) + return one_hot_pred diff --git a/paddlevideo/modeling/framework/recognizers/__init__.py b/paddlevideo/modeling/framework/recognizers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9e9ee1d656ff5d980c54ee101f286a71402ba3e1 --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from .base import BaseRecognizer +from .recognizer1d import Recognizer1D +from .recognizer2d import Recognizer2D +from .recognizer3d import Recognizer3D +from .recognizer_transformer import RecognizerTransformer +from .recognizer_gcn import RecognizerGCN +from .recognizerMRI import RecognizerMRI +from .recognizer3dMRI import Recognizer3DMRI +from .recognizer_transformer_MRI import RecognizerTransformer_MRI +from .recognizer_movinet_frame import MoViNetRecognizerFrame + +__all__ = [ + 'BaseRecognizer', 'Recognizer1D', 'Recognizer2D', 'Recognizer3D', + 'RecognizerTransformer', 'RecognizerGCN', 'RecognizerMRI', + 'Recognizer3DMRI', 'RecognizerTransformer_MRI', 'MoViNetRecognizerFrame' +] diff --git a/paddlevideo/modeling/framework/recognizers/base.py b/paddlevideo/modeling/framework/recognizers/base.py new file mode 100644 index 0000000000000000000000000000000000000000..bf31caf04cb82472c579780d9575f2edf4c53e0e --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/base.py @@ -0,0 +1,81 @@ +from abc import abstractmethod +from ... import builder +import paddle.nn as nn + + +class BaseRecognizer(nn.Layer): + """Base class for recognizers. + + All recognizers should subclass it. + All subclass should overwrite: + + - Methods:``train_step``, supporting to forward when training. + - Methods:``valid_step``, supporting to forward when validating. + - Methods:``test_step``, supporting to forward when testing. + + Args: + backbone (dict): Backbone modules to extract feature. + head (dict): Classification head to process feature. + + """ + def __init__(self, backbone=None, head=None, runtime_cfg=None): + + super().__init__() + if backbone is not None: + self.backbone = builder.build_backbone(backbone) + if hasattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + self.backbone = None + if head is not None: + self.head_name = head.name + self.head = builder.build_head(head) + if hasattr(self.head, 'init_weights'): + self.head.init_weights() + else: + self.head = None + + # Settings when the model is running, + # such as 'avg_type' + self.runtime_cfg = runtime_cfg + + def forward(self, data_batch, mode='infer'): + """ + 1. Define how the model is going to run, from input to output. + 2. Console of train, valid, test or infer step + 3. Set mode='infer' is used for saving inference model, refer to tools/export_model.py + """ + if mode == 'train': + return self.train_step(data_batch) + elif mode == 'valid': + return self.val_step(data_batch) + elif mode == 'test': + return self.test_step(data_batch) + elif mode == 'infer': + return self.infer_step(data_batch) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating step. + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Test step. + """ + raise NotImplementedError + + @abstractmethod + def infer_step(self, data_batch, **kwargs): + """Infer step. + """ + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/recognizers/recognizer1d.py b/paddlevideo/modeling/framework/recognizers/recognizer1d.py new file mode 100644 index 0000000000000000000000000000000000000000..3927b181efc71620860d56c6155778bb793673e8 --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizer1d.py @@ -0,0 +1,63 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer + + +@RECOGNIZERS.register() +class Recognizer1D(BaseRecognizer): + """1D recognizer model framework.""" + def forward_net(self, imgs): + """Define how the model is going to train, from input to output. + """ + lstm_logit, lstm_output = self.head(imgs) + return lstm_logit, lstm_output + + def train_step(self, data_batch): + """Training step. + """ + rgb_data, rgb_len, rgb_mask, audio_data, audio_len, audio_mask, labels = data_batch + imgs = [(rgb_data, rgb_len, rgb_mask), + (audio_data, audio_len, audio_mask)] + + # call forward + lstm_logit, lstm_output = self.forward_net(imgs) + loss = self.head.loss(lstm_logit, labels) + hit_at_one, perr, gap = self.head.metric(lstm_output, labels) + loss_metrics = dict() + loss_metrics['loss'] = loss + loss_metrics['hit_at_one'] = hit_at_one + loss_metrics['perr'] = perr + loss_metrics['gap'] = gap + + return loss_metrics + + def val_step(self, data_batch): + """Validating setp. + """ + return self.train_step(data_batch) + + def test_step(self, data_batch): + """Testing setp. + """ + return self.train_step(data_batch) + + def infer_step(self, data_batch): + """Infering setp. + """ + rgb_data, rgb_len, rgb_mask, audio_data, audio_len, audio_mask = data_batch + imgs = [(rgb_data, rgb_len, rgb_mask), + (audio_data, audio_len, audio_mask)] + # call forward + lstm_logit, _ = self.forward_net(imgs) + return lstm_logit diff --git a/paddlevideo/modeling/framework/recognizers/recognizer2d.py b/paddlevideo/modeling/framework/recognizers/recognizer2d.py new file mode 100644 index 0000000000000000000000000000000000000000..d8aa6619f0f78754b3f1bc85139625f1fe61e99f --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizer2d.py @@ -0,0 +1,69 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer +import paddle +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class Recognizer2D(BaseRecognizer): + """2D recognizer model framework.""" + def forward_net(self, imgs): + # NOTE: As the num_segs is an attribute of dataset phase, and didn't pass to build_head phase, should obtain it from imgs(paddle.Tensor) now, then call self.head method. + num_segs = imgs.shape[ + 1] # imgs.shape=[N,T,C,H,W], for most commonly case + imgs = paddle.reshape_(imgs, [-1] + list(imgs.shape[2:])) + + if self.backbone is not None: + feature = self.backbone(imgs) + else: + feature = imgs + + if self.head is not None: + cls_score = self.head(feature, num_segs) + else: + cls_score = None + + return cls_score + + def train_step(self, data_batch): + """Define how the model is going to train, from input to output. + """ + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self.forward_net(imgs) + loss_metrics = self.head.loss(cls_score, labels) + return loss_metrics + + def val_step(self, data_batch): + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self.forward_net(imgs) + loss_metrics = self.head.loss(cls_score, labels, valid_mode=True) + return loss_metrics + + def test_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + # NOTE: (shipping) when testing, the net won't call head.loss, we deal with the test processing in /paddlevideo/metrics + imgs = data_batch[0] + cls_score = self.forward_net(imgs) + return cls_score + + def infer_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + imgs = data_batch[0] + cls_score = self.forward_net(imgs) + return cls_score diff --git a/paddlevideo/modeling/framework/recognizers/recognizer3d.py b/paddlevideo/modeling/framework/recognizers/recognizer3d.py new file mode 100644 index 0000000000000000000000000000000000000000..9fdabf58c65babc0ce896a0a25996c9f9dd3e240 --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizer3d.py @@ -0,0 +1,69 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class Recognizer3D(BaseRecognizer): + """3D Recognizer model framework. + """ + def forward_net(self, imgs): + """Define how the model is going to run, from input to output. + """ + feature = self.backbone(imgs) + cls_score = self.head(feature) + return cls_score + + def train_step(self, data_batch): + """Training step. + """ + imgs = data_batch[0:2] + labels = data_batch[2:] + + # call forward + cls_score = self.forward_net(imgs) + loss_metrics = self.head.loss(cls_score, labels) + return loss_metrics + + def val_step(self, data_batch): + """Validating setp. + """ + imgs = data_batch[0:2] + labels = data_batch[2:] + + # call forward + cls_score = self.forward_net(imgs) + loss_metrics = self.head.loss(cls_score, labels, valid_mode=True) + return loss_metrics + + def test_step(self, data_batch): + """Test step. + """ + imgs = data_batch[0:2] + # call forward + cls_score = self.forward_net(imgs) + + return cls_score + + def infer_step(self, data_batch): + """Infer step. + """ + imgs = data_batch[0:2] + # call forward + cls_score = self.forward_net(imgs) + + return cls_score diff --git a/paddlevideo/modeling/framework/recognizers/recognizer3dMRI.py b/paddlevideo/modeling/framework/recognizers/recognizer3dMRI.py new file mode 100644 index 0000000000000000000000000000000000000000..9298491c0e9ab445be2dd56c853c9dfee12ec8af --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizer3dMRI.py @@ -0,0 +1,81 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer +from paddlevideo.utils import get_logger +import paddle + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class Recognizer3DMRI(BaseRecognizer): + """3D Recognizer model framework. + """ + def forward_net(self, imgs): + """Define how the model is going to run, from input to output. + """ + + imgs[0] = paddle.cast(imgs[0], "float32") + imgs[1] = paddle.cast(imgs[1], "float32") + imgs[0] = imgs[0].unsqueeze(1) + imgs[1] = imgs[1].unsqueeze(1) + + feature = self.backbone(imgs) + cls_score = self.head(feature) + return cls_score + + def train_step(self, data_batch): + """Training step. + """ + imgs = data_batch[0:2] + labels = data_batch[2:] + + # call forward + cls_score = self.forward_net(imgs) + cls_score = paddle.nn.functional.sigmoid(cls_score) + loss_metrics = self.head.loss(cls_score, labels, if_top5=False) + return loss_metrics + + def val_step(self, data_batch): + """Validating setp. + """ + imgs = data_batch[0:2] + labels = data_batch[2:] + + # call forward + cls_score = self.forward_net(imgs) + cls_score = paddle.nn.functional.sigmoid(cls_score) + loss_metrics = self.head.loss(cls_score, + labels, + valid_mode=True, + if_top5=False) + return loss_metrics + + def test_step(self, data_batch): + """Test step. + """ + imgs = data_batch[0:2] + # call forward + cls_score = self.forward_net(imgs) + + return cls_score + + def infer_step(self, data_batch): + """Infer step. + """ + imgs = data_batch[0:2] + # call forward + cls_score = self.forward_net(imgs) + + return cls_score diff --git a/paddlevideo/modeling/framework/recognizers/recognizerMRI.py b/paddlevideo/modeling/framework/recognizers/recognizerMRI.py new file mode 100644 index 0000000000000000000000000000000000000000..4b1713e61bf23da23c0cc84174bbd211a4e19025 --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizerMRI.py @@ -0,0 +1,76 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer +import paddle +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class RecognizerMRI(BaseRecognizer): + """2D recognizer model framework.""" + def forward_net(self, imgs): + # NOTE: As the num_segs is an attribute of dataset phase, and didn't pass to build_head phase, should obtain it from imgs(paddle.Tensor) now, then call self.head method. + num_segs = imgs.shape[ + 1] # imgs.shape=[N,T,C,H,W], for most commonly case + imgs = paddle.reshape_(imgs, [-1] + list(imgs.shape[2:])) + imgs = paddle.cast(imgs, "float32") ############# + imgs = imgs.unsqueeze(1) + + if self.backbone != None: + feature = self.backbone(imgs) + else: + feature = imgs + + if self.head != None: + cls_score = self.head(feature, num_segs) + else: + cls_score = None + + return cls_score + + def train_step(self, data_batch): + """Define how the model is going to train, from input to output. + """ + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self.forward_net(imgs) + cls_score = paddle.nn.functional.sigmoid(cls_score) + loss_metrics = self.head.loss(cls_score, labels, if_top5=False) + return loss_metrics + + def val_step(self, data_batch): + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self.forward_net(imgs) + cls_score = paddle.nn.functional.sigmoid(cls_score) + loss_metrics = self.head.loss(cls_score, + labels, + valid_mode=True, + if_top5=False) + return loss_metrics + + def test_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + # NOTE: (shipping) when testing, the net won't call head.loss, we deal with the test processing in /paddlevideo/metrics + imgs = data_batch[0] + cls_score = self.forward_net(imgs) + return cls_score + + def infer_step(self, data_batch): + """Define how the model is going to test, from input to output.""" + imgs = data_batch[0] + cls_score = self.forward_net(imgs) + return cls_score diff --git a/paddlevideo/modeling/framework/recognizers/recognizer_gcn.py b/paddlevideo/modeling/framework/recognizers/recognizer_gcn.py new file mode 100644 index 0000000000000000000000000000000000000000..281c5ac9e2bc9dd4341cc43548bd2fe14e04d518 --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizer_gcn.py @@ -0,0 +1,87 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class RecognizerGCN(BaseRecognizer): + """GCN Recognizer model framework. + """ + + def __init__(self, + backbone=None, + head=None, + runtime_cfg=None, + if_top5=True): + """ + Args: + backbone (dict): Backbone modules to extract feature. + head (dict): Classification head to process feature. + is_top5 (bool): Whether to display top-5 accuracy during training/validation steps. + """ + super(RecognizerGCN, self).__init__(backbone, head, runtime_cfg) + self.if_top5 = if_top5 + + def forward_net(self, data): + """Define how the model is going to run, from input to output. + """ + feature = self.backbone(data) + cls_score = self.head(feature) + return cls_score + + def train_step(self, data_batch): + """Training step. + """ + data = data_batch[0] + label = data_batch[1:] + + # call forward + cls_score = self.forward_net(data) + loss_metrics = self.head.loss(cls_score, label, if_top5=self.if_top5) + return loss_metrics + + def val_step(self, data_batch): + """Validating setp. + """ + data = data_batch[0] + label = data_batch[1:] + + # call forward + cls_score = self.forward_net(data) + loss_metrics = self.head.loss(cls_score, + label, + valid_mode=True, + if_top5=self.if_top5) + return loss_metrics + + def test_step(self, data_batch): + """Test step. + """ + data = data_batch[0] + + # call forward + cls_score = self.forward_net(data) + return cls_score + + def infer_step(self, data_batch): + """Infer step. + """ + data = data_batch[0] + + # call forward + cls_score = self.forward_net(data) + return cls_score diff --git a/paddlevideo/modeling/framework/recognizers/recognizer_movinet_frame.py b/paddlevideo/modeling/framework/recognizers/recognizer_movinet_frame.py new file mode 100644 index 0000000000000000000000000000000000000000..1ad2e149a3d67e85abf45c548034567a398e4ce8 --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizer_movinet_frame.py @@ -0,0 +1,78 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import paddle + +from paddlevideo.utils import get_logger +from .base import BaseRecognizer +from ...registry import RECOGNIZERS + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class MoViNetRecognizerFrame(BaseRecognizer): + + def forward_net(self, imgs): + """Define how the model is going to run, from input to output. + """ + self.backbone.clean_activation_buffers() + outputs = self.backbone(imgs) + cls_score = self.head(outputs) + return cls_score + + def train_step(self, data_batch): + """Training step. + """ + imgs = data_batch[0] + labels = data_batch[1] #.astype("int64") + data = paddle.transpose(imgs, perm=[0, 2, 1, 3, 4]) + # call forward + cls_score = self.forward_net(data) + loss_metrics = self.head.loss_func(cls_score, labels) + top1 = paddle.metric.accuracy(input=cls_score, label=labels, k=1) + top5 = paddle.metric.accuracy(input=cls_score, label=labels, k=5) + output = {'loss': loss_metrics, 'top1': top1, 'top5': top5} + return output + + def val_step(self, data_batch): + """Validating setp. + """ + imgs = data_batch[0] + labels = data_batch[1] #.astype("int64") + data = paddle.transpose(imgs, perm=[0, 2, 1, 3, 4]) + # call forward + cls_score = self.forward_net(data) + loss_metrics = self.head.loss_func(cls_score, labels) + top1 = paddle.metric.accuracy(input=cls_score, label=labels, k=1) + top5 = paddle.metric.accuracy(input=cls_score, label=labels, k=5) + output = {'loss': loss_metrics, 'top1': top1, 'top5': top5} + return output + + def test_step(self, data_batch): + """Test step. + """ + imgs = data_batch[0] + data = paddle.transpose(imgs, perm=[0, 2, 1, 3, 4]) + # call forward + cls_score = self.forward_net(data) + return cls_score + + def infer_step(self, data_batch): + """Infer step. + """ + imgs = data_batch[0] + # call forward + data = paddle.transpose(imgs, perm=[0, 2, 1, 3, 4]) + cls_score = self.forward_net(data) + + return cls_score diff --git a/paddlevideo/modeling/framework/recognizers/recognizer_transformer.py b/paddlevideo/modeling/framework/recognizers/recognizer_transformer.py new file mode 100644 index 0000000000000000000000000000000000000000..4144edacfbbf4ea569498f741b6c8b0ff07ed7b1 --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizer_transformer.py @@ -0,0 +1,98 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import paddle +import paddle.nn.functional as F +from paddlevideo.utils import get_logger + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class RecognizerTransformer(BaseRecognizer): + """Transformer's recognizer model framework.""" + def forward_net(self, imgs): + # imgs.shape=[N,C,T,H,W], for transformer case + if self.backbone is not None: + feature = self.backbone(imgs) + else: + feature = imgs + + if self.head is not None: + cls_score = self.head(feature) + else: + cls_score = None + + return cls_score + + def train_step(self, data_batch): + """Define how the model is going to train, from input to output. + """ + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self.forward_net(imgs) + loss_metrics = self.head.loss(cls_score, labels) + return loss_metrics + + def val_step(self, data_batch): + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self.forward_net(imgs) + loss_metrics = self.head.loss(cls_score, labels, valid_mode=True) + return loss_metrics + + def test_step(self, data_batch): + """Define how the model is going to infer, from input to output.""" + imgs = data_batch[0] + num_views = imgs.shape[2] // self.runtime_cfg.test.num_seg + cls_score = [] + for i in range(num_views): + view = imgs[:, :, i * self.runtime_cfg.test.num_seg:(i + 1) * + self.runtime_cfg.test.num_seg] + cls_score.append(self.forward_net(view)) + cls_score = self._average_view(cls_score, + self.runtime_cfg.test.avg_type) + return cls_score + + def infer_step(self, data_batch): + """Define how the model is going to infer, from input to output.""" + imgs = data_batch[0] + num_views = imgs.shape[2] // self.runtime_cfg.test.num_seg + cls_score = [] + for i in range(num_views): + view = imgs[:, :, i * self.runtime_cfg.test.num_seg:(i + 1) * + self.runtime_cfg.test.num_seg] + cls_score.append(self.forward_net(view)) + cls_score = self._average_view(cls_score, + self.runtime_cfg.test.avg_type) + return cls_score + + def _average_view(self, cls_score, avg_type='score'): + """Combine the predicted results of different views + + Args: + cls_score (list): results of multiple views + avg_type (str, optional): Average calculation method. Defaults to 'score'. + """ + assert avg_type in ['score', 'prob'], \ + f"Currently only the average of 'score' or 'prob' is supported, but got {avg_type}" + if avg_type == 'score': + return paddle.add_n(cls_score) / len(cls_score) + elif avg_type == 'prob': + return paddle.add_n( + [F.softmax(score, axis=-1) + for score in cls_score]) / len(cls_score) + else: + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/recognizers/recognizer_transformer_MRI.py b/paddlevideo/modeling/framework/recognizers/recognizer_transformer_MRI.py new file mode 100644 index 0000000000000000000000000000000000000000..e8696b4da591d5d5f3bdedf1dd9fc29d8ad5710b --- /dev/null +++ b/paddlevideo/modeling/framework/recognizers/recognizer_transformer_MRI.py @@ -0,0 +1,104 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import paddle +import paddle.nn.functional as F +from paddlevideo.utils import get_logger + +from ...registry import RECOGNIZERS +from .base import BaseRecognizer + +logger = get_logger("paddlevideo") + + +@RECOGNIZERS.register() +class RecognizerTransformer_MRI(BaseRecognizer): + """Transformer's recognizer model framework.""" + def forward_net(self, imgs): + # imgs.shape=[N,C,T,H,W], for transformer case + + imgs = paddle.cast(imgs, "float32") ############# + imgs = imgs.unsqueeze(1) + + if self.backbone != None: + feature = self.backbone(imgs) + else: + feature = imgs + + if self.head != None: + cls_score = self.head(feature) + else: + cls_score = None + + return cls_score + + def train_step(self, data_batch): + """Define how the model is going to train, from input to output. + """ + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self.forward_net(imgs) + cls_score = paddle.nn.functional.sigmoid(cls_score) + loss_metrics = self.head.loss(cls_score, labels, if_top5=False) + return loss_metrics + + def val_step(self, data_batch): + imgs = data_batch[0] + labels = data_batch[1:] + cls_score = self.forward_net(imgs) + cls_score = paddle.nn.functional.sigmoid(cls_score) + loss_metrics = self.head.loss(cls_score, + labels, + valid_mode=True, + if_top5=False) + return loss_metrics + + def test_step(self, data_batch): + """Define how the model is going to infer, from input to output.""" + imgs = data_batch[0] + num_views = imgs.shape[2] // self.backbone.seg_num + cls_score = [] + for i in range(num_views): + view = imgs[:, :, i * self.backbone.seg_num:(i + 1) * + self.backbone.seg_num] + cls_score.append(self.forward_net(view)) + cls_score = self.average_view(cls_score) + return cls_score + + def infer_step(self, data_batch): + """Define how the model is going to infer, from input to output.""" + imgs = data_batch[0] + num_views = imgs.shape[2] // self.backbone.seg_num + cls_score = [] + for i in range(num_views): + view = imgs[:, :, i * self.backbone.seg_num:(i + 1) * + self.backbone.seg_num] + cls_score.append(self.forward_net(view)) + cls_score = self.average_view(cls_score) + return cls_score + + def average_view(self, cls_score, average_type='score'): + """Combine the scores of different views + + Args: + cls_score (list): Scores of multiple views + average_type (str, optional): Average calculation method. Defaults to 'score'. + """ + assert average_type in ['score', 'prob'], \ + f"Currently only the average of 'score' or 'prob' is supported, but got {average_type}" + if average_type == 'score': + return paddle.add_n(cls_score) / len(cls_score) + elif average_type == 'avg': + return paddle.add_n([F.softmax(score) + for score in cls_score]) / len(cls_score) + else: + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/segment/__init__.py b/paddlevideo/modeling/framework/segment/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..28a1d2e15a23814cca03e21d515124fba25e3ae9 --- /dev/null +++ b/paddlevideo/modeling/framework/segment/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from .base import BaseSegment +from .cfbi import CFBI + +__all__ = ['BaseSegment', 'CFBI'] diff --git a/paddlevideo/modeling/framework/segment/base.py b/paddlevideo/modeling/framework/segment/base.py new file mode 100644 index 0000000000000000000000000000000000000000..0c5cb07f76a100958d21d88e8c0c795489f34497 --- /dev/null +++ b/paddlevideo/modeling/framework/segment/base.py @@ -0,0 +1,90 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from abc import abstractmethod +from ... import builder +import paddle.nn as nn + + +class BaseSegment(nn.Layer): + """Base class for semi-Video Object Segmentation. + All subclass should overwrite: + + - Methods:``train_step``, supporting to forward when training. + - Methods:``valid_step``, supporting to forward when validating. + - Methods:``test_step``, supporting to forward when testing. + + Args: + backbone (dict): Backbone modules to extract feature. + head (dict): Head to process feature. + loss(dict): Loss function. + """ + def __init__(self, backbone=None, head=None, loss=None): + super().__init__() + if backbone is not None: + self.backbone = builder.build_backbone(backbone) + if hasattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + self.backbone = None + if head is not None: + self.head_name = head.name + self.head = builder.build_head(head) + if hasattr(self.head, 'init_weights'): + self.head.init_weights() + else: + self.head = None + if loss is not None: + self.loss = builder.build_loss(loss) + else: + self.loss = None + + def forward(self, data_batch, mode='infer'): + """ + 1. Define how the model is going to run, from input to output. + 2. Console of train, valid, test or infer step + 3. Set mode='infer' is used for saving inference model, refer to tools/export_model.py + """ + if mode == 'train': + return self.train_step(data_batch) + elif mode == 'valid': + return self.val_step(data_batch) + elif mode == 'test': + return self.test_step(data_batch) + elif mode == 'infer': + return self.infer_step(data_batch) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating step. + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Test step. + """ + raise NotImplementedError + + @abstractmethod + def infer_step(self, data_batch, **kwargs): + """Infer step. + """ + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/segment/cfbi.py b/paddlevideo/modeling/framework/segment/cfbi.py new file mode 100644 index 0000000000000000000000000000000000000000..dcdc512f032a57ee11c5416aa0eb9a1b322e8bbc --- /dev/null +++ b/paddlevideo/modeling/framework/segment/cfbi.py @@ -0,0 +1,286 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np + +from .utils import foreground2background, global_matching_for_eval, local_matching, calculate_attention_head_for_eval +from ...registry import SEGMENT +from .base import BaseSegment +from paddlevideo.utils import get_logger + +logger = get_logger("paddlevideo") + + +@SEGMENT.register() +class CFBI(BaseSegment): + """CFBI model framework.""" + def __init__(self, backbone=None, head=None, loss=None): + super().__init__(backbone, head, loss) + x1 = paddle.zeros([3, 1, 1, 1]) + self.bg_bias = paddle.create_parameter( + shape=x1.shape, + dtype=x1.dtype, + default_initializer=nn.initializer.Assign(x1)) + self.fg_bias = paddle.create_parameter( + shape=x1.shape, + dtype=x1.dtype, + default_initializer=nn.initializer.Assign(x1)) + self.epsilon = 1e-05 + + def test_step(self, data_batch): + """Define how the model is going to test, from input to output. + """ + self.test_mode = True + ref_embeddings, ref_masks, prev_embedding, prev_mask, current_frame, pred_size, gt_ids = data_batch + current_frame_embedding_4x, current_frame_embedding_8x, current_frame_embedding_16x, \ + current_low_level = self.backbone(current_frame) + + current_frame_embedding = [ + current_frame_embedding_4x, current_frame_embedding_8x, + current_frame_embedding_16x + ] + + if prev_embedding is None: + return None, current_frame_embedding + else: + bs, c, h, w = current_frame_embedding_4x.shape + + tmp_dic, _ = self.before_seghead_process( + ref_embeddings, + prev_embedding, + current_frame_embedding, + ref_masks, + prev_mask, + gt_ids, + current_low_level=current_low_level, + ) + all_pred = [] + for i in range(bs): + pred = tmp_dic[i] + + pred = F.interpolate(pred, + size=[pred_size[0], pred_size[1]], + mode='bilinear', + align_corners=True) + all_pred.append(pred) + all_pred = paddle.concat(all_pred, axis=0) + all_pred = F.softmax(all_pred, axis=1) + return all_pred, current_frame_embedding + + def before_seghead_process(self, + ref_frame_embeddings=None, + previous_frame_embeddings=None, + current_frame_embeddings=None, + ref_frame_labels=None, + previous_frame_mask=None, + gt_ids=None, + current_low_level=None): + """ process befor segmentation head""" + TEST_GLOBAL_MATCHING_CHUNK = [4, 1, 1] + TEST_GLOBAL_ATROUS_RATE = [2, 1, 1] + TRAIN_LOCAL_ATROUS_RATE = [2, 1, 1] + TEST_LOCAL_ATROUS_RATE = [2, 1, 1] + MODEL_FLOAT16_MATCHING = False + TEST_GLOBAL_MATCHING_MIN_PIXEL = 100 + MODEL_MULTI_LOCAL_DISTANCE = [[4, 8, 12, 16, 20, 24], + [2, 4, 6, 8, 10, 12], [2, 4, 6, 8, 10]] + TRAIN_LOCAL_PARALLEL = True + TEST_LOCAL_PARALLEL = True + MODEL_MATCHING_BACKGROUND = True + MODEL_SEMANTIC_MATCHING_DIM = [32, 64, 128] + + dic_tmp = [] + boards = {} + scale_ref_frame_labels = [] + scale_previous_frame_labels = [] + for current_frame_embedding in current_frame_embeddings: + bs, c, h, w = current_frame_embedding.shape + if not self.test_mode: + raise NotImplementedError + else: + ref_frame_embeddings = list(zip(*ref_frame_embeddings)) + all_scale_ref_frame_label = [] + for ref_frame_label in ref_frame_labels: + scale_ref_frame_label = paddle.cast(F.interpolate( + paddle.cast(ref_frame_label, dtype="float32"), + size=(h, w), + mode='nearest'), + dtype="int32") + all_scale_ref_frame_label.append(scale_ref_frame_label) + scale_ref_frame_labels.append(all_scale_ref_frame_label) + scale_previous_frame_label = paddle.cast(F.interpolate( + paddle.cast(previous_frame_mask, dtype="float32"), + size=(h, w), + mode='nearest'), + dtype="int32") + scale_previous_frame_labels.append(scale_previous_frame_label) + for n in range(bs): + ref_obj_ids = paddle.reshape( + paddle.cast(paddle.arange(0, + np.array(gt_ids)[n] + 1), + dtype="int32"), [-1, 1, 1, 1]) + obj_num = ref_obj_ids.shape[0] + low_level_feat = paddle.unsqueeze(current_low_level[n], axis=0) + all_CE_input = [] + all_attention_head = [] + for scale_idx, current_frame_embedding, ref_frame_embedding, previous_frame_embedding, \ + scale_ref_frame_label, scale_previous_frame_label in zip(range(3), \ + current_frame_embeddings, ref_frame_embeddings, previous_frame_embeddings, \ + scale_ref_frame_labels, scale_previous_frame_labels): + #Prepare + seq_current_frame_embedding = current_frame_embedding[n] + seq_prev_frame_embedding = previous_frame_embedding[n] + seq_previous_frame_label = paddle.cast( + (paddle.cast(scale_previous_frame_label[n], dtype="int32") + == ref_obj_ids), + dtype="float32") + if np.array(gt_ids)[n] > 0: + dis_bias = paddle.concat([ + paddle.unsqueeze(self.bg_bias[scale_idx], axis=0), + paddle.expand( + paddle.unsqueeze(self.fg_bias[scale_idx], axis=0), + [np.array(gt_ids)[n], -1, -1, -1]) + ], + axis=0) + else: + dis_bias = paddle.unsqueeze(self.bg_bias[scale_idx], axis=0) + #Global FG map + matching_dim = MODEL_SEMANTIC_MATCHING_DIM[scale_idx] + seq_current_frame_embedding_for_matching = paddle.transpose( + seq_current_frame_embedding[:matching_dim], [1, 2, 0]) + + if not self.test_mode: + raise NotImplementedError + else: + all_scale_ref_frame_label = scale_ref_frame_label + all_ref_frame_embedding = ref_frame_embedding + all_reference_embeddings = [] + all_reference_labels = [] + seq_ref_frame_labels = [] + count = 0 + for idx in range(len(all_scale_ref_frame_label)): + + ref_frame_embedding = all_ref_frame_embedding[idx] + scale_ref_frame_label = all_scale_ref_frame_label[idx] + + seq_ref_frame_embedding = ref_frame_embedding[n] + seq_ref_frame_embedding = paddle.transpose( + seq_ref_frame_embedding, [1, 2, 0]) + seq_ref_frame_label = paddle.cast( + (paddle.cast(scale_ref_frame_label[n], + dtype="int32") == ref_obj_ids), + dtype="float32") + seq_ref_frame_labels.append(seq_ref_frame_label) + seq_ref_frame_label = paddle.transpose( + paddle.squeeze(seq_ref_frame_label, axis=1), + [1, 2, 0]) + all_reference_embeddings.append( + seq_ref_frame_embedding[:, :, :matching_dim]) + all_reference_labels.append(seq_ref_frame_label) + global_matching_fg = global_matching_for_eval( + all_reference_embeddings=all_reference_embeddings, + query_embeddings= + seq_current_frame_embedding_for_matching, + all_reference_labels=all_reference_labels, + n_chunks=TEST_GLOBAL_MATCHING_CHUNK[scale_idx], + dis_bias=dis_bias, + atrous_rate=TEST_GLOBAL_ATROUS_RATE[scale_idx], + use_float16=MODEL_FLOAT16_MATCHING, + atrous_obj_pixel_num=TEST_GLOBAL_MATCHING_MIN_PIXEL) + + # Local FG map + seq_prev_frame_embedding_for_matching = paddle.transpose( + seq_prev_frame_embedding[:matching_dim], [1, 2, 0]) + seq_previous_frame_label_for_matching = paddle.transpose( + paddle.squeeze(seq_previous_frame_label, axis=1), [1, 2, 0]) + local_matching_fg = local_matching( + prev_frame_embedding=seq_prev_frame_embedding_for_matching, + query_embedding=seq_current_frame_embedding_for_matching, + prev_frame_labels=seq_previous_frame_label_for_matching, + multi_local_distance=MODEL_MULTI_LOCAL_DISTANCE[scale_idx], + dis_bias=dis_bias, + atrous_rate=TRAIN_LOCAL_ATROUS_RATE[scale_idx] if + not self.test_mode else TEST_LOCAL_ATROUS_RATE[scale_idx], + use_float16=MODEL_FLOAT16_MATCHING, + allow_downsample=False, + allow_parallel=TRAIN_LOCAL_PARALLEL + if not self.test_mode else TEST_LOCAL_PARALLEL) + + #Aggregate Pixel-level Matching + to_cat_global_matching_fg = paddle.transpose( + paddle.squeeze(global_matching_fg, axis=0), [2, 3, 0, 1]) + to_cat_local_matching_fg = paddle.transpose( + paddle.squeeze(local_matching_fg, axis=0), [2, 3, 0, 1]) + all_to_cat = [ + to_cat_global_matching_fg, to_cat_local_matching_fg, + seq_previous_frame_label + ] + + #Global and Local BG map + if MODEL_MATCHING_BACKGROUND: + to_cat_global_matching_bg = foreground2background( + to_cat_global_matching_fg, + np.array(gt_ids)[n] + 1) + reshaped_prev_nn_feature_n = paddle.unsqueeze( + paddle.transpose(to_cat_local_matching_fg, + [0, 2, 3, 1]), + axis=1) + to_cat_local_matching_bg = foreground2background( + reshaped_prev_nn_feature_n, + np.array(gt_ids)[n] + 1) + to_cat_local_matching_bg = paddle.squeeze(paddle.transpose( + to_cat_local_matching_bg, [0, 4, 2, 3, 1]), + axis=-1) + all_to_cat += [ + to_cat_local_matching_bg, to_cat_global_matching_bg + ] + + to_cat_current_frame_embedding = paddle.expand( + paddle.unsqueeze(current_frame_embedding[n], axis=0), + [obj_num, -1, -1, -1]) + to_cat_prev_frame_embedding = paddle.expand( + paddle.unsqueeze(previous_frame_embedding[n], axis=0), + [obj_num, -1, -1, -1]) + to_cat_prev_frame_embedding_fg = to_cat_prev_frame_embedding * seq_previous_frame_label + to_cat_prev_frame_embedding_bg = to_cat_prev_frame_embedding * ( + 1 - seq_previous_frame_label) + all_to_cat += [ + to_cat_current_frame_embedding, + to_cat_prev_frame_embedding_fg, + to_cat_prev_frame_embedding_bg + ] + + CE_input = paddle.concat(all_to_cat, axis=1) + #Instance-level Attention + if not self.test_mode: + raise NotImplementedError + else: + attention_head = calculate_attention_head_for_eval( + all_ref_frame_embedding, + seq_ref_frame_labels, + paddle.expand( + paddle.unsqueeze(previous_frame_embedding[n], + axis=0), [obj_num, -1, -1, -1]), + seq_previous_frame_label, + epsilon=self.epsilon) + + all_CE_input.append(CE_input) + all_attention_head.append(attention_head) + + #Collaborative Ensembler + pred = self.head(all_CE_input, all_attention_head, low_level_feat) + dic_tmp.append(pred) + + return dic_tmp, boards diff --git a/paddlevideo/modeling/framework/segment/utils.py b/paddlevideo/modeling/framework/segment/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..1ec3be4d2ec1c41058d75ed54bd5c72a7f894487 --- /dev/null +++ b/paddlevideo/modeling/framework/segment/utils.py @@ -0,0 +1,754 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + + +def foreground2background(dis, obj_num): + if obj_num == 1: + return dis + bg_dis = [] + for i in range(obj_num): + obj_back = [] + for j in range(obj_num): + if i == j: + continue + obj_back.append(paddle.unsqueeze(dis[j], axis=0)) + obj_back = paddle.concat(x=obj_back, axis=1) + obj_back = paddle.min(x=obj_back, axis=1, keepdim=True) + bg_dis.append(obj_back) + bg_dis = paddle.concat(x=bg_dis, axis=0) + return bg_dis + + +WRONG_LABEL_PADDING_DISTANCE = 5e4 + + +#GLOBAL_DIST_MAP +def _pairwise_distances(x, x2, y, y2): + """ + Computes pairwise squared l2 distances between tensors x and y. + Args: + x: [n, feature_dim]. + y: [m, feature_dim]. + Returns: + d: [n, m]. + """ + xs = x2 + ys = y2 + + xs = paddle.unsqueeze(xs, axis=1) + ys = paddle.unsqueeze(ys, axis=0) + d = xs + ys - 2. * paddle.matmul(x, y, transpose_y=True) + return d + + +def _flattened_pairwise_distances(reference_embeddings, ref_square, + query_embeddings, query_square): + """ + Calculates flattened tensor of pairwise distances between ref and query. + Args: + reference_embeddings: [..., embedding_dim], + the embedding vectors for the reference frame + query_embeddings: [..., embedding_dim], + the embedding vectors for the query frames. + Returns: + dists: [reference_embeddings.size / embedding_dim, query_embeddings.size / embedding_dim] + """ + dists = _pairwise_distances(query_embeddings, query_square, + reference_embeddings, ref_square) + return dists + + +def _nn_features_per_object_for_chunk(reference_embeddings, ref_square, + query_embeddings, query_square, + wrong_label_mask): + """Extracts features for each object using nearest neighbor attention. + Args: + reference_embeddings: [n_chunk, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings: [m_chunk, embedding_dim], + the embedding vectors for the query frames. + wrong_label_mask: [n_objects, n_chunk], + the mask for pixels not used for matching. + Returns: + nn_features: A float32 tensor of nearest neighbor features of shape + [m_chunk, n_objects, n_chunk]. + """ + if reference_embeddings.dtype == "float16": + wrong_label_mask = paddle.cast(wrong_label_mask, dtype="float16") + else: + wrong_label_mask = paddle.cast(wrong_label_mask, dtype="float32") + + reference_embeddings_key = reference_embeddings + query_embeddings_key = query_embeddings + dists = _flattened_pairwise_distances(reference_embeddings_key, ref_square, + query_embeddings_key, query_square) + dists = (paddle.unsqueeze(dists, axis=1) + + paddle.unsqueeze(wrong_label_mask, axis=0) * + WRONG_LABEL_PADDING_DISTANCE) + features = paddle.min(dists, axis=2, keepdim=True) + return features + + +def _nearest_neighbor_features_per_object_in_chunks(reference_embeddings_flat, + query_embeddings_flat, + reference_labels_flat, + n_chunks): + """Calculates the nearest neighbor features per object in chunks to save mem. + Uses chunking to bound the memory use. + Args: + reference_embeddings_flat: [n, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings_flat: [m, embedding_dim], + the embedding vectors for the query frames. + reference_labels_flat: [n, n_objects], + the class labels of the reference frame. + n_chunks: Integer, the number of chunks to use to save memory + (set to 1 for no chunking). + Returns: + nn_features: [m, n_objects, n]. + """ + + feature_dim, embedding_dim = query_embeddings_flat.shape + chunk_size = int(np.ceil(float(feature_dim) / n_chunks)) + wrong_label_mask = reference_labels_flat < 0.1 + + wrong_label_mask = paddle.transpose(x=wrong_label_mask, perm=[1, 0]) + ref_square = paddle.sum(paddle.pow(reference_embeddings_flat, 2), axis=1) + query_square = paddle.sum(paddle.pow(query_embeddings_flat, 2), axis=1) + + all_features = [] + for n in range(n_chunks): + if n_chunks == 1: + query_embeddings_flat_chunk = query_embeddings_flat + query_square_chunk = query_square + chunk_start = 0 + else: + chunk_start = n * chunk_size + chunk_end = (n + 1) * chunk_size + query_square_chunk = query_square[chunk_start:chunk_end] + if query_square_chunk.shape[0] == 0: + continue + query_embeddings_flat_chunk = query_embeddings_flat[ + chunk_start:chunk_end] + features = _nn_features_per_object_for_chunk( + reference_embeddings_flat, ref_square, query_embeddings_flat_chunk, + query_square_chunk, wrong_label_mask) + all_features.append(features) + if n_chunks == 1: + nn_features = all_features[0] + else: + nn_features = paddle.concat(all_features, axis=0) + + return nn_features + + +def global_matching(reference_embeddings, + query_embeddings, + reference_labels, + n_chunks=100, + dis_bias=0., + ori_size=None, + atrous_rate=1, + use_float16=True, + atrous_obj_pixel_num=0): + """ + Calculates the distance to the nearest neighbor per object. + For every pixel of query_embeddings calculate the distance to the + nearest neighbor in the (possibly subsampled) reference_embeddings per object. + Args: + reference_embeddings: [height, width, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings: [height, width, + embedding_dim], the embedding vectors for the query frames. + reference_labels: [height, width, obj_nums], + the class labels of the reference frame. + n_chunks: Integer, the number of chunks to use to save memory + (set to 1 for no chunking). + dis_bias: [n_objects], foreground and background bias + ori_size: (ori_height, ori_width), + the original spatial size. If "None", (ori_height, ori_width) = (height, width). + atrous_rate: Integer, the atrous rate of reference_embeddings. + use_float16: Bool, if "True", use float16 type for matching. + Returns: + nn_features: [1, ori_height, ori_width, n_objects, feature_dim]. + """ + + assert (reference_embeddings.shape[:2] == reference_labels.shape[:2]) + if use_float16: + query_embeddings = paddle.cast(query_embeddings, dtype="float16") + reference_embeddings = paddle.cast(reference_embeddings, + dtype="float16") + h, w, embedding_dim = query_embeddings.shape + obj_nums = reference_labels.shape[2] + + if atrous_rate > 1: + h_pad = (atrous_rate - h % atrous_rate) % atrous_rate + w_pad = (atrous_rate - w % atrous_rate) % atrous_rate + selected_points = paddle.zeros([h + h_pad, w + w_pad]) + selected_points = selected_points.view( + (h + h_pad) // atrous_rate, atrous_rate, (w + w_pad) // atrous_rate, + atrous_rate) + selected_points[:, 0, :, 0] = 1. + selected_points = paddle.reshape(selected_points, + [h + h_pad, w + w_pad, 1])[:h, :w] + is_big_obj = (paddle.sum( + reference_labels, + axis=(0, 1))) > (atrous_obj_pixel_num * atrous_rate**2) + reference_labels[:, :, + is_big_obj] = reference_labels[:, :, + is_big_obj] * selected_points + + reference_embeddings_flat = paddle.reshape(reference_embeddings, + [-1, embedding_dim]) + reference_labels_flat = paddle.reshape(reference_labels, [-1, obj_nums]) + query_embeddings_flat = paddle.reshape(query_embeddings, + [-1, embedding_dim]) + + all_ref_fg = paddle.sum(reference_labels_flat, axis=1, keepdim=True) > 0.9 + reference_labels_flat = paddle.reshape( + paddle.masked_select(reference_labels_flat, + paddle.expand(all_ref_fg, [-1, obj_nums])), + [-1, obj_nums]) + if reference_labels_flat.shape[0] == 0: + return paddle.ones([1, h, w, obj_nums, 1]) + reference_embeddings_flat = paddle.reshape( + paddle.masked_select(reference_embeddings_flat, + paddle.expand(all_ref_fg, [-1, embedding_dim])), + [-1, embedding_dim]) + + nn_features = _nearest_neighbor_features_per_object_in_chunks( + reference_embeddings_flat, query_embeddings_flat, reference_labels_flat, + n_chunks) + + nn_features_reshape = paddle.reshape(nn_features, [1, h, w, obj_nums, 1]) + nn_features_reshape = ( + F.sigmoid(nn_features_reshape + + paddle.reshape(dis_bias, [1, 1, 1, -1, 1])) - 0.5) * 2 + + #TODO: ori_size is not None + + if use_float16: + nn_features_reshape = paddle.cast(nn_features_reshape, dtype="float32") + return nn_features_reshape + + +def global_matching_for_eval(all_reference_embeddings, + query_embeddings, + all_reference_labels, + n_chunks=20, + dis_bias=0., + ori_size=None, + atrous_rate=1, + use_float16=True, + atrous_obj_pixel_num=0): + """ + Calculates the distance to the nearest neighbor per object. + For every pixel of query_embeddings calculate the distance to the + nearest neighbor in the (possibly subsampled) reference_embeddings per object. + Args: + all_reference_embeddings: A list of reference_embeddings, + each with size [height, width, embedding_dim], + the embedding vectors for the reference frame. + query_embeddings: [n_query_images, height, width, + embedding_dim], the embedding vectors for the query frames. + all_reference_labels: A list of reference_labels, + each with size [height, width, obj_nums], + the class labels of the reference frame. + n_chunks: Integer, the number of chunks to use to save memory + (set to 1 for no chunking). + dis_bias: [n_objects], foreground and background bias + ori_size: (ori_height, ori_width), + the original spatial size. If "None", (ori_height, ori_width) = (height, width). + atrous_rate: Integer, the atrous rate of reference_embeddings. + use_float16: Bool, if "True", use float16 type for matching. + Returns: + nn_features: [n_query_images, ori_height, ori_width, n_objects, feature_dim]. + """ + + h, w, embedding_dim = query_embeddings.shape + obj_nums = all_reference_labels[0].shape[2] + all_reference_embeddings_flat = [] + all_reference_labels_flat = [] + ref_num = len(all_reference_labels) + n_chunks *= ref_num + if atrous_obj_pixel_num > 0: + if atrous_rate > 1: + h_pad = (atrous_rate - h % atrous_rate) % atrous_rate + w_pad = (atrous_rate - w % atrous_rate) % atrous_rate + selected_points = paddle.zeros([h + h_pad, w + w_pad]) + selected_points = paddle.reshape( + selected_points, [(h + h_pad) // atrous_rate, atrous_rate, + (w + w_pad) // atrous_rate, atrous_rate]) + selected_points[:, 0, :, 0] = 1. + selected_points = paddle.reshape(selected_points, + [h + h_pad, w + w_pad, 1])[:h, :w] + + for reference_embeddings, reference_labels, idx in zip( + all_reference_embeddings, all_reference_labels, range(ref_num)): + if atrous_rate > 1: + is_big_obj = paddle.sum( + reference_labels, + axis=(0, 1)) > (atrous_obj_pixel_num * atrous_rate**2) + is_big_obj = list(np.array(is_big_obj)) + for j in range(len(is_big_obj)): + if is_big_obj[j] == True: + reference_labels[:, :, j:j + + 1] = reference_labels[:, :, j:j + + 1] * selected_points + + reference_embeddings_flat = paddle.reshape(reference_embeddings, + [-1, embedding_dim]) + reference_labels_flat = paddle.reshape(reference_labels, + [-1, obj_nums]) + + all_reference_embeddings_flat.append(reference_embeddings_flat) + all_reference_labels_flat.append(reference_labels_flat) + + reference_embeddings_flat = paddle.concat( + x=all_reference_embeddings_flat, axis=0) + reference_labels_flat = paddle.concat(x=all_reference_labels_flat, + axis=0) + else: + if ref_num == 1: + reference_embeddings, reference_labels = all_reference_embeddings[ + 0], all_reference_labels[0] + if atrous_rate > 1: + h_pad = (atrous_rate - h % atrous_rate) % atrous_rate + w_pad = (atrous_rate - w % atrous_rate) % atrous_rate + if h_pad > 0 or w_pad > 0: + reference_embeddings = F.pad(reference_embeddings, + [0, h_pad, 0, w_pad, 0, 0]) + reference_labels = F.pad(reference_labels, + [0, h_pad, 0, w_pad, 0, 0]) + reference_embeddings = paddle.reshape( + reference_embeddings, + [(h + h_pad) // atrous_rate, atrous_rate, + (w + w_pad) // atrous_rate, atrous_rate, 32]) + reference_labels = paddle.reshape( + reference_labels, + [(h + h_pad) // atrous_rate, atrous_rate, + (w + w_pad) // atrous_rate, atrous_rate, -1]) + reference_embeddings = paddle.reshape( + reference_embeddings[:, 0, :, 0, :], + reference_embeddings[:, 0, :, 0, :].shape) + reference_labels = paddle.reshape( + reference_labels[:, 0, :, 0, :], + reference_labels[:, 0, :, 0, :].shape) + reference_embeddings_flat = paddle.reshape(reference_embeddings, + [-1, embedding_dim]) + reference_labels_flat = paddle.reshape(reference_labels, + [-1, obj_nums]) + else: + for reference_embeddings, reference_labels, idx in zip( + all_reference_embeddings, all_reference_labels, + range(ref_num)): + if atrous_rate > 1: + h_pad = (atrous_rate - h % atrous_rate) % atrous_rate + w_pad = (atrous_rate - w % atrous_rate) % atrous_rate + if h_pad > 0 or w_pad > 0: + reference_embeddings = F.pad(reference_embeddings, + [0, h_pad, 0, w_pad, 0, 0]) + reference_labels = F.pad(reference_labels, + [0, h_pad, 0, w_pad, 0, 0]) + + reference_embeddings = paddle.reshape( + reference_embeddings, + [(h + h_pad) // atrous_rate, atrous_rate, + (w + w_pad) // atrous_rate, atrous_rate, -1]) + reference_labels = paddle.reshape( + reference_labels, + [(h + h_pad) // atrous_rate, atrous_rate, + (w + w_pad) // atrous_rate, atrous_rate, -1]) + reference_embeddings = paddle.reshape( + reference_embeddings[:, 0, :, 0, :], + reference_embeddings[:, 0, :, 0, :].shape) + reference_labels = paddle.reshape( + reference_labels[:, 0, :, 0, :], + reference_labels[:, 0, :, 0, :].shape) + + reference_embeddings_flat = paddle.reshape( + reference_embeddings, [-1, embedding_dim]) + reference_labels_flat = paddle.reshape(reference_labels, + [-1, obj_nums]) + + all_reference_embeddings_flat.append(reference_embeddings_flat) + all_reference_labels_flat.append(reference_labels_flat) + + reference_embeddings_flat = paddle.concat( + all_reference_embeddings_flat, axis=0) + reference_labels_flat = paddle.concat(all_reference_labels_flat, + axis=0) + + query_embeddings_flat = paddle.reshape(query_embeddings, + [-1, embedding_dim]) + + all_ref_fg = paddle.sum(reference_labels_flat, axis=1, keepdim=True) > 0.9 + reference_labels_flat = paddle.reshape( + paddle.masked_select(reference_labels_flat, + paddle.expand(all_ref_fg, [-1, obj_nums])), + [-1, obj_nums]) + if reference_labels_flat.shape[0] == 0: + return paddle.ones([1, h, w, obj_nums, 1]) + reference_embeddings_flat = paddle.reshape( + paddle.masked_select(reference_embeddings_flat, + paddle.expand(all_ref_fg, [-1, embedding_dim])), + [-1, embedding_dim]) + if use_float16: + query_embeddings_flat = paddle.cast(query_embeddings_flat, + dtype="float16") + reference_embeddings_flat = paddle.cast(reference_embeddings_flat, + dtype="float16") + nn_features = _nearest_neighbor_features_per_object_in_chunks( + reference_embeddings_flat, query_embeddings_flat, reference_labels_flat, + n_chunks) + + nn_features_reshape = paddle.reshape(nn_features, [1, h, w, obj_nums, 1]) + nn_features_reshape = ( + F.sigmoid(nn_features_reshape + + paddle.reshape(dis_bias, [1, 1, 1, -1, 1])) - 0.5) * 2 + + # TODO: ori_size is not None + + if use_float16: + nn_features_reshape = paddle.cast(nn_features_reshape, dtype="float32") + return nn_features_reshape + + +#LOCAL_DIST_MAP +def local_pairwise_distances(x, + y, + max_distance=9, + atrous_rate=1, + allow_downsample=False): + """Computes pairwise squared l2 distances using a local search window. + Use for-loop for saving memory. + Args: + x: Float32 tensor of shape [height, width, feature_dim]. + y: Float32 tensor of shape [height, width, feature_dim]. + max_distance: Integer, the maximum distance in pixel coordinates + per dimension which is considered to be in the search window. + atrous_rate: Integer, the atrous rate of local matching. + allow_downsample: Bool, if "True", downsample x and y + with a stride of 2. + Returns: + Float32 distances tensor of shape [height, width, (2 * max_distance + 1) ** 2]. + """ + if allow_downsample: + ori_height = x.shape[0] + ori_width = x.shape[1] + x = paddle.unsqueeze(paddle.transpose(x, [2, 0, 1]), axis=0) + y = paddle.unsqueeze(paddle.transpose(y, [2, 0, 1]), axis=0) + down_size = (int(ori_height / 2) + 1, int(ori_width / 2) + 1) + x = F.interpolate(x, + size=down_size, + mode='bilinear', + align_corners=True) + y = F.interpolate(y, + size=down_size, + mode='bilinear', + align_corners=True) + x = paddle.unsqueeze(paddle.transpose(x, [1, 2, 0]), axis=0) + y = paddle.unsqueeze(paddle.transpose(y, [1, 2, 0]), axis=0) + + pad_max_distance = max_distance - max_distance % atrous_rate + # no change pad + padded_y = F.pad(y, (0, 0, pad_max_distance, pad_max_distance, + pad_max_distance, pad_max_distance), + value=WRONG_LABEL_PADDING_DISTANCE) + + height, width, _ = x.shape + dists = [] + for y in range(2 * pad_max_distance // atrous_rate + 1): + y_start = y * atrous_rate + y_end = y_start + height + y_slice = padded_y[y_start:y_end] + for x in range(2 * max_distance + 1): + x_start = x * atrous_rate + x_end = x_start + width + offset_y = y_slice[:, x_start:x_end] + dist = paddle.sum(paddle.pow((x - offset_y), 2), axis=2) + dists.append(dist) + dists = paddle.stack(dists, axis=2) + + return dists + + +def local_pairwise_distances_parallel(x, + y, + max_distance=9, + atrous_rate=1, + allow_downsample=True): + """Computes pairwise squared l2 distances using a local search window. + Args: + x: Float32 tensor of shape [height, width, feature_dim]. + y: Float32 tensor of shape [height, width, feature_dim]. + max_distance: Integer, the maximum distance in pixel coordinates + per dimension which is considered to be in the search window. + atrous_rate: Integer, the atrous rate of local matching. + allow_downsample: Bool, if "True", downsample x and y + with a stride of 2. + Returns: + Float32 distances tensor of shape [height, width, (2 * max_distance + 1) ** 2]. + """ + + ori_height, ori_width, _ = x.shape + x = paddle.unsqueeze(paddle.transpose(x, [2, 0, 1]), axis=0) + y = paddle.unsqueeze(paddle.transpose(y, [2, 0, 1]), axis=0) + if allow_downsample: + down_size = (int(ori_height / 2) + 1, int(ori_width / 2) + 1) + x = F.interpolate(x, + size=down_size, + mode='bilinear', + align_corners=True) + y = F.interpolate(y, + size=down_size, + mode='bilinear', + align_corners=True) + + _, channels, height, width = x.shape + + x2 = paddle.reshape(paddle.sum(paddle.pow(x, 2), axis=1), + [height, width, 1]) + y2 = paddle.reshape(paddle.sum(paddle.pow(y, 2), axis=1), + [1, 1, height, width]) + + pad_max_distance = max_distance - max_distance % atrous_rate + # no change pad + padded_y = F.pad(y, (pad_max_distance, pad_max_distance, pad_max_distance, + pad_max_distance)) + padded_y2 = F.pad(y2, (pad_max_distance, pad_max_distance, pad_max_distance, + pad_max_distance), + value=WRONG_LABEL_PADDING_DISTANCE) + + offset_y = paddle.transpose( + paddle.reshape( + F.unfold(x=padded_y, + kernel_sizes=[height, width], + strides=[atrous_rate, atrous_rate]), + [channels, height * width, -1]), [1, 0, 2]) + offset_y2 = paddle.reshape( + F.unfold(padded_y2, + kernel_sizes=[height, width], + strides=[atrous_rate, atrous_rate]), [height, width, -1]) + x = paddle.transpose(paddle.reshape(x, [channels, height * width, -1]), + [1, 2, 0]) + + dists = x2 + offset_y2 - 2. * paddle.reshape(paddle.matmul(x, offset_y), + [height, width, -1]) + + return dists + + +def local_matching(prev_frame_embedding, + query_embedding, + prev_frame_labels, + dis_bias=0., + multi_local_distance=[15], + ori_size=None, + atrous_rate=1, + use_float16=True, + allow_downsample=True, + allow_parallel=True): + """Computes nearest neighbor features while only allowing local matches. + Args: + prev_frame_embedding: [height, width, embedding_dim], + the embedding vectors for the last frame. + query_embedding: [height, width, embedding_dim], + the embedding vectors for the query frames. + prev_frame_labels: [height, width, n_objects], + the class labels of the previous frame. + multi_local_distance: A list of Integer, + a list of maximum distance allowed for local matching. + ori_size: (ori_height, ori_width), + the original spatial size. If "None", (ori_height, ori_width) = (height, width). + atrous_rate: Integer, the atrous rate of local matching. + use_float16: Bool, if "True", use float16 type for matching. + allow_downsample: Bool, if "True", downsample prev_frame_embedding and query_embedding + with a stride of 2. + allow_parallel: Bool, if "True", do matching in a parallel way. If "False", do matching in + a for-loop way, which will save GPU memory. + Returns: + nn_features: A float32 np.array of nearest neighbor features of shape + [1, height, width, n_objects, 1]. + """ + max_distance = multi_local_distance[-1] + + if ori_size is None: + height, width = prev_frame_embedding.shape[:2] + ori_size = (height, width) + + obj_num = prev_frame_labels.shape[2] + pad = paddle.ones([1]) * WRONG_LABEL_PADDING_DISTANCE + if use_float16: + query_embedding = paddle.cast(query_embedding, dtype="float16") + prev_frame_embedding = paddle.cast(prev_frame_embedding, + dtype="float16") + pad = paddle.cast(pad, dtype="float16") + + if allow_parallel: + d = local_pairwise_distances_parallel(query_embedding, + prev_frame_embedding, + max_distance=max_distance, + atrous_rate=atrous_rate, + allow_downsample=allow_downsample) + else: + d = local_pairwise_distances(query_embedding, + prev_frame_embedding, + max_distance=max_distance, + atrous_rate=atrous_rate, + allow_downsample=allow_downsample) + + height, width = d.shape[:2] + + labels = paddle.unsqueeze(paddle.transpose(prev_frame_labels, [2, 0, 1]), 1) + labels = paddle.unsqueeze(paddle.transpose(prev_frame_labels, [2, 0, 1]), + axis=1) + if (height, width) != ori_size: + labels = F.interpolate(labels, size=(height, width), mode='nearest') + + pad_max_distance = max_distance - max_distance % atrous_rate + atrous_max_distance = pad_max_distance // atrous_rate + #no change pad + padded_labels = F.pad(labels, ( + pad_max_distance, + pad_max_distance, + pad_max_distance, + pad_max_distance, + ), + mode='constant', + value=0) + + offset_masks = paddle.transpose( + paddle.reshape( + F.unfold(padded_labels, + kernel_sizes=[height, width], + strides=[atrous_rate, atrous_rate]), + [obj_num, height, width, -1]), [1, 2, 3, 0]) > 0.9 + + d_tiled = paddle.expand(paddle.unsqueeze( + d, axis=-1), [-1, -1, -1, obj_num]) # h, w, num_local_pos, obj_num + + d_masked = paddle.where(offset_masks, d_tiled, pad) + dists = paddle.min(d_masked, axis=2) + multi_dists = [ + paddle.unsqueeze(paddle.transpose(dists, [2, 0, 1]), axis=1) + ] # n_objects, num_multi_local, h, w + + reshaped_d_masked = paddle.reshape(d_masked, [ + height, width, 2 * atrous_max_distance + 1, 2 * atrous_max_distance + 1, + obj_num + ]) + for local_dis in multi_local_distance[:-1]: + local_dis = local_dis // atrous_rate + start_idx = atrous_max_distance - local_dis + end_idx = atrous_max_distance + local_dis + 1 + new_d_masked = paddle.reshape( + reshaped_d_masked[:, :, start_idx:end_idx, start_idx:end_idx, :], + reshaped_d_masked[:, :, start_idx:end_idx, + start_idx:end_idx, :].shape) + new_d_masked = paddle.reshape(new_d_masked, + [height, width, -1, obj_num]) + new_dists = paddle.min(new_d_masked, axis=2) + new_dists = paddle.unsqueeze(paddle.transpose(new_dists, [2, 0, 1]), + axis=1) + multi_dists.append(new_dists) + + multi_dists = paddle.concat(multi_dists, axis=1) + multi_dists = (F.sigmoid(multi_dists + + paddle.reshape(dis_bias, [-1, 1, 1, 1])) - 0.5) * 2 + + if use_float16: + multi_dists = paddle.cast(multi_dists, dtype="float32") + + if (height, width) != ori_size: + multi_dists = F.interpolate(multi_dists, + size=ori_size, + mode='bilinear', + align_corners=True) + multi_dists = paddle.transpose(multi_dists, perm=[2, 3, 0, 1]) + multi_dists = paddle.reshape(multi_dists, + [1, ori_size[0], ori_size[1], obj_num, -1]) + + return multi_dists + + +def calculate_attention_head(ref_embedding, + ref_label, + prev_embedding, + prev_label, + epsilon=1e-5): + + ref_head = ref_embedding * ref_label + ref_head_pos = paddle.sum(ref_head, axis=(2, 3)) + ref_head_neg = paddle.sum(ref_embedding, axis=(2, 3)) - ref_head_pos + ref_pos_num = paddle.sum(ref_label, axis=(2, 3)) + ref_neg_num = paddle.sum(1. - ref_label, axis=(2, 3)) + ref_head_pos = ref_head_pos / (ref_pos_num + epsilon) + ref_head_neg = ref_head_neg / (ref_neg_num + epsilon) + + prev_head = prev_embedding * prev_label + prev_head_pos = paddle.sum(prev_head, axis=(2, 3)) + prev_head_neg = paddle.sum(prev_embedding, axis=(2, 3)) - prev_head_pos + prev_pos_num = paddle.sum(prev_label, axis=(2, 3)) + prev_neg_num = paddle.sum(1. - prev_label, axis=(2, 3)) + prev_head_pos = prev_head_pos / (prev_pos_num + epsilon) + prev_head_neg = prev_head_neg / (prev_neg_num + epsilon) + + total_head = paddle.concat( + x=[ref_head_pos, ref_head_neg, prev_head_pos, prev_head_neg], axis=1) + + return total_head + + +def calculate_attention_head_for_eval(ref_embeddings, + ref_labels, + prev_embedding, + prev_label, + epsilon=1e-5): + total_ref_head_pos = 0. + total_ref_head_neg = 0. + total_ref_pos_num = 0. + total_ref_neg_num = 0. + + for idx in range(len(ref_embeddings)): + ref_embedding = ref_embeddings[idx] + ref_label = ref_labels[idx] + ref_head = ref_embedding * ref_label + ref_head_pos = paddle.sum(ref_head, axis=(2, 3)) + ref_head_neg = paddle.sum(ref_embedding, axis=(2, 3)) - ref_head_pos + ref_pos_num = paddle.sum(ref_label, axis=(2, 3)) + ref_neg_num = paddle.sum(1. - ref_label, axis=(2, 3)) + total_ref_head_pos = total_ref_head_pos + ref_head_pos + total_ref_head_neg = total_ref_head_neg + ref_head_neg + total_ref_pos_num = total_ref_pos_num + ref_pos_num + total_ref_neg_num = total_ref_neg_num + ref_neg_num + ref_head_pos = total_ref_head_pos / (total_ref_pos_num + epsilon) + ref_head_neg = total_ref_head_neg / (total_ref_neg_num + epsilon) + + prev_head = prev_embedding * prev_label + prev_head_pos = paddle.sum(prev_head, axis=(2, 3)) + prev_head_neg = paddle.sum(prev_embedding, axis=(2, 3)) - prev_head_pos + prev_pos_num = paddle.sum(prev_label, axis=(2, 3)) + prev_neg_num = paddle.sum(1. - prev_label, axis=(2, 3)) + prev_head_pos = prev_head_pos / (prev_pos_num + epsilon) + prev_head_neg = prev_head_neg / (prev_neg_num + epsilon) + + total_head = paddle.concat( + x=[ref_head_pos, ref_head_neg, prev_head_pos, prev_head_neg], axis=1) + return total_head diff --git a/paddlevideo/modeling/framework/segmenters/__init__.py b/paddlevideo/modeling/framework/segmenters/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..de4bf573426a6b53831bbab6a5c59ab22df8e775 --- /dev/null +++ b/paddlevideo/modeling/framework/segmenters/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from .base import BaseSegmenter +from .ms_tcn import MSTCN +from .asrf import ASRF + +__all__ = ['BaseSegmenter', 'MSTCN', 'ASRF'] diff --git a/paddlevideo/modeling/framework/segmenters/asrf.py b/paddlevideo/modeling/framework/segmenters/asrf.py new file mode 100644 index 0000000000000000000000000000000000000000..3d962c714e49e131078def93f6fd414d38af76c9 --- /dev/null +++ b/paddlevideo/modeling/framework/segmenters/asrf.py @@ -0,0 +1,143 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import SEGMENTERS +from .base import BaseSegmenter + +import paddle +import paddle.nn.functional as F +from .utils import ASRFPostProcessing + + +@SEGMENTERS.register() +class ASRF(BaseSegmenter): + """ASRF model framework.""" + + def __init__(self, + postprocessing_method, + boundary_threshold, + backbone=None, + head=None, + loss=None): + + super().__init__(backbone=backbone, head=head, loss=loss) + self.postprocessing_method = postprocessing_method + self.boundary_threshold = boundary_threshold + + def forward_net(self, video_feature): + """Define how the model is going to train, from input to output. + """ + if self.backbone is not None: + feature = self.backbone(video_feature) + else: + feature = video_feature + + if self.head is not None: + network_outputs = self.head(feature) + else: + network_outputs = None + + return network_outputs + + def train_step(self, data_batch): + """Training step. + """ + feature, label, boundary = data_batch + # call forward + outputs_cls, outputs_boundary = self.forward_net(feature) + + # transfer data + outputs_cls_np = outputs_cls[-1].numpy() + outputs_boundary_np = outputs_boundary[-1].numpy() + + # caculate loss + if self.loss is not None: + output_loss = self.loss(feature, outputs_cls, label, + outputs_boundary, boundary) + else: + output_loss = None + + # predict post process + predicted = ASRFPostProcessing(outputs_cls_np, outputs_boundary_np, + self.postprocessing_method) + predicted = paddle.squeeze(predicted) + + loss_metrics = dict() + loss_metrics['loss'] = output_loss + loss_metrics['F1@0.50'] = self.head.get_F1_score(predicted, label) + + return loss_metrics + + def val_step(self, data_batch): + """Validating setp. + """ + feature, label, boundary = data_batch + + # call forward + outputs_cls, outputs_boundary = self.forward_net(feature) + + # transfer data + outputs_cls_np = outputs_cls[-1].numpy() + outputs_boundary_np = outputs_boundary[-1].numpy() + + ## caculate loss + if self.loss is not None: + output_loss = self.loss(feature, outputs_cls, label, + outputs_boundary, boundary) + else: + output_loss = None + + # predict post process + predicted = ASRFPostProcessing(outputs_cls_np, outputs_boundary_np, + self.postprocessing_method) + predicted = paddle.squeeze(predicted) + + outputs_dict = dict() + outputs_dict['loss'] = output_loss + outputs_dict['F1@0.50'] = self.head.get_F1_score(predicted, label) + return outputs_dict + + def test_step(self, data_batch): + """Testing setp. + """ + feature, _, _ = data_batch + + outputs_dict = dict() + # call forward + outputs_cls, outputs_boundary = self.forward_net(feature) + # transfer data + outputs_cls_np = outputs_cls[-1].numpy() + outputs_boundary_np = outputs_boundary[-1].numpy() + + # predict post process + predicted = ASRFPostProcessing(outputs_cls_np, outputs_boundary_np, + self.postprocessing_method) + outputs_dict['predict'] = paddle.to_tensor(predicted[0, :]) + outputs_dict['output_np'] = F.sigmoid(outputs_cls[-1]) + return outputs_dict + + def infer_step(self, data_batch): + """Infering setp. + """ + feature = data_batch[0] + + # call forward + outputs_cls, outputs_boundary = self.forward_net(feature) + # transfer data + outputs_cls_np = outputs_cls[-1] + outputs_boundary_np = outputs_boundary[-1] + + outputs = [ + outputs_cls_np, outputs_boundary_np, + F.sigmoid(outputs_cls[-1]) + ] + return outputs diff --git a/paddlevideo/modeling/framework/segmenters/base.py b/paddlevideo/modeling/framework/segmenters/base.py new file mode 100644 index 0000000000000000000000000000000000000000..e0856d9ad24af228479eed7f30ec7ab6dd81172f --- /dev/null +++ b/paddlevideo/modeling/framework/segmenters/base.py @@ -0,0 +1,100 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from abc import abstractmethod +from ... import builder +import paddle.nn as nn + + +class BaseSegmenter(nn.Layer): + """Base class for segementers. + + All segementers should subclass it. + All subclass should overwrite: + + - Methods:``train_step``, supporting to forward when training. + - Methods:``valid_step``, supporting to forward when validating. + - Methods:``test_step``, supporting to forward when testing. + + Args: + backbone (dict): Backbone modules to extract feature. + head (dict): Classification head to process feature. + + """ + + def __init__(self, backbone=None, head=None, loss=None): + + super().__init__() + # build backbone + if backbone is not None: + self.backbone = builder.build_backbone(backbone) + if hasattr(self.backbone, 'init_weights'): + self.backbone.init_weights() + else: + self.backbone = None + # build head + if head is not None: + self.head_name = head.name + self.head = builder.build_head(head) + if hasattr(self.head, 'init_weights'): + self.head.init_weights() + else: + self.head = None + # build loss + if loss is not None: + self.loss_name = loss.name + self.loss = builder.build_loss(loss) + if hasattr(self.loss, 'init_weights'): + self.loss.init_weights() + else: + self.loss = None + + def forward(self, data_batch, mode='infer'): + """ + 1. Define how the model is going to run, from input to output. + 2. Console of train, valid, test or infer step + 3. Set mode='infer' is used for saving inference model, refer to tools/export_model.py + """ + if mode == 'train': + return self.train_step(data_batch) + elif mode == 'valid': + return self.val_step(data_batch) + elif mode == 'test': + return self.test_step(data_batch) + elif mode == 'infer': + return self.infer_step(data_batch) + else: + raise NotImplementedError + + @abstractmethod + def train_step(self, data_batch, **kwargs): + """Training step. + """ + raise NotImplementedError + + @abstractmethod + def val_step(self, data_batch, **kwargs): + """Validating step. + """ + raise NotImplementedError + + @abstractmethod + def test_step(self, data_batch, **kwargs): + """Test step. + """ + raise NotImplementedError + + @abstractmethod + def infer_step(self, data_batch, **kwargs): + """Infer step. + """ + raise NotImplementedError diff --git a/paddlevideo/modeling/framework/segmenters/ms_tcn.py b/paddlevideo/modeling/framework/segmenters/ms_tcn.py new file mode 100644 index 0000000000000000000000000000000000000000..a5982a7c990c2b505ceeea9f75a544ffcac9ba19 --- /dev/null +++ b/paddlevideo/modeling/framework/segmenters/ms_tcn.py @@ -0,0 +1,101 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +from ...registry import SEGMENTERS +from .base import BaseSegmenter + +import paddle +import paddle.nn.functional as F + + +@SEGMENTERS.register() +class MSTCN(BaseSegmenter): + """MS-TCN model framework.""" + + def forward_net(self, video_feature): + """Define how the model is going to train, from input to output. + """ + if self.backbone is not None: + feature = self.backbone(video_feature) + else: + feature = video_feature + + if self.head is not None: + cls_score = self.head(feature) + else: + cls_score = None + + return cls_score + + def train_step(self, data_batch): + """Training step. + """ + video_feat, video_gt = data_batch + + # call forward + output = self.forward_net(video_feat) + loss = 0. + for i in range(len(output)): + loss += self.head.loss(output[i], video_gt) + + predicted = paddle.argmax(output[-1], axis=1) + predicted = paddle.squeeze(predicted) + + loss_metrics = dict() + loss_metrics['loss'] = loss + loss_metrics['F1@0.50'] = self.head.get_F1_score(predicted, video_gt) + return loss_metrics + + def val_step(self, data_batch): + """Validating setp. + """ + video_feat, video_gt = data_batch + + # call forward + output = self.forward_net(video_feat) + loss = 0. + for i in range(len(output)): + loss += self.head.loss(output[i], video_gt) + + predicted = paddle.argmax(output[-1], axis=1) + predicted = paddle.squeeze(predicted) + + outputs_dict = dict() + outputs_dict['loss'] = loss + outputs_dict['F1@0.50'] = self.head.get_F1_score(predicted, video_gt) + return outputs_dict + + def test_step(self, data_batch): + """Testing setp. + """ + video_feat, _ = data_batch + + outputs_dict = dict() + # call forward + output = self.forward_net(video_feat) + predicted = paddle.argmax(output[-1], axis=1) + predicted = paddle.squeeze(predicted) + outputs_dict['predict'] = predicted + outputs_dict['output_np'] = F.sigmoid(output[-1]) + return outputs_dict + + def infer_step(self, data_batch): + """Infering setp. + """ + video_feat = data_batch[0] + + # call forward + output = self.forward_net(video_feat) + predicted = paddle.argmax(output[-1], axis=1) + predicted = paddle.squeeze(predicted) + output_np = F.sigmoid(output[-1]) + return predicted, output_np diff --git a/paddlevideo/modeling/framework/segmenters/utils.py b/paddlevideo/modeling/framework/segmenters/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9c21cbb869a0f598a1a35e5267f5187a0a1249d6 --- /dev/null +++ b/paddlevideo/modeling/framework/segmenters/utils.py @@ -0,0 +1,343 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +# https://github.com/yiskw713/asrf/libs/postprocess.py + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np +import math + + +class GaussianSmoothing(nn.Layer): + """ + Apply gaussian smoothing on a 1d tensor. + Filtering is performed seperately for each channel + in the input using a depthwise convolution. + Arguments: + channels (int, sequence): Number of channels of the input tensors. Output will + have this number of channels as well. + kernel_size (int, sequence): Size of the gaussian kernel. + sigma (float, sequence): Standard deviation of the gaussian kernel. + """ + + def __init__(self, kernel_size=15, sigma=1.0): + super().__init__() + self.kernel_size = kernel_size + + # The gaussian kernel is the product of the + # gaussian function of each dimension. + kernel = 1 + meshgrid = paddle.arange(kernel_size) + + meshgrid = paddle.cast(meshgrid, dtype='float32') + + mean = (kernel_size - 1) / 2 + kernel = kernel / (sigma * math.sqrt(2 * math.pi)) + kernel = kernel * paddle.exp(-(((meshgrid - mean) / sigma)**2) / 2) + + # Make sure sum of values in gaussian kernel equals 1. + # kernel = kernel / paddle.max(kernel) + + self.kernel = paddle.reshape(kernel, [1, 1, -1]) + + def forward(self, inputs): + """ + Apply gaussian filter to input. + Arguments: + input (paddle.Tensor): Input to apply gaussian filter on. + Returns: + filtered (paddle.Tensor): Filtered output. + """ + _, c, _ = inputs.shape + inputs = F.pad(inputs, + pad=((self.kernel_size - 1) // 2, + (self.kernel_size - 1) // 2), + mode="reflect", + data_format='NCL') + + kernel = paddle.expand(self.kernel, shape=[c, 1, self.kernel_size]) + return F.conv1d(inputs, weight=kernel, groups=c) + + +def argrelmax(prob, threshold=0.7): + """ + Calculate arguments of relative maxima. + prob: np.array. boundary probability maps distributerd in [0, 1] + prob shape is (T) + ignore the peak whose value is under threshold + + Return: + Index of peaks for each batch + """ + # ignore the values under threshold + prob[prob < threshold] = 0.0 + + # calculate the relative maxima of boundary maps + # treat the first frame as boundary + peak = np.concatenate( + [ + np.ones((1), dtype=np.bool), + (prob[:-2] < prob[1:-1]) & (prob[2:] < prob[1:-1]), + np.zeros((1), dtype=np.bool), + ], + axis=0, + ) + + peak_idx = np.where(peak)[0].tolist() + + return peak_idx + + +def is_probability(x): + assert x.ndim == 3 + + if x.shape[1] == 1: + # sigmoid + if x.min() >= 0 and x.max() <= 1: + return True + else: + return False + else: + # softmax + _sum = np.sum(x, axis=1).astype(np.float32) + _ones = np.ones_like(_sum, dtype=np.float32) + return np.allclose(_sum, _ones) + + +def convert2probability(x): + """ + Args: x (N, C, T) + """ + assert x.ndim == 3 + + if is_probability(x): + return x + else: + if x.shape[1] == 1: + # sigmoid + prob = 1 / (1 + np.exp(-x)) + else: + # softmax + prob = np.exp(x) / np.sum(np.exp(x), axis=1) + return prob.astype(np.float32) + + +def convert2label(x): + assert x.ndim == 2 or x.ndim == 3 + + if x.ndim == 2: + return x.astype(np.int64) + else: + if not is_probability(x): + x = convert2probability(x) + + label = np.argmax(x, axis=1) + return label.astype(np.int64) + + +def refinement_with_boundary(outputs, boundaries, boundary_threshold): + """ + Get segments which is defined as the span b/w two boundaries, + and decide their classes by majority vote. + Args: + outputs: numpy array. shape (N, C, T) + the model output for frame-level class prediction. + boundaries: numpy array. shape (N, 1, T) + boundary prediction. + boundary_threshold: the threshold of the size of action segments. float(default=0.7) + Return: + preds: np.array. shape (N, T) + final class prediction considering boundaries. + """ + + preds = convert2label(outputs) + boundaries = convert2probability(boundaries) + + for i, (output, pred, boundary) in enumerate(zip(outputs, preds, + boundaries)): + idx = argrelmax(boundary[0, :], threshold=boundary_threshold) + + # add the index of the last action ending + T = pred.shape[0] + idx.append(T) + + # majority vote + for j in range(len(idx) - 1): + count = np.bincount(pred[idx[j]:idx[j + 1]]) + modes = np.where(count == count.max())[0] + if len(modes) == 1: + mode = modes + else: + if outputs.ndim == 3: + # if more than one majority class exist + prob_sum_max = 0 + for m in modes: + prob_sum = output[m, idx[j]:idx[j + 1]].sum() + if prob_sum_max < prob_sum: + mode = m + prob_sum_max = prob_sum + else: + # decide first mode when more than one majority class + # have the same number during oracle experiment + mode = modes[0] + + preds[i, idx[j]:idx[j + 1]] = mode + return preds + + +def relabeling(outputs, theta_t): + """ + Relabeling small action segments with their previous action segment + Args: + output: the results of action segmentation. (N, T) or (N, C, T) + theta_t: the threshold of the size of action segments. + Return: + relabeled output. (N, T) + """ + + preds = convert2label(outputs) + + for i in range(preds.shape[0]): + # shape (T,) + last = preds[i][0] + cnt = 1 + for j in range(1, preds.shape[1]): + if last == preds[i][j]: + cnt += 1 + else: + if cnt > theta_t: + cnt = 1 + last = preds[i][j] + else: + preds[i][j - cnt:j] = preds[i][j - cnt - 1] + cnt = 1 + last = preds[i][j] + + if cnt <= theta_t: + preds[i][j - cnt:j] = preds[i][j - cnt - 1] + + return preds + + +def smoothing(outputs, filter_func): + """ + Smoothing action probabilities with gaussian filter. + Args: + outputs: frame-wise action probabilities. (N, C, T) + Return: + predictions: final prediction. (N, T) + """ + + outputs = convert2probability(outputs) + outputs = filter_func(paddle.to_tensor(outputs)).numpy() + + preds = convert2label(outputs) + return preds + + +def ASRFPostProcessing(outputs_cls, + outputs_boundary, + refinement_method, + boundary_threshold=0.7, + theta_t=15, + kernel_size=15): + """ + ASRF post processing is to refine action boundary + Args: + outputs_cls: the results of action segmentation. (N, T) or (N, C, T) + outputs_boundary: action boundary probability. (N, 1, T) + refinement_method: the way of refine predict boundary and classification. str + boundary_threshold: the threshold of the size of action segments. float(default=0.7) + theta_t: the threshold of the size of action segments. int(default=15) + kernel_size: Size of the gaussian kernel. int(default=15) + Return: + preds output. (N, T) + """ + func = [ + "refinement_with_boundary", + "relabeling", + "smoothing", + ] + + if refinement_method == "smoothing": + filter_func = GaussianSmoothing(kernel_size) + preds = smoothing(outputs_cls, filter_func) + elif refinement_method == "relabeling": + preds = relabeling(outputs_cls, theta_t) + elif refinement_method == "refinement_with_boundary": + preds = refinement_with_boundary(outputs_cls, outputs_boundary, + boundary_threshold) + else: + preds = np.zeros((1, 1)) + assert refinement_method in func + + return paddle.to_tensor(preds) + + +def _calculate_fan_in_and_fan_out(tensor): + dimensions = len(tensor.shape) + if dimensions < 2: + raise ValueError("Fan in and fan out can not be computed \ + for tensor with fewer than 2 dimensions") + + if dimensions == 2: # Linear + fan_in = tensor.shape[1] + fan_out = tensor.shape[0] + else: + num_input_fmaps = tensor.shape[1] + num_output_fmaps = tensor.shape[0] + receptive_field_size = 1 + if tensor.dim() > 2: + receptive_field_size = tensor[0][0].numel() + fan_in = num_input_fmaps * receptive_field_size + fan_out = num_output_fmaps * receptive_field_size + + return fan_in, fan_out + + +def calculate_gain(nonlinearity=None, a=None): + if nonlinearity == 'tanh': + return 5.0 / 3 + elif nonlinearity == 'relu': + return math.sqrt(2.0) + elif nonlinearity == 'leaky_relu': + if a is not None: + return math.sqrt(2.0 / (1 + a**2)) + else: + return math.sqrt(2.0 / (1 + 0.01**2)) + elif nonlinearity == 'selu': + return 3.0 / 4 + else: + return 1 + + +def KaimingUniform_like_torch(weight_npy, + mode='fan_in', + nonlinearity='leaky_relu'): + fan_in, fan_out = _calculate_fan_in_and_fan_out(weight_npy) + if mode == 'fan_in': + fan_mode = fan_in + else: + fan_mode = fan_out + a = math.sqrt(5.0) + gain = calculate_gain(nonlinearity=nonlinearity, a=a) + std = gain / math.sqrt(fan_mode) + bound = math.sqrt(3.0) * std + return np.random.uniform(-bound, bound, weight_npy.shape) + + +def init_bias(weight_npy, bias_npy): + fan_in, fan_out = _calculate_fan_in_and_fan_out(weight_npy) + bound = 1.0 / math.sqrt(fan_in) + return np.random.uniform(-bound, bound, bias_npy.shape) diff --git a/paddlevideo/modeling/heads/__init__.py b/paddlevideo/modeling/heads/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..79ff4fd81a2b3d0338779bf970a22f113283f7b6 --- /dev/null +++ b/paddlevideo/modeling/heads/__init__.py @@ -0,0 +1,46 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .adds_head import AddsHead +from .asrf_head import ASRFHead +from .attention_lstm_head import AttentionLstmHead +from .base import BaseHead +from .bbox_head import BBoxHeadAVA +from .cfbi_head import CollaborativeEnsemblerMS +from .i3d_head import I3DHead +from .movinet_head import MoViNetHead +from .ms_tcn_head import MSTCNHead +from .pptimesformer_head import ppTimeSformerHead +from .pptsm_head import ppTSMHead +from .pptsn_head import ppTSNHead +from .roi_head import AVARoIHead +from .single_straight3d import SingleRoIExtractor3D +from .slowfast_head import SlowFastHead +from .stgcn_head import STGCNHead +from .timesformer_head import TimeSformerHead +from .transnetv2_head import TransNetV2Head +from .tsm_head import TSMHead +from .tsn_head import TSNHead +from .ms_tcn_head import MSTCNHead +from .asrf_head import ASRFHead +from .ctrgcn_head import CTRGCNHead +from .movinet_head import MoViNetHead + +__all__ = [ + 'BaseHead', 'TSNHead', 'TSMHead', 'ppTSMHead', 'ppTSNHead', 'SlowFastHead', + 'AttentionLstmHead', 'TimeSformerHead', 'STGCNHead', 'TransNetV2Head', + 'I3DHead', 'SingleRoIExtractor3D', 'AVARoIHead', 'BBoxHeadAVA', 'AddsHead', + 'ppTimeSformerHead', 'CollaborativeEnsemblerMS', 'MSTCNHead', 'ASRFHead', + 'MoViNetHead', 'CTRGCNHead' +] diff --git a/paddlevideo/modeling/heads/adds_head.py b/paddlevideo/modeling/heads/adds_head.py new file mode 100644 index 0000000000000000000000000000000000000000..3b1cd2462320484dd1d65e13939f55c876c5bbd6 --- /dev/null +++ b/paddlevideo/modeling/heads/adds_head.py @@ -0,0 +1,146 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cv2 +import numpy as np +import paddle.nn as nn +from paddlevideo.utils import get_dist_info +import paddle +from ..builder import build_loss +from ..registry import HEADS + +MIN_DEPTH = 1e-3 +MAX_DEPTH = 80 + + +@HEADS.register() +class AddsHead(nn.Layer): + """TimeSformerHead Head. + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + std(float): Std(Scale) value in normal initilizar. Default: 0.01. + kwargs (dict, optional): Any keyword argument to initialize. + + """ + def __init__(self, + avg_reprojection, + disparity_smoothness, + no_ssim, + loss_cfg=dict(name='ADDSLoss'), + max_gt_depth=60, + pred_depth_scale_factor=1): + + super(AddsHead, self).__init__() + loss_cfg['avg_reprojection'] = avg_reprojection + loss_cfg['disparity_smoothness'] = disparity_smoothness + loss_cfg['no_ssim'] = no_ssim + self.max_gt_depth = max_gt_depth + self.pred_depth_scale_factor = pred_depth_scale_factor + self.loss_func = build_loss(loss_cfg) + + def forward(self): + raise NotImplemented + + def loss(self, inputs, outputs): + if self.training: + return self.loss_func(inputs, outputs) + else: + abs_rel, sq_rel, rmse, rmse_log, a1, a2, a3 = self.get_metrics( + outputs['pred_disp'], outputs['gt']) + outputs['abs_rel'] = abs_rel + outputs['sq_rel'] = sq_rel + outputs['rmse'] = rmse + outputs['rmse_log'] = rmse_log + outputs['a1'] = a1 + outputs['a2'] = a2 + outputs['a3'] = a3 + return outputs + + def get_metrics(self, pred_disp, gt_depth): + gt_height, gt_width = gt_depth.shape[:2] + + pred_disp = cv2.resize(pred_disp, (gt_width, gt_height)) + pred_depth = 1 / pred_disp + + mask = gt_depth > 0 + + pred_depth = pred_depth[mask] + gt_depth = gt_depth[mask] + + pred_depth *= self.pred_depth_scale_factor + ratio = np.median(gt_depth) / np.median(pred_depth) + pred_depth *= ratio + + pred_depth[pred_depth < MIN_DEPTH] = MIN_DEPTH + pred_depth[pred_depth > MAX_DEPTH] = MAX_DEPTH + + mask2 = gt_depth <= self.max_gt_depth + pred_depth = pred_depth[mask2] + gt_depth = gt_depth[mask2] + + abs_rel, sq_rel, rmse, rmse_log, a1, a2, a3 = self.compute_errors( + gt_depth, pred_depth) + + _, world_size = get_dist_info() + if world_size > 1: + # educe sum when valid + # TODO: there are some problems with multi gpu gather code. + abs_rel = paddle.to_tensor(abs_rel) + sq_rel = paddle.to_tensor(sq_rel) + rmse = paddle.to_tensor(rmse) + rmse_log = paddle.to_tensor(rmse_log) + a1 = paddle.to_tensor(a1) + a2 = paddle.to_tensor(a2) + a3 = paddle.to_tensor(a3) + abs_rel = paddle.distributed.all_reduce( + abs_rel, op=paddle.distributed.ReduceOp.SUM) / world_size + sq_rel = paddle.distributed.all_reduce( + sq_rel, op=paddle.distributed.ReduceOp.SUM) / world_size + rmse = paddle.distributed.all_reduce( + rmse, op=paddle.distributed.ReduceOp.SUM) / world_size + rmse_log = paddle.distributed.all_reduce( + rmse_log, op=paddle.distributed.ReduceOp.SUM) / world_size + a1 = paddle.distributed.all_reduce( + a1, op=paddle.distributed.ReduceOp.SUM) / world_size + a2 = paddle.distributed.all_reduce( + a2, op=paddle.distributed.ReduceOp.SUM) / world_size + a3 = paddle.distributed.all_reduce( + a3, op=paddle.distributed.ReduceOp.SUM) / world_size + return abs_rel.item(), sq_rel.item(), rmse.item(), rmse_log.item( + ), a1.item(), a2.item(), a3.item() + + return abs_rel, sq_rel, rmse, rmse_log, a1, a2, a3 + + def compute_errors(self, gt, pred): + """Computation of error metrics between predicted and ground truth depths + """ + thresh = np.maximum((gt / pred), (pred / gt)) + a1 = (thresh < 1.25).mean() + a2 = (thresh < 1.25**2).mean() + a3 = (thresh < 1.25**3).mean() + + rmse = (gt - pred)**2 + rmse = np.sqrt(rmse.mean()) + + rmse_log = (np.log(gt) - np.log(pred))**2 + rmse_log = np.sqrt(rmse_log.mean()) + + abs_rel = np.mean(np.abs(gt - pred) / gt) + + sq_rel = np.mean(((gt - pred)**2) / gt) + + return abs_rel, sq_rel, rmse, rmse_log, a1, a2, a3 diff --git a/paddlevideo/modeling/heads/asrf_head.py b/paddlevideo/modeling/heads/asrf_head.py new file mode 100644 index 0000000000000000000000000000000000000000..c3aab77add49e2d8f5853b3483505cadf1c8a43a --- /dev/null +++ b/paddlevideo/modeling/heads/asrf_head.py @@ -0,0 +1,212 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://github.com/yiskw713/asrf/libs/models/tcn.py +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np + +from paddle import ParamAttr + +from ..backbones.ms_tcn import SingleStageModel + +from .base import BaseHead +from ..registry import HEADS +from ..weight_init import weight_init_ +from ..framework.segmenters.utils import init_bias, KaimingUniform_like_torch + + +@HEADS.register() +class ASRFHead(BaseHead): + + def __init__(self, + num_classes, + num_features, + num_stages, + num_layers, + num_stages_asb=None, + num_stages_brb=None): + super().__init__(num_classes=num_classes, in_channels=num_features) + if not isinstance(num_stages_asb, int): + num_stages_asb = num_stages + + if not isinstance(num_stages_brb, int): + num_stages_brb = num_stages + + self.num_layers = num_layers + self.num_stages_asb = num_stages_asb + self.num_stages_brb = num_stages_brb + self.num_features = num_features + + # cls score + self.overlap = 0.5 + + self.conv_cls = nn.Conv1D(self.num_features, self.num_classes, 1) + self.conv_boundary = nn.Conv1D(self.num_features, 1, 1) + + # action segmentation branch + asb = [ + SingleStageModel(self.num_layers, self.num_features, + self.num_classes, self.num_classes) + for _ in range(self.num_stages_asb - 1) + ] + + # boundary regression branch + brb = [ + SingleStageModel(self.num_layers, self.num_features, 1, 1) + for _ in range(self.num_stages_brb - 1) + ] + self.brb = nn.LayerList(brb) + self.asb = nn.LayerList(asb) + + self.activation_asb = nn.Softmax(axis=1) + self.activation_brb = nn.Sigmoid() + + def init_weights(self): + """ + initialize model layers' weight + """ + # init weight + for layer in self.sublayers(): + if isinstance(layer, nn.Conv1D): + layer.weight.set_value( + KaimingUniform_like_torch(layer.weight).astype('float32')) + if layer.bias is not None: + layer.bias.set_value( + init_bias(layer.weight, layer.bias).astype('float32')) + + def forward(self, x): + """ + ASRF head + """ + out_cls = self.conv_cls(x) + out_boundary = self.conv_boundary(x) + + outputs_cls = [out_cls] + outputs_boundary = [out_boundary] + + for as_stage in self.asb: + out_cls = as_stage(self.activation_asb(out_cls)) + outputs_cls.append(out_cls) + + for br_stage in self.brb: + out_boundary = br_stage(self.activation_brb(out_boundary)) + outputs_boundary.append(out_boundary) + + return outputs_cls, outputs_boundary + + def get_F1_score(self, predicted, groundTruth): + recog_content = list(predicted.numpy()) + gt_content = list(groundTruth[0].numpy()) + + # cls score + correct = 0 + total = 0 + edit = 0 + + for i in range(len(gt_content)): + total += 1 + + if gt_content[i] == recog_content[i]: + correct += 1 + + edit_num = self.edit_score(recog_content, gt_content) + edit += edit_num + + tp, fp, fn = self.f_score(recog_content, gt_content, self.overlap) + + # cls metric + + precision = tp / float(tp + fp) + recall = tp / float(fp + fn) + + if precision + recall > 0.0: + f1 = 2.0 * (precision * recall) / (precision + recall) + else: + f1 = 0.0 + f1 = np.nan_to_num(f1) + return f1 + + def get_labels_start_end_time(self, frame_wise_labels): + labels = [] + starts = [] + ends = [] + last_label = frame_wise_labels[0] + labels.append(frame_wise_labels[0]) + starts.append(0) + for i in range(len(frame_wise_labels)): + if frame_wise_labels[i] != last_label: + labels.append(frame_wise_labels[i]) + starts.append(i) + ends.append(i) + last_label = frame_wise_labels[i] + ends.append(i + 1) + return labels, starts, ends + + def levenstein(self, p, y, norm=False): + m_row = len(p) + n_col = len(y) + D = np.zeros([m_row + 1, n_col + 1], np.float) + for i in range(m_row + 1): + D[i, 0] = i + for i in range(n_col + 1): + D[0, i] = i + + for j in range(1, n_col + 1): + for i in range(1, m_row + 1): + if y[j - 1] == p[i - 1]: + D[i, j] = D[i - 1, j - 1] + else: + D[i, j] = min(D[i - 1, j] + 1, D[i, j - 1] + 1, + D[i - 1, j - 1] + 1) + + if norm: + score = (1 - D[-1, -1] / max(m_row, n_col)) * 100 + else: + score = D[-1, -1] + + return score + + def edit_score(self, recognized, ground_truth, norm=True): + P, _, _ = self.get_labels_start_end_time(recognized) + Y, _, _ = self.get_labels_start_end_time(ground_truth) + return self.levenstein(P, Y, norm) + + def f_score(self, recognized, ground_truth, overlap): + p_label, p_start, p_end = self.get_labels_start_end_time(recognized) + y_label, y_start, y_end = self.get_labels_start_end_time(ground_truth) + + tp = 0 + fp = 0 + + hits = np.zeros(len(y_label)) + + for j in range(len(p_label)): + intersection = np.minimum(p_end[j], y_end) - np.maximum( + p_start[j], y_start) + union = np.maximum(p_end[j], y_end) - np.minimum( + p_start[j], y_start) + IoU = (1.0 * intersection / union) * ( + [p_label[j] == y_label[x] for x in range(len(y_label))]) + # Get the best scoring segment + idx = np.array(IoU).argmax() + + if IoU[idx] >= overlap and not hits[idx]: + tp += 1 + hits[idx] = 1 + else: + fp += 1 + fn = len(y_label) - sum(hits) + return float(tp), float(fp), float(fn) diff --git a/paddlevideo/modeling/heads/attention_lstm_head.py b/paddlevideo/modeling/heads/attention_lstm_head.py new file mode 100644 index 0000000000000000000000000000000000000000..f3415a307db2a4467e9f4d3a9abab6648feefcc7 --- /dev/null +++ b/paddlevideo/modeling/heads/attention_lstm_head.py @@ -0,0 +1,156 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from paddle import ParamAttr +from paddle.nn.initializer import Normal +from paddle.regularizer import L2Decay + +from ...metrics.youtube8m import eval_util as youtube8m_metrics +from ..registry import HEADS +from ..weight_init import weight_init_ +from .base import BaseHead + + +@HEADS.register() +class AttentionLstmHead(BaseHead): + """AttentionLstmHead. + Args: TODO + """ + def __init__(self, + num_classes=3862, + feature_num=2, + feature_dims=[1024, 128], + embedding_size=512, + lstm_size=1024, + in_channels=2048, + loss_cfg=dict(name='CrossEntropyLoss')): + super(AttentionLstmHead, self).__init__(num_classes, in_channels, + loss_cfg) + self.num_classes = num_classes + self.feature_dims = feature_dims + self.embedding_size = embedding_size + self.lstm_size = lstm_size + self.feature_num = len(self.feature_dims) + for i in range(self.feature_num): # 0:rgb, 1:audio + fc_feature = paddle.nn.Linear(in_features=self.feature_dims[i], + out_features=self.embedding_size) + self.add_sublayer("fc_feature{}".format(i), fc_feature) + + bi_lstm = paddle.nn.LSTM(input_size=self.embedding_size, + hidden_size=self.lstm_size, + direction="bidirectional") + self.add_sublayer("bi_lstm{}".format(i), bi_lstm) + + drop_rate = 0.5 + self.dropout = paddle.nn.Dropout(drop_rate) + + att_fc = paddle.nn.Linear(in_features=self.lstm_size * 2, + out_features=1) + self.add_sublayer("att_fc{}".format(i), att_fc) + self.softmax = paddle.nn.Softmax() + + self.fc_out1 = paddle.nn.Linear(in_features=self.lstm_size * 4, + out_features=8192, + bias_attr=ParamAttr( + regularizer=L2Decay(0.0), + initializer=Normal())) + self.relu = paddle.nn.ReLU() + self.fc_out2 = paddle.nn.Linear(in_features=8192, + out_features=4096, + bias_attr=ParamAttr( + regularizer=L2Decay(0.0), + initializer=Normal())) + self.fc_logit = paddle.nn.Linear(in_features=4096, + out_features=self.num_classes, + bias_attr=ParamAttr( + regularizer=L2Decay(0.0), + initializer=Normal())) + self.sigmoid = paddle.nn.Sigmoid() + + def init_weights(self): + pass + + def forward(self, inputs): + # inputs = [(rgb_data, rgb_len, rgb_mask), (audio_data, audio_len, audio_mask)] + # deal with features with different length + # 1. padding to same lenght, make a tensor + # 2. make a mask tensor with the same shpae with 1 + # 3. compute output using mask tensor, s.t. output is nothing todo with padding + assert (len(inputs) == self.feature_num + ), "Input tensor does not contain {} features".format( + self.feature_num) + att_outs = [] + for i in range(len(inputs)): + # 1. fc + m = getattr(self, "fc_feature{}".format(i)) + output_fc = m(inputs[i][0]) + output_fc = paddle.tanh(output_fc) + + # 2. bi_lstm + m = getattr(self, "bi_lstm{}".format(i)) + lstm_out, _ = m(inputs=output_fc, sequence_length=inputs[i][1]) + + lstm_dropout = self.dropout(lstm_out) + + # 3. att_fc + m = getattr(self, "att_fc{}".format(i)) + lstm_weight = m(lstm_dropout) + + # 4. softmax replace start, for it's relevant to sum in time step + lstm_exp = paddle.exp(lstm_weight) + lstm_mask = paddle.mean(inputs[i][2], axis=2) + lstm_mask = paddle.unsqueeze(lstm_mask, axis=2) + lstm_exp_with_mask = paddle.multiply(x=lstm_exp, y=lstm_mask) + lstm_sum_with_mask = paddle.sum(lstm_exp_with_mask, axis=1) + exponent = -1 + lstm_denominator = paddle.pow(lstm_sum_with_mask, exponent) + lstm_denominator = paddle.unsqueeze(lstm_denominator, axis=2) + lstm_softmax = paddle.multiply(x=lstm_exp, y=lstm_denominator) + lstm_weight = lstm_softmax + # softmax replace end + + lstm_scale = paddle.multiply(x=lstm_dropout, y=lstm_weight) + + # 5. sequence_pool's replace start, for it's relevant to sum in time step + lstm_scale_with_mask = paddle.multiply(x=lstm_scale, y=lstm_mask) + fea_lens = inputs[i][1] + fea_len = int(fea_lens[0]) + lstm_pool = paddle.sum(lstm_scale_with_mask, axis=1) + # sequence_pool's replace end + att_outs.append(lstm_pool) + att_out = paddle.concat(att_outs, axis=1) + fc_out1 = self.fc_out1(att_out) + fc_out1_act = self.relu(fc_out1) + fc_out2 = self.fc_out2(fc_out1_act) + fc_out2_act = paddle.tanh(fc_out2) + fc_logit = self.fc_logit(fc_out2_act) + output = self.sigmoid(fc_logit) + return fc_logit, output + + def loss(self, lstm_logit, labels, **kwargs): + labels.stop_gradient = True + losses = dict() + bce_logit_loss = paddle.nn.BCEWithLogitsLoss(reduction='sum') + sum_cost = bce_logit_loss(lstm_logit, labels) + return sum_cost + + def metric(self, lstm_output, labels): + pred = lstm_output.numpy() + label = labels.numpy() + hit_at_one = youtube8m_metrics.calculate_hit_at_one(pred, label) + perr = youtube8m_metrics.calculate_precision_at_equal_recall_rate( + pred, label) + gap = youtube8m_metrics.calculate_gap(pred, label) + return hit_at_one, perr, gap diff --git a/paddlevideo/modeling/heads/base.py b/paddlevideo/modeling/heads/base.py new file mode 100644 index 0000000000000000000000000000000000000000..887fceef56d2c7b8083889c851428e33b5fb1bc1 --- /dev/null +++ b/paddlevideo/modeling/heads/base.py @@ -0,0 +1,176 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from abc import abstractmethod + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from ..builder import build_loss +from paddlevideo.utils import get_logger, get_dist_info + +logger = get_logger("paddlevideo") + + +class BaseHead(nn.Layer): + """Base class for head part. + + All head should subclass it. + All subclass should overwrite: + + - Methods: ```init_weights```, initializing weights. + - Methods: ```forward```, forward function. + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channels in input feature. + loss_cfg (dict): Config for building loss. Default: dict(type='CrossEntropyLoss'). + ls_eps (float): label smoothing epsilon. Default: 0. . + + """ + + def __init__( + self, + num_classes=None, + in_channels=None, + loss_cfg=dict( + name="CrossEntropyLoss" + ), #TODO(shipping): only pass a name or standard build cfg format. + #multi_class=False, NOTE(shipping): not supported now. + ls_eps=0.): + + super().__init__() + self.num_classes = num_classes + self.in_channels = in_channels + self.loss_func = build_loss(loss_cfg) + #self.multi_class = multi_class NOTE(shipping): not supported now + self.ls_eps = ls_eps + + @abstractmethod + def forward(self, x): + """Define how the head is going to run. + """ + raise NotImplemented + + def loss(self, scores, labels, valid_mode=False, if_top5=True, **kwargs): + """Calculate the loss accroding to the model output ```scores```, + and the target ```labels```. + + Args: + scores (paddle.Tensor): The output of the model. + labels (paddle.Tensor): The target output of the model. + + Returns: + losses (dict): A dict containing field 'loss'(mandatory) and 'top1_acc', 'top5_acc'(optional). + + """ + if len(labels) == 1: #commonly case + labels = labels[0] + losses = dict() + if self.ls_eps != 0. and not valid_mode: # label_smooth + loss = self.label_smooth_loss(scores, labels, **kwargs) + else: + loss = self.loss_func(scores, labels, **kwargs) + if if_top5: + top1, top5 = self.get_acc(scores, labels, valid_mode) + losses['top1'] = top1 + losses['top5'] = top5 + losses['loss'] = loss + else: + top1 = self.get_acc(scores, labels, valid_mode, if_top5) + losses['top1'] = top1 + losses['loss'] = loss + return losses + # MRI目前二分类无top5 + elif len(labels) == 3: # mix_up + labels_a, labels_b, lam = labels + lam = lam[0] # get lam value + losses = dict() + if self.ls_eps != 0: + loss_a = self.label_smooth_loss(scores, labels_a, **kwargs) + loss_b = self.label_smooth_loss(scores, labels_b, **kwargs) + else: + loss_a = self.loss_func(scores, labels_a, **kwargs) + loss_b = self.loss_func(scores, labels_b, **kwargs) + loss = lam * loss_a + (1 - lam) * loss_b + + if if_top5: + top1a, top5a = self.get_acc(scores, labels_a, valid_mode) + top1b, top5b = self.get_acc(scores, labels_b, valid_mode) + top1 = lam * top1a + (1 - lam) * top1b + top5 = lam * top5a + (1 - lam) * top5b + losses['top1'] = top1 + losses['top5'] = top5 + losses['loss'] = loss + + else: + top1a = self.get_acc(scores, labels_a, valid_mode, if_top5) + top1b = self.get_acc(scores, labels_b, valid_mode, if_top5) + top1 = lam * top1a + (1 - lam) * top1b + losses['top1'] = top1 + losses['loss'] = loss + + return losses + else: + raise NotImplemented + + def label_smooth_loss(self, scores, labels, **kwargs): + """ + Args: + scores (paddle.Tensor): [N, num_classes] + labels (paddle.Tensor): [N, ] + Returns: + paddle.Tensor: [1,] + """ + if paddle.fluid.core.is_compiled_with_npu(): + """ + Designed for the lack of temporary operators of NPU, + main idea is to split smooth loss into uniform distribution loss + and hard label calculation + """ + hard_loss = (1.0 - self.ls_eps) * F.cross_entropy(scores, labels) + uniform_loss = (self.ls_eps / self.num_classes) * ( + -F.log_softmax(scores, -1).sum(-1).mean(0)) + loss = hard_loss + uniform_loss + else: + labels = F.one_hot(labels, self.num_classes) + labels = F.label_smooth(labels, epsilon=self.ls_eps) + labels = paddle.squeeze(labels, axis=1) + loss = self.loss_func(scores, labels, soft_label=True, **kwargs) + return loss + + def get_acc(self, scores, labels, valid_mode, if_top5=True): + if if_top5: + top1 = paddle.metric.accuracy(input=scores, label=labels, k=1) + top5 = paddle.metric.accuracy(input=scores, label=labels, k=5) + _, world_size = get_dist_info() + #NOTE(shipping): deal with multi cards validate + if world_size > 1 and valid_mode: #reduce sum when valid + top1 = paddle.distributed.all_reduce( + top1, op=paddle.distributed.ReduceOp.SUM) / world_size + top5 = paddle.distributed.all_reduce( + top5, op=paddle.distributed.ReduceOp.SUM) / world_size + + return top1, top5 + else: + top1 = paddle.metric.accuracy(input=scores, label=labels, k=1) + _, world_size = get_dist_info() + #NOTE(shipping): deal with multi cards validate + if world_size > 1 and valid_mode: #reduce sum when valid + top1 = paddle.distributed.all_reduce( + top1, op=paddle.distributed.ReduceOp.SUM) / world_size + + return top1 diff --git a/paddlevideo/modeling/heads/bbox_head.py b/paddlevideo/modeling/heads/bbox_head.py new file mode 100644 index 0000000000000000000000000000000000000000..688251ebb8499df88a2e544428a5af03fb74ff10 --- /dev/null +++ b/paddlevideo/modeling/heads/bbox_head.py @@ -0,0 +1,225 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np +from .. import builder + +from ..registry import HEADS + +@HEADS.register() +class BBoxHeadAVA(nn.Layer): + """Simplest RoI head, with only two fc layers for classification and + regression respectively. """ + + def __init__( + self, + temporal_pool_type='avg', + spatial_pool_type='max', + in_channels=2048, + num_classes=81,# The first class is reserved, to classify bbox as pos / neg + dropout_ratio=0, + dropout_before_pool=True, + topk=(3, 5), + multilabel=True): + + super(BBoxHeadAVA, self).__init__() + assert temporal_pool_type in ['max', 'avg'] + assert spatial_pool_type in ['max', 'avg'] + self.temporal_pool_type = temporal_pool_type + self.spatial_pool_type = spatial_pool_type + + self.in_channels = in_channels + self.num_classes = num_classes + + self.dropout_ratio = dropout_ratio + self.dropout_before_pool = dropout_before_pool + + self.multilabel = multilabel + if topk is None: + self.topk = () + elif isinstance(topk, int): + self.topk = (topk, ) + elif isinstance(topk, tuple): + assert all([isinstance(k, int) for k in topk]) + self.topk = topk + else: + raise TypeError('topk should be int or tuple[int], ' + f'but get {type(topk)}') + # Class 0 is ignored when calculaing multilabel accuracy, + # so topk cannot be equal to num_classes + assert all([k < num_classes for k in self.topk]) + assert self.multilabel + + in_channels = self.in_channels + if self.temporal_pool_type == 'avg': + self.temporal_pool = nn.AdaptiveAvgPool3D((1, None, None)) + else: + self.temporal_pool = nn.AdaptiveMaxPool3D((1, None, None)) + if self.spatial_pool_type == 'avg': + self.spatial_pool = nn.AdaptiveAvgPool3D((None, 1, 1)) + else: + self.spatial_pool = nn.AdaptiveMaxPool3D((None, 1, 1)) + + if dropout_ratio > 0: + self.dropout = nn.Dropout(dropout_ratio) + + weight_attr = paddle.framework.ParamAttr(name="weight", + initializer=paddle.nn.initializer.Normal(mean=0.0, std=0.01)) + bias_attr = paddle.ParamAttr(name="bias", + initializer=paddle.nn.initializer.Constant(value=0.0)) + + self.fc_cls = nn.Linear(in_channels, num_classes, weight_attr=weight_attr, bias_attr=bias_attr) + + self.debug_imgs = None + + def forward(self, x,rois, rois_num): + roi = paddle.concat(rois) + roi_x1 = paddle.index_select(roi, index=paddle.to_tensor(0), axis=1) + roi_x2 = paddle.index_select(roi, index=paddle.to_tensor(2), axis=1) + roi_w = roi_x2 - roi_x1 + roi_y1 = paddle.index_select(roi, index=paddle.to_tensor(1), axis=1) + roi_y2 = paddle.index_select(roi, index=paddle.to_tensor(3), axis=1) + roi_h = roi_y2 - roi_y1 + roi_area = paddle.multiply(roi_w, roi_h) + A = roi_area + A1 = paddle.full(A.shape, 1, dtype='int32') + A2 = paddle.where(A == 0, paddle.zeros_like(A1), A1) + AE = paddle.expand(A2, [A.shape[0], x.shape[1]]) + rois_num = paddle.to_tensor(rois_num, dtype='int32') + if self.dropout_before_pool and self.dropout_ratio > 0 : + x = self.dropout(x) + x = self.temporal_pool(x) + x = self.spatial_pool(x) + if not self.dropout_before_pool and self.dropout_ratio > 0 : + x = self.dropout(x) + x = paddle.reshape(x, [x.shape[0], -1]) + x = paddle.multiply(x, paddle.cast(AE,"float32")) + cls_score = self.fc_cls(x) + # We do not predict bbox, so return None + return cls_score, None + + def get_targets(self, sampling_results, gt_bboxes, gt_labels, pos_weight): + pos_proposals = [res.pos_bboxes for res in sampling_results] + neg_proposals = [res.neg_bboxes for res in sampling_results] + pos_gt_labels = [res.pos_gt_labels for res in sampling_results] + cls_reg_targets = self.bbox_target(pos_proposals, neg_proposals, + pos_gt_labels, pos_weight) + return cls_reg_targets + + def bbox_target(self, pos_bboxes_list, neg_bboxes_list, gt_labels, pos_weight): + """Generate classification targets for bboxes. """ + labels, label_weights = [], [] + pos_weight = 1.0 if pos_weight <= 0 else pos_weight + + assert len(pos_bboxes_list) == len(neg_bboxes_list) == len(gt_labels) + length = len(pos_bboxes_list) + + for i in range(length): + pos_bboxes = pos_bboxes_list[i] + neg_bboxes = neg_bboxes_list[i] + gt_label = gt_labels[i] + num_pos = pos_bboxes.shape[0] + if neg_bboxes is not None: + num_neg = neg_bboxes.shape[0] + else: + num_neg = 0 + num_samples = num_pos + num_neg + neg_label = paddle.zeros([num_neg, gt_label.shape[1]]) + label = paddle.concat([gt_label,neg_label]) + labels.append(label) + + labels = paddle.concat(labels, 0) + return labels + + def recall_prec(self, pred_vec, target_vec): + correct = paddle.to_tensor(np.logical_and(pred_vec.numpy(), target_vec.numpy())) + correct = paddle.where(correct, + paddle.full(correct.shape,1,dtype='int32'), + paddle.full(correct.shape,0,dtype='int32')) + recall_correct = paddle.cast(paddle.sum(correct, axis=1), 'float32') + target_vec = paddle.where(target_vec, + paddle.full(target_vec.shape,1,dtype='int32'), + paddle.full(target_vec.shape,0,dtype='int32')) + recall_target = paddle.cast(paddle.sum(target_vec, axis=1),'float32') + recall = recall_correct / recall_target + pred_vec = paddle.where(pred_vec, + paddle.full(pred_vec.shape,1,dtype='int32'), + paddle.full(pred_vec.shape,0,dtype='int32')) + prec_target = paddle.cast(paddle.sum(pred_vec, axis=1) + 1e-6, 'float32') + prec = recall_correct / prec_target + recall_mean = paddle.mean(recall) + prec_mean = paddle.mean(prec) + return recall_mean, prec_mean + + def multilabel_accuracy(self, pred, target, thr=0.5): + pred = paddle.nn.functional.sigmoid(pred) + pred_vec = pred > thr + target_vec = target > 0.5 + recall_thr, prec_thr = self.recall_prec(pred_vec, target_vec) + recalls, precs = [], [] + for k in self.topk: + _, pred_label = paddle.topk(pred, k, 1, True, True) + pred_vec = paddle.full(pred.shape,0,dtype='bool') + num_sample = pred.shape[0] + for i in range(num_sample): + pred_vec[i, pred_label[i].numpy()] = 1 + recall_k, prec_k = self.recall_prec(pred_vec, target_vec) + recalls.append(recall_k) + precs.append(prec_k) + return recall_thr, prec_thr, recalls, precs + + def loss(self, + cls_score, + labels): + losses = dict() + if cls_score is not None: + # Only use the cls_score + labels = labels[:, 1:] + pos_inds_bool = paddle.sum(labels, axis=-1) > 0 + pos_inds = paddle.where(paddle.sum(labels, axis=-1) > 0, + paddle.full([labels.shape[0]],1,dtype='int32'), + paddle.full([labels.shape[0]],0,dtype='int32')) + pos_inds = paddle.nonzero(pos_inds, as_tuple=False) + cls_score = paddle.index_select(cls_score, pos_inds, axis=0) + cls_score = cls_score[:, 1:] + labels = paddle.index_select(labels, pos_inds, axis=0) + bce_loss = F.binary_cross_entropy_with_logits + loss = bce_loss(cls_score, labels, reduction='none') + losses['loss'] = paddle.mean(loss) + recall_thr, prec_thr, recall_k, prec_k = self.multilabel_accuracy( + cls_score, labels, thr=0.5) + losses['recall@thr=0.5'] = recall_thr + losses['prec@thr=0.5'] = prec_thr + for i, k in enumerate(self.topk): + losses[f'recall@top{k}'] = recall_k[i] + losses[f'prec@top{k}'] = prec_k[i] + return losses + + def get_det_bboxes(self, + rois, + cls_score, + img_shape, + flip=False, + crop_quadruple=None, + cfg=None): + if isinstance(cls_score, list): + cls_score = sum(cls_score) / float(len(cls_score)) + assert self.multilabel + m = paddle.nn.Sigmoid() + scores = m(cls_score) + bboxes = rois + return bboxes, scores diff --git a/paddlevideo/modeling/heads/cfbi_head.py b/paddlevideo/modeling/heads/cfbi_head.py new file mode 100644 index 0000000000000000000000000000000000000000..f7cbd910ef3c233eb4313d9506cee2cefd1e9746 --- /dev/null +++ b/paddlevideo/modeling/heads/cfbi_head.py @@ -0,0 +1,448 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from .base import BaseHead +from ..registry import HEADS +from ..weight_init import weight_init_ + + +class IA_gate(nn.Layer): + def __init__(self, in_dim, out_dim): + super(IA_gate, self).__init__() + self.IA = nn.Linear(in_dim, out_dim) + + def forward(self, x, IA_head): + a = self.IA(IA_head) + a = 1. + paddle.tanh(a) + a = paddle.unsqueeze(paddle.unsqueeze(a, axis=-1), axis=-1) + x = a * x + return x + + +class GCT(nn.Layer): + def __init__(self, num_channels, epsilon=1e-5, mode='l2', after_relu=False): + super(GCT, self).__init__() + x1 = paddle.zeros([1, num_channels, 1, 1]) + x2 = paddle.ones([1, num_channels, 1, 1]) + self.alpha = paddle.create_parameter( + shape=x2.shape, + dtype=x2.dtype, + default_initializer=nn.initializer.Assign(x2)) + self.alpha.stop_gradient = False + self.gamma = paddle.create_parameter( + shape=x1.shape, + dtype=x1.dtype, + default_initializer=nn.initializer.Assign(x1)) + self.gamma.stop_gradient = False + self.beta = paddle.create_parameter( + shape=x1.shape, + dtype=x1.dtype, + default_initializer=nn.initializer.Assign(x1)) + self.beta.stop_gradient = False + + self.epsilon = epsilon + self.mode = mode + self.after_relu = after_relu + + def forward(self, x): + + if self.mode == 'l2': + embedding = paddle.pow( + paddle.sum(paddle.pow(x, 2), axis=[2, 3], keepdim=True) + + self.epsilon, 0.5) * self.alpha + norm = self.gamma / paddle.pow( + (paddle.mean(paddle.pow(embedding, 2), axis=1, keepdim=True) + + self.epsilon), 0.5) + elif self.mode == 'l1': + if not self.after_relu: + _x = paddle.abs(x) + else: + _x = x + embedding = paddle.sum(_x, axis=(2, 3), keepdim=True) * self.alpha + norm = self.gamma / (paddle.mean( + paddle.abs(embedding), axis=1, keepdim=True) + self.epsilon) + else: + print('Unknown mode!') + exit() + + gate = 1. + paddle.tanh(embedding * norm + self.beta) + + return x * gate + + +class Bottleneck(nn.Layer): + def __init__(self, inplanes, outplanes, stride=1, dilation=1): + super(Bottleneck, self).__init__() + expansion = 4 + planes = int(outplanes / expansion) + + self.GCT1 = GCT(inplanes) + self.conv1 = nn.Conv2D(inplanes, planes, kernel_size=1, bias_attr=False) + self.bn1 = nn.GroupNorm(num_groups=32, num_channels=planes) + + self.conv2 = nn.Conv2D(planes, + planes, + kernel_size=3, + stride=stride, + dilation=dilation, + padding=dilation, + bias_attr=False) + self.bn2 = nn.GroupNorm(num_groups=32, num_channels=planes) + + self.conv3 = nn.Conv2D(planes, + planes * expansion, + kernel_size=1, + bias_attr=False) + self.bn3 = nn.GroupNorm(num_groups=32, num_channels=planes * expansion) + self.relu = nn.ReLU() + if stride != 1 or inplanes != planes * expansion: + downsample = nn.Sequential( + nn.Conv2D(inplanes, + planes * expansion, + kernel_size=1, + stride=stride, + bias_attr=False), + nn.GroupNorm(num_groups=32, num_channels=planes * expansion), + ) + else: + downsample = None + self.downsample = downsample + + self.stride = stride + self.dilation = dilation + + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + nn.initializer.KaimingNormal() + + def forward(self, x): + residual = x + + out = self.GCT1(x) + out = self.conv1(out) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class _ASPPModule(nn.Layer): + def __init__(self, inplanes, planes, kernel_size, padding, dilation): + super(_ASPPModule, self).__init__() + self.GCT = GCT(inplanes) + self.atrous_conv = nn.Conv2D(inplanes, + planes, + kernel_size=kernel_size, + stride=1, + padding=padding, + dilation=dilation, + bias_attr=False) + self.bn = nn.GroupNorm(num_groups=int(planes / 4), num_channels=planes) + self.relu = nn.ReLU() + + self._init_weight() + + def forward(self, x): + x = self.GCT(x) + x = self.atrous_conv(x) + x = self.bn(x) + + return self.relu(x) + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + nn.initializer.KaimingNormal() + elif isinstance(m, nn.GroupNorm): + m.weight.data = nn.initializer.Constant(1) + m.bias.data = nn.initializer.Constant(0) + + +class ASPP(nn.Layer): + def __init__(self): + super(ASPP, self).__init__() + + inplanes = 512 + dilations = [1, 6, 12, 18] + + self.aspp1 = _ASPPModule(inplanes, + 128, + 1, + padding=0, + dilation=dilations[0]) + self.aspp2 = _ASPPModule(inplanes, + 128, + 3, + padding=dilations[1], + dilation=dilations[1]) + self.aspp3 = _ASPPModule(inplanes, + 128, + 3, + padding=dilations[2], + dilation=dilations[2]) + self.aspp4 = _ASPPModule(inplanes, + 128, + 3, + padding=dilations[3], + dilation=dilations[3]) + + self.global_avg_pool = nn.Sequential( + nn.AdaptiveAvgPool2D((1, 1)), + nn.Conv2D(inplanes, 128, 1, stride=1, bias_attr=False), nn.ReLU()) + + self.GCT = GCT(640) + self.conv1 = nn.Conv2D(640, 256, 1, bias_attr=False) + self.bn1 = nn.GroupNorm(num_groups=32, num_channels=256) + self.relu = nn.ReLU() + self._init_weight() + + def forward(self, x): + x1 = self.aspp1(x) + x2 = self.aspp2(x) + x3 = self.aspp3(x) + x4 = self.aspp4(x) + x5 = self.global_avg_pool(x) + x5 = F.interpolate(x5, + size=x4.shape[2:], + mode='bilinear', + align_corners=True) + x = paddle.concat([x1, x2, x3, x4, x5], axis=1) + + x = self.GCT(x) + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + return x + + def _init_weight(self): + for m in self.sublayers(): + if isinstance(m, nn.Conv2D): + nn.initializer.KaimingNormal() + elif isinstance(m, nn.GroupNorm): + m.weight.data = nn.initializer.Constant(1) + m.bias.data = nn.initializer.Constant(0) + + +@HEADS.register() +class CollaborativeEnsemblerMS(nn.Layer): + def __init__( + self, + model_semantic_embedding_dim=256, + model_multi_local_distance=[[4, 8, 12, 16, 20, 24], + [2, 4, 6, 8, 10, 12], [2, 4, 6, 8, 10]], + model_head_embedding_dim=256, + model_refine_channels=64, + model_low_level_inplanes=256, + ): + super(CollaborativeEnsemblerMS, self).__init__() + in_dim_4x = model_semantic_embedding_dim * 3 + 3 + 2 * len( + model_multi_local_distance[0]) + in_dim_8x = model_semantic_embedding_dim * 3 + 3 + 2 * len( + model_multi_local_distance[1]) + in_dim_16x = model_semantic_embedding_dim * 3 + 3 + 2 * len( + model_multi_local_distance[2]) + attention_dim = model_semantic_embedding_dim * 4 + embed_dim = model_head_embedding_dim + refine_dim = model_refine_channels + low_level_dim = model_low_level_inplanes + + IA_in_dim = attention_dim + + self.relu = nn.ReLU() + + # stage 1 + + self.S1_IA1 = IA_gate(IA_in_dim, in_dim_4x) + self.S1_layer1 = Bottleneck(in_dim_4x, embed_dim) + + self.S1_IA2 = IA_gate(IA_in_dim, embed_dim) + self.S1_layer2 = Bottleneck(embed_dim, embed_dim, 1, 2) + + # stage2 + self.S2_IA1 = IA_gate(IA_in_dim, embed_dim) + self.S2_layer1 = Bottleneck(embed_dim, embed_dim * 2, 2) + + self.S2_IA2 = IA_gate(IA_in_dim, embed_dim * 2 + in_dim_8x) + self.S2_layer2 = Bottleneck(embed_dim * 2 + in_dim_8x, embed_dim * 2, 1, + 2) + + self.S2_IA3 = IA_gate(IA_in_dim, embed_dim * 2) + self.S2_layer3 = Bottleneck(embed_dim * 2, embed_dim * 2, 1, 4) + + # stage3 + self.S3_IA1 = IA_gate(IA_in_dim, embed_dim * 2) + self.S3_layer1 = Bottleneck(embed_dim * 2, embed_dim * 2, 2) + + self.S3_IA2 = IA_gate(IA_in_dim, embed_dim * 2 + in_dim_16x) + self.S3_layer2 = Bottleneck(embed_dim * 2 + in_dim_16x, embed_dim * 2, + 1, 2) + + self.S3_IA3 = IA_gate(IA_in_dim, embed_dim * 2) + self.S3_layer3 = Bottleneck(embed_dim * 2, embed_dim * 2, 1, 4) + + self.ASPP_IA = IA_gate(IA_in_dim, embed_dim * 2) + self.ASPP = ASPP() + + # Decoder + self.GCT_sc = GCT(low_level_dim + embed_dim) + self.conv_sc = nn.Conv2D(low_level_dim + embed_dim, + refine_dim, + 1, + bias_attr=False) + self.bn_sc = nn.GroupNorm(num_groups=int(refine_dim / 4), + num_channels=refine_dim) + self.relu = nn.ReLU() + + self.IA10 = IA_gate(IA_in_dim, embed_dim + refine_dim) + self.conv1 = nn.Conv2D(embed_dim + refine_dim, + int(embed_dim / 2), + kernel_size=3, + padding=1, + bias_attr=False) + self.bn1 = nn.GroupNorm(num_groups=32, num_channels=int(embed_dim / 2)) + + self.IA11 = IA_gate(IA_in_dim, int(embed_dim / 2)) + self.conv2 = nn.Conv2D(int(embed_dim / 2), + int(embed_dim / 2), + kernel_size=3, + padding=1, + bias_attr=False) + self.bn2 = nn.GroupNorm(num_groups=32, num_channels=int(embed_dim / 2)) + + # Output + self.IA_final_fg = nn.Linear(IA_in_dim, int(embed_dim / 2) + 1) + self.IA_final_bg = nn.Linear(IA_in_dim, int(embed_dim / 2) + 1) + + self.conv_sc.weight.data = nn.initializer.KaimingNormal() + self.conv1.weight.data = nn.initializer.KaimingNormal() + self.conv2.weight.data = nn.initializer.KaimingNormal() + + def forward(self, all_x, all_IA_head=None, low_level_feat=None): + x_4x, x_8x, x_16x = all_x + IA_head = all_IA_head[0] + + # stage 1 + x = self.S1_IA1(x_4x, IA_head) + x = self.S1_layer1(x) + + x = self.S1_IA2(x, IA_head) + x = self.S1_layer2(x) + + low_level_feat = paddle.concat( + [paddle.expand(low_level_feat, [x.shape[0], -1, -1, -1]), x], + axis=1) + + # stage 2 + x = self.S2_IA1(x, IA_head) + x = self.S2_layer1(x) + + x = paddle.concat([x, x_8x], axis=1) + x = self.S2_IA2(x, IA_head) + x = self.S2_layer2(x) + + x = self.S2_IA3(x, IA_head) + x = self.S2_layer3(x) + + # stage 3 + x = self.S3_IA1(x, IA_head) + x = self.S3_layer1(x) + + x = paddle.concat([x, x_16x], axis=1) + x = self.S3_IA2(x, IA_head) + x = self.S3_layer2(x) + + x = self.S3_IA3(x, IA_head) + x = self.S3_layer3(x) + + # ASPP + Decoder + x = self.ASPP_IA(x, IA_head) + x = self.ASPP(x) + + x = self.decoder(x, low_level_feat, IA_head) + + fg_logit = self.IA_logit(x, IA_head, self.IA_final_fg) + bg_logit = self.IA_logit(x, IA_head, self.IA_final_bg) + + pred = self.augment_background_logit(fg_logit, bg_logit) + + return pred + + def IA_logit(self, x, IA_head, IA_final): + n, c, h, w = x.shape + x = paddle.reshape(x, [1, n * c, h, w]) + IA_output = IA_final(IA_head) + IA_weight = IA_output[:, :c] + IA_bias = IA_output[:, -1] + IA_weight = paddle.reshape(IA_weight, [n, c, 1, 1]) + + IA_bias = paddle.reshape(IA_bias, [-1]) + logit = paddle.reshape( + F.conv2d(x, weight=IA_weight, bias=IA_bias, groups=n), [n, 1, h, w]) + return logit + + def decoder(self, x, low_level_feat, IA_head): + x = F.interpolate(x, + size=low_level_feat.shape[2:], + mode='bicubic', + align_corners=True) + + low_level_feat = self.GCT_sc(low_level_feat) + low_level_feat = self.conv_sc(low_level_feat) + low_level_feat = self.bn_sc(low_level_feat) + low_level_feat = self.relu(low_level_feat) + + x = paddle.concat([x, low_level_feat], axis=1) + x = self.IA10(x, IA_head) + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + x = self.IA11(x, IA_head) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu(x) + + return x + + def augment_background_logit(self, fg_logit, bg_logit): + # We augment the logit of absolute background by using the relative background logit of all the + # foreground objects. + obj_num = fg_logit.shape[0] + pred = fg_logit + if obj_num > 1: + bg_logit = bg_logit[1:obj_num, :, :, :] + aug_bg_logit = paddle.min(bg_logit, axis=0, keepdim=True) + pad = paddle.expand(paddle.zeros(aug_bg_logit.shape), + [obj_num - 1, -1, -1, -1]) + aug_bg_logit = paddle.concat([aug_bg_logit, pad], axis=0) + pred = pred + aug_bg_logit + pred = paddle.transpose(pred, [1, 0, 2, 3]) + return pred diff --git a/paddlevideo/modeling/heads/ctrgcn_head.py b/paddlevideo/modeling/heads/ctrgcn_head.py new file mode 100644 index 0000000000000000000000000000000000000000..c551d0d3ea9e8bde7f33b90079f6e94ffcb5a433 --- /dev/null +++ b/paddlevideo/modeling/heads/ctrgcn_head.py @@ -0,0 +1,65 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import paddle +import paddle.nn as nn + +from .base import BaseHead +from ..registry import HEADS +from ..weight_init import weight_init_ + + +@HEADS.register() +class CTRGCNHead(BaseHead): + """ + Head for CTR-GCN model. + Args: + in_channels: int, input feature channels. Default: 64. + num_classes: int, output the number of classes. + drop_out: float, dropout ratio of layer. Default: 0. + """ + + def __init__(self, in_channels=64, num_classes=10, drop_out=0, **kwargs): + super().__init__(num_classes, in_channels, **kwargs) + self.in_channels = in_channels + self.drop_out = drop_out + + self.fc = nn.Linear(self.in_channels * 4, self.num_classes) + if drop_out: + self.drop_out = nn.Dropout(self.drop_out) + else: + self.drop_out = lambda x: x + + def init_weights(self): + """Initiate the parameters. + """ + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + weight_init_(layer.weight, + 'Normal', + mean=0.0, + std=math.sqrt(2. / self.num_classes)) + + def forward(self, output_patch): + """Define how the head is going to run. + """ + x, N, M = output_patch + # N*M,C,T,V + _, c_new, T, V = x.shape + x = paddle.reshape(x, shape=[N, M, c_new, T * V]) + x = x.mean(3).mean(1) + x = self.drop_out(x) + + return self.fc(x) diff --git a/paddlevideo/modeling/heads/i3d_head.py b/paddlevideo/modeling/heads/i3d_head.py new file mode 100644 index 0000000000000000000000000000000000000000..269c8184e46952292be4a849597afa9d24e723e7 --- /dev/null +++ b/paddlevideo/modeling/heads/i3d_head.py @@ -0,0 +1,95 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +from paddle import ParamAttr + +from ..registry import HEADS +from ..weight_init import weight_init_ +from .base import BaseHead + + +@HEADS.register() +class I3DHead(BaseHead): + """Classification head for I3D. + + Args: + num_classes (int): Number of classes to be classified. + in_channels (int): Number of channels in input feature. + loss_cls (dict): Config for building loss. + Default: dict(name='CrossEntropyLoss') + spatial_type (str): Pooling type in spatial dimension. Default: 'avg'. + drop_ratio (float): Probability of dropout layer. Default: 0.5. + std (float): Std value for Initiation. Default: 0.01. + kwargs (dict, optional): Any keyword argument to be used to initialize + the head. + """ + def __init__(self, + num_classes, + in_channels, + loss_cfg=dict(name='CrossEntropyLoss'), + spatial_type='avg', + drop_ratio=0.5, + std=0.01, + **kwargs): + + super().__init__(num_classes, in_channels, loss_cfg, **kwargs) + + self.spatial_type = spatial_type + self.drop_ratio = drop_ratio + self.stdv = std + if self.drop_ratio != 0: + self.dropout = nn.Dropout(p=self.drop_ratio) + else: + self.dropout = None + self.fc = nn.Linear( + self.in_channels, + self.num_classes, + weight_attr=ParamAttr(learning_rate=10.0), + bias_attr=ParamAttr(learning_rate=10.0), + ) + + if self.spatial_type == 'avg': + # use `nn.AdaptiveAvgPool3d` to adaptively match the in_channels. + self.avg_pool = nn.AdaptiveAvgPool3D((1, 1, 1)) + else: + self.avg_pool = None + + def init_weights(self): + """Initiate the parameters from scratch.""" + weight_init_(self.fc, 'Normal', 'fc_0.w_0', 'fc_0.b_0', std=self.stdv) + + def forward(self, x): + """Defines the computation performed at every call. + + Args: + x (torch.Tensor): The input data. + + Returns: + torch.Tensor: The classification scores for input samples. + """ + # [N, in_channels, 4, 7, 7] + if self.avg_pool is not None: + x = self.avg_pool(x) + # [N, in_channels, 1, 1, 1] + if self.dropout is not None: + x = self.dropout(x) + # [N, in_channels, 1, 1, 1] + N = paddle.shape(x)[0] + x = x.reshape([N, -1]) + # [N, in_channels] + cls_score = self.fc(x) + # [N, num_classes] + return cls_score diff --git a/paddlevideo/modeling/heads/movinet_head.py b/paddlevideo/modeling/heads/movinet_head.py new file mode 100644 index 0000000000000000000000000000000000000000..b7db42e807fa0064fa8582a9b256e0996fbe85b0 --- /dev/null +++ b/paddlevideo/modeling/heads/movinet_head.py @@ -0,0 +1,16 @@ +import collections.abc + +container_abcs = collections.abc +from ..registry import HEADS +from .base import BaseHead +from ..builder import build_loss + + +@HEADS.register() +class MoViNetHead(BaseHead): + + def __init__(self): + super().__init__() + + def forward(self, x): + return x diff --git a/paddlevideo/modeling/heads/ms_tcn_head.py b/paddlevideo/modeling/heads/ms_tcn_head.py new file mode 100644 index 0000000000000000000000000000000000000000..e0f435f2a5c2e14ab1747fa8d9d81b144ccf01e4 --- /dev/null +++ b/paddlevideo/modeling/heads/ms_tcn_head.py @@ -0,0 +1,165 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import numpy as np + +from paddle import ParamAttr + +from .base import BaseHead +from ..registry import HEADS +from ..weight_init import weight_init_ + + +@HEADS.register() +class MSTCNHead(BaseHead): + + def __init__(self, num_classes, in_channels): + super().__init__(num_classes, in_channels) + self.ce = nn.CrossEntropyLoss(ignore_index=-100) + self.mse = nn.MSELoss(reduction='none') + self.num_classes = num_classes + + # cls score + self.overlap = 0.5 + + def forward(self, x): + """MS-TCN no head + """ + return x + + def loss(self, output, video_gt): + """calculate loss + """ + output_transpose = paddle.transpose(output, [2, 0, 1]) + ce_x = paddle.reshape(output_transpose, + (output_transpose.shape[0] * + output_transpose.shape[1], self.num_classes)) + ce_y = video_gt[0, :] + ce_loss = self.ce(ce_x, ce_y) + loss = ce_loss + + mse = self.mse(F.log_softmax(output[:, :, 1:], axis=1), + F.log_softmax(output.detach()[:, :, :-1], axis=1)) + mse = paddle.clip(mse, min=0, max=16) + mse_loss = 0.15 * paddle.mean(mse) + loss += mse_loss + + return loss + + def get_F1_score(self, predicted, groundTruth): + recog_content = list(predicted.numpy()) + gt_content = list(groundTruth[0].numpy()) + + # cls score + correct = 0 + total = 0 + edit = 0 + + for i in range(len(gt_content)): + total += 1 + + if gt_content[i] == recog_content[i]: + correct += 1 + + edit_num = self.edit_score(recog_content, gt_content) + edit += edit_num + + tp, fp, fn = self.f_score(recog_content, gt_content, self.overlap) + + # cls metric + + precision = tp / float(tp + fp) + recall = tp / float(fp + fn) + + if precision + recall > 0.0: + f1 = 2.0 * (precision * recall) / (precision + recall) + else: + f1 = 0.0 + f1 = np.nan_to_num(f1) + return f1 + + def get_labels_start_end_time(self, frame_wise_labels): + labels = [] + starts = [] + ends = [] + last_label = frame_wise_labels[0] + labels.append(frame_wise_labels[0]) + starts.append(0) + for i in range(len(frame_wise_labels)): + if frame_wise_labels[i] != last_label: + labels.append(frame_wise_labels[i]) + starts.append(i) + ends.append(i) + last_label = frame_wise_labels[i] + ends.append(i + 1) + return labels, starts, ends + + def levenstein(self, p, y, norm=False): + m_row = len(p) + n_col = len(y) + D = np.zeros([m_row + 1, n_col + 1], np.float) + for i in range(m_row + 1): + D[i, 0] = i + for i in range(n_col + 1): + D[0, i] = i + + for j in range(1, n_col + 1): + for i in range(1, m_row + 1): + if y[j - 1] == p[i - 1]: + D[i, j] = D[i - 1, j - 1] + else: + D[i, j] = min(D[i - 1, j] + 1, D[i, j - 1] + 1, + D[i - 1, j - 1] + 1) + + if norm: + score = (1 - D[-1, -1] / max(m_row, n_col)) * 100 + else: + score = D[-1, -1] + + return score + + def edit_score(self, recognized, ground_truth, norm=True): + P, _, _ = self.get_labels_start_end_time(recognized) + Y, _, _ = self.get_labels_start_end_time(ground_truth) + return self.levenstein(P, Y, norm) + + def f_score(self, recognized, ground_truth, overlap): + p_label, p_start, p_end = self.get_labels_start_end_time(recognized) + y_label, y_start, y_end = self.get_labels_start_end_time(ground_truth) + + tp = 0 + fp = 0 + + hits = np.zeros(len(y_label)) + + for j in range(len(p_label)): + intersection = np.minimum(p_end[j], y_end) - np.maximum( + p_start[j], y_start) + union = np.maximum(p_end[j], y_end) - np.minimum( + p_start[j], y_start) + IoU = (1.0 * intersection / union) * ( + [p_label[j] == y_label[x] for x in range(len(y_label))]) + # Get the best scoring segment + idx = np.array(IoU).argmax() + + if IoU[idx] >= overlap and not hits[idx]: + tp += 1 + hits[idx] = 1 + else: + fp += 1 + fn = len(y_label) - sum(hits) + return float(tp), float(fp), float(fn) diff --git a/paddlevideo/modeling/heads/ops.py b/paddlevideo/modeling/heads/ops.py new file mode 100644 index 0000000000000000000000000000000000000000..0c357fa707836c56fdc51cf008df2ca919e05468 --- /dev/null +++ b/paddlevideo/modeling/heads/ops.py @@ -0,0 +1,1583 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn.functional as F +import paddle.nn as nn +from paddle import ParamAttr +from paddle.regularizer import L2Decay + +from paddle.fluid.framework import Variable, in_dygraph_mode +from paddle.fluid import core +from paddle.fluid.layer_helper import LayerHelper +from paddle.fluid.dygraph import layers +from paddle.fluid.data_feeder import check_variable_and_dtype, check_type, check_dtype, convert_dtype +import math +import six +import numpy as np +from functools import reduce +chaj_debug = 0 + +__all__ = [ + 'roi_pool', + 'roi_align', + 'prior_box', + 'generate_proposals', + 'iou_similarity', + 'box_coder', + 'yolo_box', + 'multiclass_nms', + 'distribute_fpn_proposals', + 'collect_fpn_proposals', + 'matrix_nms', + 'batch_norm', + 'mish', +] + + +def mish(x): + return x * paddle.tanh(F.softplus(x)) + + +def batch_norm(ch, + norm_type='bn', + norm_decay=0., + initializer=None, + data_format='NCHW'): + if norm_type == 'sync_bn': + batch_norm = nn.SyncBatchNorm + else: + batch_norm = nn.BatchNorm2D + + return batch_norm( + ch, + weight_attr=ParamAttr( + initializer=initializer, regularizer=L2Decay(norm_decay)), + bias_attr=ParamAttr(regularizer=L2Decay(norm_decay)), + data_format=data_format) + + +@paddle.jit.not_to_static +def roi_pool(input, + rois, + output_size, + spatial_scale=1.0, + rois_num=None, + name=None): + """ + + This operator implements the roi_pooling layer. + Region of interest pooling (also known as RoI pooling) is to perform max pooling on inputs of nonuniform sizes to obtain fixed-size feature maps (e.g. 7*7). + + The operator has three steps: + + 1. Dividing each region proposal into equal-sized sections with output_size(h, w); + 2. Finding the largest value in each section; + 3. Copying these max values to the output buffer. + + For more information, please refer to https://stackoverflow.com/questions/43430056/what-is-roi-layer-in-fast-rcnn + + Args: + input (Tensor): Input feature, 4D-Tensor with the shape of [N,C,H,W], + where N is the batch size, C is the input channel, H is Height, W is weight. + The data type is float32 or float64. + rois (Tensor): ROIs (Regions of Interest) to pool over. + 2D-Tensor or 2D-LoDTensor with the shape of [num_rois,4], the lod level is 1. + Given as [[x1, y1, x2, y2], ...], (x1, y1) is the top left coordinates, + and (x2, y2) is the bottom right coordinates. + output_size (int or tuple[int, int]): The pooled output size(h, w), data type is int32. If int, h and w are both equal to output_size. + spatial_scale (float, optional): Multiplicative spatial scale factor to translate ROI coords from their input scale to the scale used when pooling. Default: 1.0 + rois_num (Tensor): The number of RoIs in each image. Default: None + name(str, optional): For detailed information, please refer + to :ref:`api_guide_Name`. Usually name is no need to set and + None by default. + + + Returns: + Tensor: The pooled feature, 4D-Tensor with the shape of [num_rois, C, output_size[0], output_size[1]]. + + + Examples: + + .. code-block:: python + + import paddle + from ppdet.modeling import ops + paddle.enable_static() + + x = paddle.static.data( + name='data', shape=[None, 256, 32, 32], dtype='float32') + rois = paddle.static.data( + name='rois', shape=[None, 4], dtype='float32') + rois_num = paddle.static.data(name='rois_num', shape=[None], dtype='int32') + + pool_out = ops.roi_pool( + input=x, + rois=rois, + output_size=(1, 1), + spatial_scale=1.0, + rois_num=rois_num) + """ + check_type(output_size, 'output_size', (int, tuple), 'roi_pool') + if isinstance(output_size, int): + output_size = (output_size, output_size) + + pooled_height, pooled_width = output_size + if in_dygraph_mode(): + assert rois_num is not None, "rois_num should not be None in dygraph mode." + pool_out, argmaxes = core.ops.roi_pool( + input, rois, rois_num, "pooled_height", pooled_height, + "pooled_width", pooled_width, "spatial_scale", spatial_scale) + return pool_out, argmaxes + + else: + check_variable_and_dtype(input, 'input', ['float32'], 'roi_pool') + check_variable_and_dtype(rois, 'rois', ['float32'], 'roi_pool') + helper = LayerHelper('roi_pool', **locals()) + dtype = helper.input_dtype() + pool_out = helper.create_variable_for_type_inference(dtype) + argmaxes = helper.create_variable_for_type_inference(dtype='int32') + + inputs = { + "X": input, + "ROIs": rois, + } + if rois_num is not None: + inputs['RoisNum'] = rois_num + helper.append_op( + type="roi_pool", + inputs=inputs, + outputs={"Out": pool_out, + "Argmax": argmaxes}, + attrs={ + "pooled_height": pooled_height, + "pooled_width": pooled_width, + "spatial_scale": spatial_scale + }) + return pool_out, argmaxes + + +@paddle.jit.not_to_static +def roi_align(input, + rois, + output_size, + spatial_scale=1.0, + sampling_ratio=-1, + rois_num=None, + aligned=True, + name=None): + """ + + Region of interest align (also known as RoI align) is to perform + bilinear interpolation on inputs of nonuniform sizes to obtain + fixed-size feature maps (e.g. 7*7) + + Dividing each region proposal into equal-sized sections with + the pooled_width and pooled_height. Location remains the origin + result. + + In each ROI bin, the value of the four regularly sampled locations + are computed directly through bilinear interpolation. The output is + the mean of four locations. + Thus avoid the misaligned problem. + + Args: + input (Tensor): Input feature, 4D-Tensor with the shape of [N,C,H,W], + where N is the batch size, C is the input channel, H is Height, W is weight. + The data type is float32 or float64. + rois (Tensor): ROIs (Regions of Interest) to pool over.It should be + a 2-D Tensor or 2-D LoDTensor of shape (num_rois, 4), the lod level is 1. + The data type is float32 or float64. Given as [[x1, y1, x2, y2], ...], + (x1, y1) is the top left coordinates, and (x2, y2) is the bottom right coordinates. + output_size (int or tuple[int, int]): The pooled output size(h, w), data type is int32. If int, h and w are both equal to output_size. + spatial_scale (float32, optional): Multiplicative spatial scale factor to translate ROI coords + from their input scale to the scale used when pooling. Default: 1.0 + sampling_ratio(int32, optional): number of sampling points in the interpolation grid. + If <=0, then grid points are adaptive to roi_width and pooled_w, likewise for height. Default: -1 + rois_num (Tensor): The number of RoIs in each image. Default: None + name(str, optional): For detailed information, please refer + to :ref:`api_guide_Name`. Usually name is no need to set and + None by default. + + Returns: + Tensor: + + Output: The output of ROIAlignOp is a 4-D tensor with shape (num_rois, channels, pooled_h, pooled_w). The data type is float32 or float64. + + + Examples: + .. code-block:: python + + import paddle + from ppdet.modeling import ops + paddle.enable_static() + + x = paddle.static.data( + name='data', shape=[None, 256, 32, 32], dtype='float32') + rois = paddle.static.data( + name='rois', shape=[None, 4], dtype='float32') + rois_num = paddle.static.data(name='rois_num', shape=[None], dtype='int32') + align_out = ops.roi_align(input=x, + rois=rois, + ouput_size=(7, 7), + spatial_scale=0.5, + sampling_ratio=-1, + rois_num=rois_num) + """ + check_type(output_size, 'output_size', (int, tuple), 'roi_align') + if isinstance(output_size, int): + output_size = (output_size, output_size) + + pooled_height, pooled_width = output_size + + if in_dygraph_mode(): + assert rois_num is not None, "rois_num should not be None in dygraph mode." + if chaj_debug: + print("chajchaj, ops.py, bf core.ops.roi_align, type(rois):",type(rois)) + print("chajchaj, ops.py, bf core.ops.roi_align, rois.shape:",rois.shape) + if rois.shape[0]>0: + print("chajchaj, ops.py, bf core.ops.roi_align, (rois):",(rois)) + align_out = core.ops.roi_align( + input, rois, rois_num, "pooled_height", pooled_height, + "pooled_width", pooled_width, "spatial_scale", spatial_scale, + "sampling_ratio", sampling_ratio, "aligned", aligned) + return align_out + + else: + check_variable_and_dtype(input, 'input', ['float32', 'float64'], + 'roi_align') + check_variable_and_dtype(rois, 'rois', ['float32', 'float64'], + 'roi_align') + helper = LayerHelper('roi_align', **locals()) + dtype = helper.input_dtype() + align_out = helper.create_variable_for_type_inference(dtype) + inputs = { + "X": input, + "ROIs": rois, + } + if rois_num is not None: + inputs['RoisNum'] = rois_num + helper.append_op( + type="roi_align", + inputs=inputs, + outputs={"Out": align_out}, + attrs={ + "pooled_height": pooled_height, + "pooled_width": pooled_width, + "spatial_scale": spatial_scale, + "sampling_ratio": sampling_ratio, + "aligned": aligned, + }) + return align_out + + +@paddle.jit.not_to_static +def iou_similarity(x, y, box_normalized=True, name=None): + """ + Computes intersection-over-union (IOU) between two box lists. + Box list 'X' should be a LoDTensor and 'Y' is a common Tensor, + boxes in 'Y' are shared by all instance of the batched inputs of X. + Given two boxes A and B, the calculation of IOU is as follows: + + $$ + IOU(A, B) = + \\frac{area(A\\cap B)}{area(A)+area(B)-area(A\\cap B)} + $$ + + Args: + x (Tensor): Box list X is a 2-D Tensor with shape [N, 4] holds N + boxes, each box is represented as [xmin, ymin, xmax, ymax], + the shape of X is [N, 4]. [xmin, ymin] is the left top + coordinate of the box if the input is image feature map, they + are close to the origin of the coordinate system. + [xmax, ymax] is the right bottom coordinate of the box. + The data type is float32 or float64. + y (Tensor): Box list Y holds M boxes, each box is represented as + [xmin, ymin, xmax, ymax], the shape of X is [N, 4]. + [xmin, ymin] is the left top coordinate of the box if the + input is image feature map, and [xmax, ymax] is the right + bottom coordinate of the box. The data type is float32 or float64. + box_normalized(bool): Whether treat the priorbox as a normalized box. + Set true by default. + name(str, optional): For detailed information, please refer + to :ref:`api_guide_Name`. Usually name is no need to set and + None by default. + + Returns: + Tensor: The output of iou_similarity op, a tensor with shape [N, M] + representing pairwise iou scores. The data type is same with x. + + Examples: + .. code-block:: python + + import paddle + from ppdet.modeling import ops + paddle.enable_static() + + x = paddle.static.data(name='x', shape=[None, 4], dtype='float32') + y = paddle.static.data(name='y', shape=[None, 4], dtype='float32') + iou = ops.iou_similarity(x=x, y=y) + """ + + if in_dygraph_mode(): + out = core.ops.iou_similarity(x, y, 'box_normalized', box_normalized) + return out + else: + helper = LayerHelper("iou_similarity", **locals()) + out = helper.create_variable_for_type_inference(dtype=x.dtype) + + helper.append_op( + type="iou_similarity", + inputs={"X": x, + "Y": y}, + attrs={"box_normalized": box_normalized}, + outputs={"Out": out}) + return out + + +@paddle.jit.not_to_static +def collect_fpn_proposals(multi_rois, + multi_scores, + min_level, + max_level, + post_nms_top_n, + rois_num_per_level=None, + name=None): + """ + + **This OP only supports LoDTensor as input**. Concat multi-level RoIs + (Region of Interest) and select N RoIs with respect to multi_scores. + This operation performs the following steps: + + 1. Choose num_level RoIs and scores as input: num_level = max_level - min_level + 2. Concat multi-level RoIs and scores + 3. Sort scores and select post_nms_top_n scores + 4. Gather RoIs by selected indices from scores + 5. Re-sort RoIs by corresponding batch_id + + Args: + multi_rois(list): List of RoIs to collect. Element in list is 2-D + LoDTensor with shape [N, 4] and data type is float32 or float64, + N is the number of RoIs. + multi_scores(list): List of scores of RoIs to collect. Element in list + is 2-D LoDTensor with shape [N, 1] and data type is float32 or + float64, N is the number of RoIs. + min_level(int): The lowest level of FPN layer to collect + max_level(int): The highest level of FPN layer to collect + post_nms_top_n(int): The number of selected RoIs + rois_num_per_level(list, optional): The List of RoIs' numbers. + Each element is 1-D Tensor which contains the RoIs' number of each + image on each level and the shape is [B] and data type is + int32, B is the number of images. If it is not None then return + a 1-D Tensor contains the output RoIs' number of each image and + the shape is [B]. Default: None + name(str, optional): For detailed information, please refer + to :ref:`api_guide_Name`. Usually name is no need to set and + None by default. + + Returns: + Variable: + + fpn_rois(Variable): 2-D LoDTensor with shape [N, 4] and data type is + float32 or float64. Selected RoIs. + + rois_num(Tensor): 1-D Tensor contains the RoIs's number of each + image. The shape is [B] and data type is int32. B is the number of + images. + + Examples: + .. code-block:: python + + import paddle + from ppdet.modeling import ops + paddle.enable_static() + multi_rois = [] + multi_scores = [] + for i in range(4): + multi_rois.append(paddle.static.data( + name='roi_'+str(i), shape=[None, 4], dtype='float32', lod_level=1)) + for i in range(4): + multi_scores.append(paddle.static.data( + name='score_'+str(i), shape=[None, 1], dtype='float32', lod_level=1)) + + fpn_rois = ops.collect_fpn_proposals( + multi_rois=multi_rois, + multi_scores=multi_scores, + min_level=2, + max_level=5, + post_nms_top_n=2000) + """ + check_type(multi_rois, 'multi_rois', list, 'collect_fpn_proposals') + check_type(multi_scores, 'multi_scores', list, 'collect_fpn_proposals') + num_lvl = max_level - min_level + 1 + input_rois = multi_rois[:num_lvl] + input_scores = multi_scores[:num_lvl] + + if in_dygraph_mode(): + assert rois_num_per_level is not None, "rois_num_per_level should not be None in dygraph mode." + attrs = ('post_nms_topN', post_nms_top_n) + output_rois, rois_num = core.ops.collect_fpn_proposals( + input_rois, input_scores, rois_num_per_level, *attrs) + return output_rois, rois_num + + else: + helper = LayerHelper('collect_fpn_proposals', **locals()) + dtype = helper.input_dtype('multi_rois') + check_dtype(dtype, 'multi_rois', ['float32', 'float64'], + 'collect_fpn_proposals') + output_rois = helper.create_variable_for_type_inference(dtype) + output_rois.stop_gradient = True + + inputs = { + 'MultiLevelRois': input_rois, + 'MultiLevelScores': input_scores, + } + outputs = {'FpnRois': output_rois} + if rois_num_per_level is not None: + inputs['MultiLevelRoIsNum'] = rois_num_per_level + rois_num = helper.create_variable_for_type_inference(dtype='int32') + rois_num.stop_gradient = True + outputs['RoisNum'] = rois_num + helper.append_op( + type='collect_fpn_proposals', + inputs=inputs, + outputs=outputs, + attrs={'post_nms_topN': post_nms_top_n}) + return output_rois, rois_num + + +@paddle.jit.not_to_static +def distribute_fpn_proposals(fpn_rois, + min_level, + max_level, + refer_level, + refer_scale, + pixel_offset=False, + rois_num=None, + name=None): + """ + + **This op only takes LoDTensor as input.** In Feature Pyramid Networks + (FPN) models, it is needed to distribute all proposals into different FPN + level, with respect to scale of the proposals, the referring scale and the + referring level. Besides, to restore the order of proposals, we return an + array which indicates the original index of rois in current proposals. + To compute FPN level for each roi, the formula is given as follows: + + .. math:: + + roi\_scale &= \sqrt{BBoxArea(fpn\_roi)} + + level = floor(&\log(\\frac{roi\_scale}{refer\_scale}) + refer\_level) + + where BBoxArea is a function to compute the area of each roi. + + Args: + + fpn_rois(Variable): 2-D Tensor with shape [N, 4] and data type is + float32 or float64. The input fpn_rois. + min_level(int32): The lowest level of FPN layer where the proposals come + from. + max_level(int32): The highest level of FPN layer where the proposals + come from. + refer_level(int32): The referring level of FPN layer with specified scale. + refer_scale(int32): The referring scale of FPN layer with specified level. + rois_num(Tensor): 1-D Tensor contains the number of RoIs in each image. + The shape is [B] and data type is int32. B is the number of images. + If it is not None then return a list of 1-D Tensor. Each element + is the output RoIs' number of each image on the corresponding level + and the shape is [B]. None by default. + name(str, optional): For detailed information, please refer + to :ref:`api_guide_Name`. Usually name is no need to set and + None by default. + + Returns: + Tuple: + + multi_rois(List) : A list of 2-D LoDTensor with shape [M, 4] + and data type of float32 and float64. The length is + max_level-min_level+1. The proposals in each FPN level. + + restore_ind(Variable): A 2-D Tensor with shape [N, 1], N is + the number of total rois. The data type is int32. It is + used to restore the order of fpn_rois. + + rois_num_per_level(List): A list of 1-D Tensor and each Tensor is + the RoIs' number in each image on the corresponding level. The shape + is [B] and data type of int32. B is the number of images + + + Examples: + .. code-block:: python + + import paddle + from ppdet.modeling import ops + paddle.enable_static() + fpn_rois = paddle.static.data( + name='data', shape=[None, 4], dtype='float32', lod_level=1) + multi_rois, restore_ind = ops.distribute_fpn_proposals( + fpn_rois=fpn_rois, + min_level=2, + max_level=5, + refer_level=4, + refer_scale=224) + """ + num_lvl = max_level - min_level + 1 + + if in_dygraph_mode(): + assert rois_num is not None, "rois_num should not be None in dygraph mode." + attrs = ('min_level', min_level, 'max_level', max_level, 'refer_level', + refer_level, 'refer_scale', refer_scale, 'pixel_offset', + pixel_offset) + multi_rois, restore_ind, rois_num_per_level = core.ops.distribute_fpn_proposals( + fpn_rois, rois_num, num_lvl, num_lvl, *attrs) + return multi_rois, restore_ind, rois_num_per_level + + else: + check_variable_and_dtype(fpn_rois, 'fpn_rois', ['float32', 'float64'], + 'distribute_fpn_proposals') + helper = LayerHelper('distribute_fpn_proposals', **locals()) + dtype = helper.input_dtype('fpn_rois') + multi_rois = [ + helper.create_variable_for_type_inference(dtype) + for i in range(num_lvl) + ] + + restore_ind = helper.create_variable_for_type_inference(dtype='int32') + + inputs = {'FpnRois': fpn_rois} + outputs = { + 'MultiFpnRois': multi_rois, + 'RestoreIndex': restore_ind, + } + + if rois_num is not None: + inputs['RoisNum'] = rois_num + rois_num_per_level = [ + helper.create_variable_for_type_inference(dtype='int32') + for i in range(num_lvl) + ] + outputs['MultiLevelRoIsNum'] = rois_num_per_level + + helper.append_op( + type='distribute_fpn_proposals', + inputs=inputs, + outputs=outputs, + attrs={ + 'min_level': min_level, + 'max_level': max_level, + 'refer_level': refer_level, + 'refer_scale': refer_scale, + 'pixel_offset': pixel_offset + }) + return multi_rois, restore_ind, rois_num_per_level + + +@paddle.jit.not_to_static +def yolo_box( + x, + origin_shape, + anchors, + class_num, + conf_thresh, + downsample_ratio, + clip_bbox=True, + scale_x_y=1., + name=None, ): + """ + + This operator generates YOLO detection boxes from output of YOLOv3 network. + + The output of previous network is in shape [N, C, H, W], while H and W + should be the same, H and W specify the grid size, each grid point predict + given number boxes, this given number, which following will be represented as S, + is specified by the number of anchors. In the second dimension(the channel + dimension), C should be equal to S * (5 + class_num), class_num is the object + category number of source dataset(such as 80 in coco dataset), so the + second(channel) dimension, apart from 4 box location coordinates x, y, w, h, + also includes confidence score of the box and class one-hot key of each anchor + box. + Assume the 4 location coordinates are :math:`t_x, t_y, t_w, t_h`, the box + predictions should be as follows: + $$ + b_x = \\sigma(t_x) + c_x + $$ + $$ + b_y = \\sigma(t_y) + c_y + $$ + $$ + b_w = p_w e^{t_w} + $$ + $$ + b_h = p_h e^{t_h} + $$ + in the equation above, :math:`c_x, c_y` is the left top corner of current grid + and :math:`p_w, p_h` is specified by anchors. + The logistic regression value of the 5th channel of each anchor prediction boxes + represents the confidence score of each prediction box, and the logistic + regression value of the last :attr:`class_num` channels of each anchor prediction + boxes represents the classifcation scores. Boxes with confidence scores less than + :attr:`conf_thresh` should be ignored, and box final scores is the product of + confidence scores and classification scores. + $$ + score_{pred} = score_{conf} * score_{class} + $$ + + Args: + x (Tensor): The input tensor of YoloBox operator is a 4-D tensor with shape of [N, C, H, W]. + The second dimension(C) stores box locations, confidence score and + classification one-hot keys of each anchor box. Generally, X should be the output of YOLOv3 network. + The data type is float32 or float64. + origin_shape (Tensor): The image size tensor of YoloBox operator, This is a 2-D tensor with shape of [N, 2]. + This tensor holds height and width of each input image used for resizing output box in input image + scale. The data type is int32. + anchors (list|tuple): The anchor width and height, it will be parsed pair by pair. + class_num (int): The number of classes to predict. + conf_thresh (float): The confidence scores threshold of detection boxes. Boxes with confidence scores + under threshold should be ignored. + downsample_ratio (int): The downsample ratio from network input to YoloBox operator input, + so 32, 16, 8 should be set for the first, second, and thrid YoloBox operators. + clip_bbox (bool): Whether clip output bonding box in Input(ImgSize) boundary. Default true. + scale_x_y (float): Scale the center point of decoded bounding box. Default 1.0. + name (string): The default value is None. Normally there is no need + for user to set this property. For more information, + please refer to :ref:`api_guide_Name` + + Returns: + boxes Tensor: A 3-D tensor with shape [N, M, 4], the coordinates of boxes, N is the batch num, + M is output box number, and the 3rd dimension stores [xmin, ymin, xmax, ymax] coordinates of boxes. + scores Tensor: A 3-D tensor with shape [N, M, :attr:`class_num`], the coordinates of boxes, N is the batch num, + M is output box number. + + Raises: + TypeError: Attr anchors of yolo box must be list or tuple + TypeError: Attr class_num of yolo box must be an integer + TypeError: Attr conf_thresh of yolo box must be a float number + + Examples: + + .. code-block:: python + + import paddle + from ppdet.modeling import ops + + paddle.enable_static() + x = paddle.static.data(name='x', shape=[None, 255, 13, 13], dtype='float32') + img_size = paddle.static.data(name='img_size',shape=[None, 2],dtype='int64') + anchors = [10, 13, 16, 30, 33, 23] + boxes,scores = ops.yolo_box(x=x, img_size=img_size, class_num=80, anchors=anchors, + conf_thresh=0.01, downsample_ratio=32) + """ + helper = LayerHelper('yolo_box', **locals()) + + if not isinstance(anchors, list) and not isinstance(anchors, tuple): + raise TypeError("Attr anchors of yolo_box must be list or tuple") + if not isinstance(class_num, int): + raise TypeError("Attr class_num of yolo_box must be an integer") + if not isinstance(conf_thresh, float): + raise TypeError("Attr ignore_thresh of yolo_box must be a float number") + + if in_dygraph_mode(): + attrs = ('anchors', anchors, 'class_num', class_num, 'conf_thresh', + conf_thresh, 'downsample_ratio', downsample_ratio, 'clip_bbox', + clip_bbox, 'scale_x_y', scale_x_y) + boxes, scores = core.ops.yolo_box(x, origin_shape, *attrs) + return boxes, scores + else: + boxes = helper.create_variable_for_type_inference(dtype=x.dtype) + scores = helper.create_variable_for_type_inference(dtype=x.dtype) + + attrs = { + "anchors": anchors, + "class_num": class_num, + "conf_thresh": conf_thresh, + "downsample_ratio": downsample_ratio, + "clip_bbox": clip_bbox, + "scale_x_y": scale_x_y, + } + + helper.append_op( + type='yolo_box', + inputs={ + "X": x, + "ImgSize": origin_shape, + }, + outputs={ + 'Boxes': boxes, + 'Scores': scores, + }, + attrs=attrs) + return boxes, scores + + +@paddle.jit.not_to_static +def prior_box(input, + image, + min_sizes, + max_sizes=None, + aspect_ratios=[1.], + variance=[0.1, 0.1, 0.2, 0.2], + flip=False, + clip=False, + steps=[0.0, 0.0], + offset=0.5, + min_max_aspect_ratios_order=False, + name=None): + """ + + This op generates prior boxes for SSD(Single Shot MultiBox Detector) algorithm. + Each position of the input produce N prior boxes, N is determined by + the count of min_sizes, max_sizes and aspect_ratios, The size of the + box is in range(min_size, max_size) interval, which is generated in + sequence according to the aspect_ratios. + + Parameters: + input(Tensor): 4-D tensor(NCHW), the data type should be float32 or float64. + image(Tensor): 4-D tensor(NCHW), the input image data of PriorBoxOp, + the data type should be float32 or float64. + min_sizes(list|tuple|float): the min sizes of generated prior boxes. + max_sizes(list|tuple|None): the max sizes of generated prior boxes. + Default: None. + aspect_ratios(list|tuple|float): the aspect ratios of generated + prior boxes. Default: [1.]. + variance(list|tuple): the variances to be encoded in prior boxes. + Default:[0.1, 0.1, 0.2, 0.2]. + flip(bool): Whether to flip aspect ratios. Default:False. + clip(bool): Whether to clip out-of-boundary boxes. Default: False. + step(list|tuple): Prior boxes step across width and height, If + step[0] equals to 0.0 or step[1] equals to 0.0, the prior boxes step across + height or weight of the input will be automatically calculated. + Default: [0., 0.] + offset(float): Prior boxes center offset. Default: 0.5 + min_max_aspect_ratios_order(bool): If set True, the output prior box is + in order of [min, max, aspect_ratios], which is consistent with + Caffe. Please note, this order affects the weights order of + convolution layer followed by and does not affect the final + detection results. Default: False. + name(str, optional): The default value is None. Normally there is no need for + user to set this property. For more information, please refer to :ref:`api_guide_Name` + + Returns: + Tuple: A tuple with two Variable (boxes, variances) + + boxes(Tensor): the output prior boxes of PriorBox. + 4-D tensor, the layout is [H, W, num_priors, 4]. + H is the height of input, W is the width of input, + num_priors is the total box count of each position of input. + + variances(Tensor): the expanded variances of PriorBox. + 4-D tensor, the layput is [H, W, num_priors, 4]. + H is the height of input, W is the width of input + num_priors is the total box count of each position of input + + Examples: + .. code-block:: python + + import paddle + from ppdet.modeling import ops + + paddle.enable_static() + input = paddle.static.data(name="input", shape=[None,3,6,9]) + image = paddle.static.data(name="image", shape=[None,3,9,12]) + box, var = ops.prior_box( + input=input, + image=image, + min_sizes=[100.], + clip=True, + flip=True) + """ + helper = LayerHelper("prior_box", **locals()) + dtype = helper.input_dtype() + check_variable_and_dtype( + input, 'input', ['uint8', 'int8', 'float32', 'float64'], 'prior_box') + + def _is_list_or_tuple_(data): + return (isinstance(data, list) or isinstance(data, tuple)) + + if not _is_list_or_tuple_(min_sizes): + min_sizes = [min_sizes] + if not _is_list_or_tuple_(aspect_ratios): + aspect_ratios = [aspect_ratios] + if not (_is_list_or_tuple_(steps) and len(steps) == 2): + raise ValueError('steps should be a list or tuple ', + 'with length 2, (step_width, step_height).') + + min_sizes = list(map(float, min_sizes)) + aspect_ratios = list(map(float, aspect_ratios)) + steps = list(map(float, steps)) + + cur_max_sizes = None + if max_sizes is not None and len(max_sizes) > 0 and max_sizes[0] > 0: + if not _is_list_or_tuple_(max_sizes): + max_sizes = [max_sizes] + cur_max_sizes = max_sizes + + if in_dygraph_mode(): + attrs = ('min_sizes', min_sizes, 'aspect_ratios', aspect_ratios, + 'variances', variance, 'flip', flip, 'clip', clip, 'step_w', + steps[0], 'step_h', steps[1], 'offset', offset, + 'min_max_aspect_ratios_order', min_max_aspect_ratios_order) + if cur_max_sizes is not None: + attrs += ('max_sizes', cur_max_sizes) + box, var = core.ops.prior_box(input, image, *attrs) + return box, var + else: + attrs = { + 'min_sizes': min_sizes, + 'aspect_ratios': aspect_ratios, + 'variances': variance, + 'flip': flip, + 'clip': clip, + 'step_w': steps[0], + 'step_h': steps[1], + 'offset': offset, + 'min_max_aspect_ratios_order': min_max_aspect_ratios_order + } + + if cur_max_sizes is not None: + attrs['max_sizes'] = cur_max_sizes + + box = helper.create_variable_for_type_inference(dtype) + var = helper.create_variable_for_type_inference(dtype) + helper.append_op( + type="prior_box", + inputs={"Input": input, + "Image": image}, + outputs={"Boxes": box, + "Variances": var}, + attrs=attrs, ) + box.stop_gradient = True + var.stop_gradient = True + return box, var + + +@paddle.jit.not_to_static +def multiclass_nms(bboxes, + scores, + score_threshold, + nms_top_k, + keep_top_k, + nms_threshold=0.3, + normalized=True, + nms_eta=1., + background_label=-1, + return_index=False, + return_rois_num=True, + rois_num=None, + name=None): + """ + This operator is to do multi-class non maximum suppression (NMS) on + boxes and scores. + In the NMS step, this operator greedily selects a subset of detection bounding + boxes that have high scores larger than score_threshold, if providing this + threshold, then selects the largest nms_top_k confidences scores if nms_top_k + is larger than -1. Then this operator pruns away boxes that have high IOU + (intersection over union) overlap with already selected boxes by adaptive + threshold NMS based on parameters of nms_threshold and nms_eta. + Aftern NMS step, at most keep_top_k number of total bboxes are to be kept + per image if keep_top_k is larger than -1. + Args: + bboxes (Tensor): Two types of bboxes are supported: + 1. (Tensor) A 3-D Tensor with shape + [N, M, 4 or 8 16 24 32] represents the + predicted locations of M bounding bboxes, + N is the batch size. Each bounding box has four + coordinate values and the layout is + [xmin, ymin, xmax, ymax], when box size equals to 4. + 2. (LoDTensor) A 3-D Tensor with shape [M, C, 4] + M is the number of bounding boxes, C is the + class number + scores (Tensor): Two types of scores are supported: + 1. (Tensor) A 3-D Tensor with shape [N, C, M] + represents the predicted confidence predictions. + N is the batch size, C is the class number, M is + number of bounding boxes. For each category there + are total M scores which corresponding M bounding + boxes. Please note, M is equal to the 2nd dimension + of BBoxes. + 2. (LoDTensor) A 2-D LoDTensor with shape [M, C]. + M is the number of bbox, C is the class number. + In this case, input BBoxes should be the second + case with shape [M, C, 4]. + background_label (int): The index of background label, the background + label will be ignored. If set to -1, then all + categories will be considered. Default: 0 + score_threshold (float): Threshold to filter out bounding boxes with + low confidence score. If not provided, + consider all boxes. + nms_top_k (int): Maximum number of detections to be kept according to + the confidences after the filtering detections based + on score_threshold. + nms_threshold (float): The threshold to be used in NMS. Default: 0.3 + nms_eta (float): The threshold to be used in NMS. Default: 1.0 + keep_top_k (int): Number of total bboxes to be kept per image after NMS + step. -1 means keeping all bboxes after NMS step. + normalized (bool): Whether detections are normalized. Default: True + return_index(bool): Whether return selected index. Default: False + rois_num(Tensor): 1-D Tensor contains the number of RoIs in each image. + The shape is [B] and data type is int32. B is the number of images. + If it is not None then return a list of 1-D Tensor. Each element + is the output RoIs' number of each image on the corresponding level + and the shape is [B]. None by default. + name(str): Name of the multiclass nms op. Default: None. + Returns: + A tuple with two Variables: (Out, Index) if return_index is True, + otherwise, a tuple with one Variable(Out) is returned. + Out: A 2-D LoDTensor with shape [No, 6] represents the detections. + Each row has 6 values: [label, confidence, xmin, ymin, xmax, ymax] + or A 2-D LoDTensor with shape [No, 10] represents the detections. + Each row has 10 values: [label, confidence, x1, y1, x2, y2, x3, y3, + x4, y4]. No is the total number of detections. + If all images have not detected results, all elements in LoD will be + 0, and output tensor is empty (None). + Index: Only return when return_index is True. A 2-D LoDTensor with + shape [No, 1] represents the selected index which type is Integer. + The index is the absolute value cross batches. No is the same number + as Out. If the index is used to gather other attribute such as age, + one needs to reshape the input(N, M, 1) to (N * M, 1) as first, where + N is the batch size and M is the number of boxes. + Examples: + .. code-block:: python + + import paddle + from ppdet.modeling import ops + boxes = paddle.static.data(name='bboxes', shape=[81, 4], + dtype='float32', lod_level=1) + scores = paddle.static.data(name='scores', shape=[81], + dtype='float32', lod_level=1) + out, index = ops.multiclass_nms(bboxes=boxes, + scores=scores, + background_label=0, + score_threshold=0.5, + nms_top_k=400, + nms_threshold=0.3, + keep_top_k=200, + normalized=False, + return_index=True) + """ + helper = LayerHelper('multiclass_nms3', **locals()) + + if in_dygraph_mode(): + attrs = ('background_label', background_label, 'score_threshold', + score_threshold, 'nms_top_k', nms_top_k, 'nms_threshold', + nms_threshold, 'keep_top_k', keep_top_k, 'nms_eta', nms_eta, + 'normalized', normalized) + output, index, nms_rois_num = core.ops.multiclass_nms3(bboxes, scores, + rois_num, *attrs) + if return_index: + index = None + return output, nms_rois_num, index + + else: + output = helper.create_variable_for_type_inference(dtype=bboxes.dtype) + index = helper.create_variable_for_type_inference(dtype='int') + + inputs = {'BBoxes': bboxes, 'Scores': scores} + outputs = {'Out': output, 'Index': index} + + if rois_num is not None: + inputs['RoisNum'] = rois_num + + if return_rois_num: + nms_rois_num = helper.create_variable_for_type_inference( + dtype='int32') + outputs['NmsRoisNum'] = nms_rois_num + + helper.append_op( + type="multiclass_nms3", + inputs=inputs, + attrs={ + 'background_label': background_label, + 'score_threshold': score_threshold, + 'nms_top_k': nms_top_k, + 'nms_threshold': nms_threshold, + 'keep_top_k': keep_top_k, + 'nms_eta': nms_eta, + 'normalized': normalized + }, + outputs=outputs) + output.stop_gradient = True + index.stop_gradient = True + if not return_index: + index = None + if not return_rois_num: + nms_rois_num = None + + return output, nms_rois_num, index + + +@paddle.jit.not_to_static +def matrix_nms(bboxes, + scores, + score_threshold, + post_threshold, + nms_top_k, + keep_top_k, + use_gaussian=False, + gaussian_sigma=2., + background_label=0, + normalized=True, + return_index=False, + return_rois_num=True, + name=None): + """ + **Matrix NMS** + This operator does matrix non maximum suppression (NMS). + First selects a subset of candidate bounding boxes that have higher scores + than score_threshold (if provided), then the top k candidate is selected if + nms_top_k is larger than -1. Score of the remaining candidate are then + decayed according to the Matrix NMS scheme. + Aftern NMS step, at most keep_top_k number of total bboxes are to be kept + per image if keep_top_k is larger than -1. + Args: + bboxes (Tensor): A 3-D Tensor with shape [N, M, 4] represents the + predicted locations of M bounding bboxes, + N is the batch size. Each bounding box has four + coordinate values and the layout is + [xmin, ymin, xmax, ymax], when box size equals to 4. + The data type is float32 or float64. + scores (Tensor): A 3-D Tensor with shape [N, C, M] + represents the predicted confidence predictions. + N is the batch size, C is the class number, M is + number of bounding boxes. For each category there + are total M scores which corresponding M bounding + boxes. Please note, M is equal to the 2nd dimension + of BBoxes. The data type is float32 or float64. + score_threshold (float): Threshold to filter out bounding boxes with + low confidence score. + post_threshold (float): Threshold to filter out bounding boxes with + low confidence score AFTER decaying. + nms_top_k (int): Maximum number of detections to be kept according to + the confidences after the filtering detections based + on score_threshold. + keep_top_k (int): Number of total bboxes to be kept per image after NMS + step. -1 means keeping all bboxes after NMS step. + use_gaussian (bool): Use Gaussian as the decay function. Default: False + gaussian_sigma (float): Sigma for Gaussian decay function. Default: 2.0 + background_label (int): The index of background label, the background + label will be ignored. If set to -1, then all + categories will be considered. Default: 0 + normalized (bool): Whether detections are normalized. Default: True + return_index(bool): Whether return selected index. Default: False + return_rois_num(bool): whether return rois_num. Default: True + name(str): Name of the matrix nms op. Default: None. + Returns: + A tuple with three Tensor: (Out, Index, RoisNum) if return_index is True, + otherwise, a tuple with two Tensor (Out, RoisNum) is returned. + Out (Tensor): A 2-D Tensor with shape [No, 6] containing the + detection results. + Each row has 6 values: [label, confidence, xmin, ymin, xmax, ymax] + (After version 1.3, when no boxes detected, the lod is changed + from {0} to {1}) + Index (Tensor): A 2-D Tensor with shape [No, 1] containing the + selected indices, which are absolute values cross batches. + rois_num (Tensor): A 1-D Tensor with shape [N] containing + the number of detected boxes in each image. + Examples: + .. code-block:: python + import paddle + from ppdet.modeling import ops + boxes = paddle.static.data(name='bboxes', shape=[None,81, 4], + dtype='float32', lod_level=1) + scores = paddle.static.data(name='scores', shape=[None,81], + dtype='float32', lod_level=1) + out = ops.matrix_nms(bboxes=boxes, scores=scores, background_label=0, + score_threshold=0.5, post_threshold=0.1, + nms_top_k=400, keep_top_k=200, normalized=False) + """ + check_variable_and_dtype(bboxes, 'BBoxes', ['float32', 'float64'], + 'matrix_nms') + check_variable_and_dtype(scores, 'Scores', ['float32', 'float64'], + 'matrix_nms') + check_type(score_threshold, 'score_threshold', float, 'matrix_nms') + check_type(post_threshold, 'post_threshold', float, 'matrix_nms') + check_type(nms_top_k, 'nums_top_k', int, 'matrix_nms') + check_type(keep_top_k, 'keep_top_k', int, 'matrix_nms') + check_type(normalized, 'normalized', bool, 'matrix_nms') + check_type(use_gaussian, 'use_gaussian', bool, 'matrix_nms') + check_type(gaussian_sigma, 'gaussian_sigma', float, 'matrix_nms') + check_type(background_label, 'background_label', int, 'matrix_nms') + + if in_dygraph_mode(): + attrs = ('background_label', background_label, 'score_threshold', + score_threshold, 'post_threshold', post_threshold, 'nms_top_k', + nms_top_k, 'gaussian_sigma', gaussian_sigma, 'use_gaussian', + use_gaussian, 'keep_top_k', keep_top_k, 'normalized', + normalized) + out, index, rois_num = core.ops.matrix_nms(bboxes, scores, *attrs) + if not return_index: + index = None + if not return_rois_num: + rois_num = None + return out, rois_num, index + else: + helper = LayerHelper('matrix_nms', **locals()) + output = helper.create_variable_for_type_inference(dtype=bboxes.dtype) + index = helper.create_variable_for_type_inference(dtype='int') + outputs = {'Out': output, 'Index': index} + if return_rois_num: + rois_num = helper.create_variable_for_type_inference(dtype='int') + outputs['RoisNum'] = rois_num + + helper.append_op( + type="matrix_nms", + inputs={'BBoxes': bboxes, + 'Scores': scores}, + attrs={ + 'background_label': background_label, + 'score_threshold': score_threshold, + 'post_threshold': post_threshold, + 'nms_top_k': nms_top_k, + 'gaussian_sigma': gaussian_sigma, + 'use_gaussian': use_gaussian, + 'keep_top_k': keep_top_k, + 'normalized': normalized + }, + outputs=outputs) + output.stop_gradient = True + + if not return_index: + index = None + if not return_rois_num: + rois_num = None + return output, rois_num, index + + +def bipartite_match(dist_matrix, + match_type=None, + dist_threshold=None, + name=None): + """ + + This operator implements a greedy bipartite matching algorithm, which is + used to obtain the matching with the maximum distance based on the input + distance matrix. For input 2D matrix, the bipartite matching algorithm can + find the matched column for each row (matched means the largest distance), + also can find the matched row for each column. And this operator only + calculate matched indices from column to row. For each instance, + the number of matched indices is the column number of the input distance + matrix. **The OP only supports CPU**. + + There are two outputs, matched indices and distance. + A simple description, this algorithm matched the best (maximum distance) + row entity to the column entity and the matched indices are not duplicated + in each row of ColToRowMatchIndices. If the column entity is not matched + any row entity, set -1 in ColToRowMatchIndices. + + NOTE: the input DistMat can be LoDTensor (with LoD) or Tensor. + If LoDTensor with LoD, the height of ColToRowMatchIndices is batch size. + If Tensor, the height of ColToRowMatchIndices is 1. + + NOTE: This API is a very low level API. It is used by :code:`ssd_loss` + layer. Please consider to use :code:`ssd_loss` instead. + + Args: + dist_matrix(Tensor): This input is a 2-D LoDTensor with shape + [K, M]. The data type is float32 or float64. It is pair-wise + distance matrix between the entities represented by each row and + each column. For example, assumed one entity is A with shape [K], + another entity is B with shape [M]. The dist_matrix[i][j] is the + distance between A[i] and B[j]. The bigger the distance is, the + better matching the pairs are. NOTE: This tensor can contain LoD + information to represent a batch of inputs. One instance of this + batch can contain different numbers of entities. + match_type(str, optional): The type of matching method, should be + 'bipartite' or 'per_prediction'. None ('bipartite') by default. + dist_threshold(float32, optional): If `match_type` is 'per_prediction', + this threshold is to determine the extra matching bboxes based + on the maximum distance, 0.5 by default. + name(str, optional): For detailed information, please refer + to :ref:`api_guide_Name`. Usually name is no need to set and + None by default. + + Returns: + Tuple: + + matched_indices(Tensor): A 2-D Tensor with shape [N, M]. The data + type is int32. N is the batch size. If match_indices[i][j] is -1, it + means B[j] does not match any entity in i-th instance. + Otherwise, it means B[j] is matched to row + match_indices[i][j] in i-th instance. The row number of + i-th instance is saved in match_indices[i][j]. + + matched_distance(Tensor): A 2-D Tensor with shape [N, M]. The data + type is float32. N is batch size. If match_indices[i][j] is -1, + match_distance[i][j] is also -1.0. Otherwise, assumed + match_distance[i][j] = d, and the row offsets of each instance + are called LoD. Then match_distance[i][j] = + dist_matrix[d+LoD[i]][j]. + + Examples: + + .. code-block:: python + import paddle + from ppdet.modeling import ops + from ppdet.modeling.utils import iou_similarity + + paddle.enable_static() + + x = paddle.static.data(name='x', shape=[None, 4], dtype='float32') + y = paddle.static.data(name='y', shape=[None, 4], dtype='float32') + iou = iou_similarity(x=x, y=y) + matched_indices, matched_dist = ops.bipartite_match(iou) + """ + check_variable_and_dtype(dist_matrix, 'dist_matrix', + ['float32', 'float64'], 'bipartite_match') + + if in_dygraph_mode(): + match_indices, match_distance = core.ops.bipartite_match( + dist_matrix, "match_type", match_type, "dist_threshold", + dist_threshold) + return match_indices, match_distance + + helper = LayerHelper('bipartite_match', **locals()) + match_indices = helper.create_variable_for_type_inference(dtype='int32') + match_distance = helper.create_variable_for_type_inference( + dtype=dist_matrix.dtype) + helper.append_op( + type='bipartite_match', + inputs={'DistMat': dist_matrix}, + attrs={ + 'match_type': match_type, + 'dist_threshold': dist_threshold, + }, + outputs={ + 'ColToRowMatchIndices': match_indices, + 'ColToRowMatchDist': match_distance + }) + return match_indices, match_distance + + +@paddle.jit.not_to_static +def box_coder(prior_box, + prior_box_var, + target_box, + code_type="encode_center_size", + box_normalized=True, + axis=0, + name=None): + """ + **Box Coder Layer** + Encode/Decode the target bounding box with the priorbox information. + + The Encoding schema described below: + .. math:: + ox = (tx - px) / pw / pxv + oy = (ty - py) / ph / pyv + ow = \log(\abs(tw / pw)) / pwv + oh = \log(\abs(th / ph)) / phv + The Decoding schema described below: + + .. math:: + + ox = (pw * pxv * tx * + px) - tw / 2 + oy = (ph * pyv * ty * + py) - th / 2 + ow = \exp(pwv * tw) * pw + tw / 2 + oh = \exp(phv * th) * ph + th / 2 + where `tx`, `ty`, `tw`, `th` denote the target box's center coordinates, + width and height respectively. Similarly, `px`, `py`, `pw`, `ph` denote + the priorbox's (anchor) center coordinates, width and height. `pxv`, + `pyv`, `pwv`, `phv` denote the variance of the priorbox and `ox`, `oy`, + `ow`, `oh` denote the encoded/decoded coordinates, width and height. + During Box Decoding, two modes for broadcast are supported. Say target + box has shape [N, M, 4], and the shape of prior box can be [N, 4] or + [M, 4]. Then prior box will broadcast to target box along the + assigned axis. + + Args: + prior_box(Tensor): Box list prior_box is a 2-D Tensor with shape + [M, 4] holds M boxes and data type is float32 or float64. Each box + is represented as [xmin, ymin, xmax, ymax], [xmin, ymin] is the + left top coordinate of the anchor box, if the input is image feature + map, they are close to the origin of the coordinate system. + [xmax, ymax] is the right bottom coordinate of the anchor box. + prior_box_var(List|Tensor|None): prior_box_var supports three types + of input. One is Tensor with shape [M, 4] which holds M group and + data type is float32 or float64. The second is list consist of + 4 elements shared by all boxes and data type is float32 or float64. + Other is None and not involved in calculation. + target_box(Tensor): This input can be a 2-D LoDTensor with shape + [N, 4] when code_type is 'encode_center_size'. This input also can + be a 3-D Tensor with shape [N, M, 4] when code_type is + 'decode_center_size'. Each box is represented as + [xmin, ymin, xmax, ymax]. The data type is float32 or float64. + code_type(str): The code type used with the target box. It can be + `encode_center_size` or `decode_center_size`. `encode_center_size` + by default. + box_normalized(bool): Whether treat the priorbox as a normalized box. + Set true by default. + axis(int): Which axis in PriorBox to broadcast for box decode, + for example, if axis is 0 and TargetBox has shape [N, M, 4] and + PriorBox has shape [M, 4], then PriorBox will broadcast to [N, M, 4] + for decoding. It is only valid when code type is + `decode_center_size`. Set 0 by default. + name(str, optional): For detailed information, please refer + to :ref:`api_guide_Name`. Usually name is no need to set and + None by default. + + Returns: + Tensor: + output_box(Tensor): When code_type is 'encode_center_size', the + output tensor of box_coder_op with shape [N, M, 4] representing the + result of N target boxes encoded with M Prior boxes and variances. + When code_type is 'decode_center_size', N represents the batch size + and M represents the number of decoded boxes. + + Examples: + + .. code-block:: python + + import paddle + from ppdet.modeling import ops + paddle.enable_static() + # For encode + prior_box_encode = paddle.static.data(name='prior_box_encode', + shape=[512, 4], + dtype='float32') + target_box_encode = paddle.static.data(name='target_box_encode', + shape=[81, 4], + dtype='float32') + output_encode = ops.box_coder(prior_box=prior_box_encode, + prior_box_var=[0.1,0.1,0.2,0.2], + target_box=target_box_encode, + code_type="encode_center_size") + # For decode + prior_box_decode = paddle.static.data(name='prior_box_decode', + shape=[512, 4], + dtype='float32') + target_box_decode = paddle.static.data(name='target_box_decode', + shape=[512, 81, 4], + dtype='float32') + output_decode = ops.box_coder(prior_box=prior_box_decode, + prior_box_var=[0.1,0.1,0.2,0.2], + target_box=target_box_decode, + code_type="decode_center_size", + box_normalized=False, + axis=1) + """ + check_variable_and_dtype(prior_box, 'prior_box', ['float32', 'float64'], + 'box_coder') + check_variable_and_dtype(target_box, 'target_box', ['float32', 'float64'], + 'box_coder') + + if in_dygraph_mode(): + if isinstance(prior_box_var, Variable): + output_box = core.ops.box_coder( + prior_box, prior_box_var, target_box, "code_type", code_type, + "box_normalized", box_normalized, "axis", axis) + + elif isinstance(prior_box_var, list): + output_box = core.ops.box_coder( + prior_box, None, target_box, "code_type", code_type, + "box_normalized", box_normalized, "axis", axis, "variance", + prior_box_var) + else: + raise TypeError( + "Input variance of box_coder must be Variable or list") + return output_box + else: + helper = LayerHelper("box_coder", **locals()) + + output_box = helper.create_variable_for_type_inference( + dtype=prior_box.dtype) + + inputs = {"PriorBox": prior_box, "TargetBox": target_box} + attrs = { + "code_type": code_type, + "box_normalized": box_normalized, + "axis": axis + } + if isinstance(prior_box_var, Variable): + inputs['PriorBoxVar'] = prior_box_var + elif isinstance(prior_box_var, list): + attrs['variance'] = prior_box_var + else: + raise TypeError( + "Input variance of box_coder must be Variable or list") + helper.append_op( + type="box_coder", + inputs=inputs, + attrs=attrs, + outputs={"OutputBox": output_box}) + return output_box + + +@paddle.jit.not_to_static +def generate_proposals(scores, + bbox_deltas, + im_shape, + anchors, + variances, + pre_nms_top_n=6000, + post_nms_top_n=1000, + nms_thresh=0.5, + min_size=0.1, + eta=1.0, + pixel_offset=False, + return_rois_num=False, + name=None): + """ + **Generate proposal Faster-RCNN** + This operation proposes RoIs according to each box with their + probability to be a foreground object and + the box can be calculated by anchors. Bbox_deltais and scores + to be an object are the output of RPN. Final proposals + could be used to train detection net. + For generating proposals, this operation performs following steps: + 1. Transposes and resizes scores and bbox_deltas in size of + (H*W*A, 1) and (H*W*A, 4) + 2. Calculate box locations as proposals candidates. + 3. Clip boxes to image + 4. Remove predicted boxes with small area. + 5. Apply NMS to get final proposals as output. + Args: + scores(Tensor): A 4-D Tensor with shape [N, A, H, W] represents + the probability for each box to be an object. + N is batch size, A is number of anchors, H and W are height and + width of the feature map. The data type must be float32. + bbox_deltas(Tensor): A 4-D Tensor with shape [N, 4*A, H, W] + represents the difference between predicted box location and + anchor location. The data type must be float32. + im_shape(Tensor): A 2-D Tensor with shape [N, 2] represents H, W, the + origin image size or input size. The data type can be float32 or + float64. + anchors(Tensor): A 4-D Tensor represents the anchors with a layout + of [H, W, A, 4]. H and W are height and width of the feature map, + num_anchors is the box count of each position. Each anchor is + in (xmin, ymin, xmax, ymax) format an unnormalized. The data type must be float32. + variances(Tensor): A 4-D Tensor. The expanded variances of anchors with a layout of + [H, W, num_priors, 4]. Each variance is in + (xcenter, ycenter, w, h) format. The data type must be float32. + pre_nms_top_n(float): Number of total bboxes to be kept per + image before NMS. The data type must be float32. `6000` by default. + post_nms_top_n(float): Number of total bboxes to be kept per + image after NMS. The data type must be float32. `1000` by default. + nms_thresh(float): Threshold in NMS. The data type must be float32. `0.5` by default. + min_size(float): Remove predicted boxes with either height or + width < min_size. The data type must be float32. `0.1` by default. + eta(float): Apply in adaptive NMS, if adaptive `threshold > 0.5`, + `adaptive_threshold = adaptive_threshold * eta` in each iteration. + return_rois_num(bool): When setting True, it will return a 1D Tensor with shape [N, ] that includes Rois's + num of each image in one batch. The N is the image's num. For example, the tensor has values [4,5] that represents + the first image has 4 Rois, the second image has 5 Rois. It only used in rcnn model. + 'False' by default. + name(str, optional): For detailed information, please refer + to :ref:`api_guide_Name`. Usually name is no need to set and + None by default. + + Returns: + tuple: + A tuple with format ``(rpn_rois, rpn_roi_probs)``. + - **rpn_rois**: The generated RoIs. 2-D Tensor with shape ``[N, 4]`` while ``N`` is the number of RoIs. The data type is the same as ``scores``. + - **rpn_roi_probs**: The scores of generated RoIs. 2-D Tensor with shape ``[N, 1]`` while ``N`` is the number of RoIs. The data type is the same as ``scores``. + + Examples: + .. code-block:: python + + import paddle + from ppdet.modeling import ops + paddle.enable_static() + scores = paddle.static.data(name='scores', shape=[None, 4, 5, 5], dtype='float32') + bbox_deltas = paddle.static.data(name='bbox_deltas', shape=[None, 16, 5, 5], dtype='float32') + im_shape = paddle.static.data(name='im_shape', shape=[None, 2], dtype='float32') + anchors = paddle.static.data(name='anchors', shape=[None, 5, 4, 4], dtype='float32') + variances = paddle.static.data(name='variances', shape=[None, 5, 10, 4], dtype='float32') + rois, roi_probs = ops.generate_proposals(scores, bbox_deltas, + im_shape, anchors, variances) + """ + if in_dygraph_mode(): + assert return_rois_num, "return_rois_num should be True in dygraph mode." + attrs = ('pre_nms_topN', pre_nms_top_n, 'post_nms_topN', post_nms_top_n, + 'nms_thresh', nms_thresh, 'min_size', min_size, 'eta', eta, + 'pixel_offset', pixel_offset) + rpn_rois, rpn_roi_probs, rpn_rois_num = core.ops.generate_proposals_v2( + scores, bbox_deltas, im_shape, anchors, variances, *attrs) + return rpn_rois, rpn_roi_probs, rpn_rois_num + + else: + helper = LayerHelper('generate_proposals_v2', **locals()) + + check_variable_and_dtype(scores, 'scores', ['float32'], + 'generate_proposals_v2') + check_variable_and_dtype(bbox_deltas, 'bbox_deltas', ['float32'], + 'generate_proposals_v2') + check_variable_and_dtype(im_shape, 'im_shape', ['float32', 'float64'], + 'generate_proposals_v2') + check_variable_and_dtype(anchors, 'anchors', ['float32'], + 'generate_proposals_v2') + check_variable_and_dtype(variances, 'variances', ['float32'], + 'generate_proposals_v2') + + rpn_rois = helper.create_variable_for_type_inference( + dtype=bbox_deltas.dtype) + rpn_roi_probs = helper.create_variable_for_type_inference( + dtype=scores.dtype) + outputs = { + 'RpnRois': rpn_rois, + 'RpnRoiProbs': rpn_roi_probs, + } + if return_rois_num: + rpn_rois_num = helper.create_variable_for_type_inference( + dtype='int32') + rpn_rois_num.stop_gradient = True + outputs['RpnRoisNum'] = rpn_rois_num + + helper.append_op( + type="generate_proposals_v2", + inputs={ + 'Scores': scores, + 'BboxDeltas': bbox_deltas, + 'ImShape': im_shape, + 'Anchors': anchors, + 'Variances': variances + }, + attrs={ + 'pre_nms_topN': pre_nms_top_n, + 'post_nms_topN': post_nms_top_n, + 'nms_thresh': nms_thresh, + 'min_size': min_size, + 'eta': eta, + 'pixel_offset': pixel_offset + }, + outputs=outputs) + rpn_rois.stop_gradient = True + rpn_roi_probs.stop_gradient = True + + return rpn_rois, rpn_roi_probs, rpn_rois_num + + +def sigmoid_cross_entropy_with_logits(input, + label, + ignore_index=-100, + normalize=False): + output = F.binary_cross_entropy_with_logits(input, label, reduction='none') + mask_tensor = paddle.cast(label != ignore_index, 'float32') + output = paddle.multiply(output, mask_tensor) + if normalize: + sum_valid_mask = paddle.sum(mask_tensor) + output = output / sum_valid_mask + return output + + +def smooth_l1(input, label, inside_weight=None, outside_weight=None, + sigma=None): + input_new = paddle.multiply(input, inside_weight) + label_new = paddle.multiply(label, inside_weight) + delta = 1 / (sigma * sigma) + out = F.smooth_l1_loss(input_new, label_new, reduction='none', delta=delta) + out = paddle.multiply(out, outside_weight) + out = out / delta + out = paddle.reshape(out, shape=[out.shape[0], -1]) + out = paddle.sum(out, axis=1) + return out diff --git a/paddlevideo/modeling/heads/pptimesformer_head.py b/paddlevideo/modeling/heads/pptimesformer_head.py new file mode 100644 index 0000000000000000000000000000000000000000..113bde8b56835cb3fbd21b208c92c9b0127a1ccd --- /dev/null +++ b/paddlevideo/modeling/heads/pptimesformer_head.py @@ -0,0 +1,74 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from paddle.nn import Linear + +from ..registry import HEADS +from ..weight_init import trunc_normal_, weight_init_ +from .base import BaseHead +from paddle import ParamAttr +from paddle.regularizer import L2Decay + + +@HEADS.register() +class ppTimeSformerHead(BaseHead): + """TimeSformerHead Head. + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + std(float): Std(Scale) value in normal initilizar. Default: 0.01. + kwargs (dict, optional): Any keyword argument to initialize. + + """ + def __init__(self, + num_classes, + in_channels, + loss_cfg=dict(name='CrossEntropyLoss'), + std=0.02, + **kwargs): + + super().__init__(num_classes, in_channels, loss_cfg, **kwargs) + self.std = std + self.fc = Linear(self.in_channels, + self.num_classes, + bias_attr=ParamAttr(regularizer=L2Decay(0.0))) + + def init_weights(self): + """Initiate the FC layer parameters""" + + weight_init_(self.fc, + 'TruncatedNormal', + 'fc_0.w_0', + 'fc_0.b_0', + mean=0.0, + std=self.std) + # NOTE: Temporarily use trunc_normal_ instead of TruncatedNormal + trunc_normal_(self.fc.weight, std=self.std) + + def forward(self, x): + """Define how the head is going to run. + Args: + x (paddle.Tensor): The input data. + Returns: + score: (paddle.Tensor) The classification scores for input samples. + """ + # XXX: check dropout location! + # x.shape = [N, embed_dim] + + score = self.fc(x) + # [N, num_class] + # x = F.softmax(x) # NOTE remove + return score diff --git a/paddlevideo/modeling/heads/pptsm_head.py b/paddlevideo/modeling/heads/pptsm_head.py new file mode 100644 index 0000000000000000000000000000000000000000..88ad2a8e50415badf4f2b4c0616aa6fc41c42785 --- /dev/null +++ b/paddlevideo/modeling/heads/pptsm_head.py @@ -0,0 +1,88 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from paddle import ParamAttr +from paddle.nn import Linear +from paddle.regularizer import L2Decay +from .tsn_head import TSNHead +from ..registry import HEADS + +from ..weight_init import weight_init_ + + +@HEADS.register() +class ppTSMHead(TSNHead): + """ ppTSM Head + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + drop_ratio(float): drop ratio. Default: 0.8. + std(float): Std(Scale) value in normal initilizar. Default: 0.001. + kwargs (dict, optional): Any keyword argument to initialize. + """ + def __init__(self, + num_classes, + in_channels, + drop_ratio=0.8, + std=0.01, + data_format="NCHW", + **kwargs): + + super().__init__(num_classes, + in_channels, + drop_ratio=drop_ratio, + std=std, + data_format=data_format, + **kwargs) + + self.fc = Linear(self.in_channels, + self.num_classes, + weight_attr=ParamAttr(learning_rate=5.0, + regularizer=L2Decay(1e-4)), + bias_attr=ParamAttr(learning_rate=10.0, + regularizer=L2Decay(0.0))) + self.stdv = std + + def init_weights(self): + """Initiate the FC layer parameters""" + weight_init_(self.fc, 'Normal', 'fc_0.w_0', 'fc_0.b_0', std=self.stdv) + + def forward(self, x, num_seg): + """Define how the head is going to run. + Args: + x (paddle.Tensor): The input data. + num_segs (int): Number of segments. + Returns: + score: (paddle.Tensor) The classification scores for input samples. + """ + + #XXX: check dropout location! + # [N * num_segs, in_channels, 7, 7] + x = self.avgpool2d(x) + # [N * num_segs, in_channels, 1, 1] + if self.dropout is not None: + x = self.dropout(x) + # [N * num_seg, in_channels, 1, 1] + x = paddle.reshape(x, [-1, num_seg, x.shape[1]]) + # [N, num_seg, in_channels] + x = paddle.mean(x, axis=1) + # [N, in_channels] + x = paddle.reshape(x, shape=[-1, self.in_channels]) + # [N, in_channels] + score = self.fc(x) + # [N, num_class] + #x = F.softmax(x) #NOTE remove + return score diff --git a/paddlevideo/modeling/heads/pptsn_head.py b/paddlevideo/modeling/heads/pptsn_head.py new file mode 100644 index 0000000000000000000000000000000000000000..44314ac7de0c5eb63e965994fd7a4f29f3eeab46 --- /dev/null +++ b/paddlevideo/modeling/heads/pptsn_head.py @@ -0,0 +1,103 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from paddle import ParamAttr +from paddle.nn import AdaptiveAvgPool2D, Linear, Dropout +from paddle.regularizer import L2Decay +from .base import BaseHead +from ..registry import HEADS +from ..weight_init import weight_init_ + + +@HEADS.register() +class ppTSNHead(BaseHead): + """ppTSN Head. + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + drop_ratio(float): drop ratio. Default: 0.4. + std(float): Std(Scale) value in normal initilizar. Default: 0.01. + data_format(str): data format of input tensor in ['NCHW', 'NHWC']. Default: 'NCHW'. + fclr5(bool): Whether to increase the learning rate of the fully connected layer. Default: True + kwargs (dict, optional): Any keyword argument to initialize. + + """ + def __init__(self, + num_classes, + in_channels, + loss_cfg=dict(name='CrossEntropyLoss'), + drop_ratio=0.4, + std=0.01, + data_format="NCHW", + fclr5=True, + **kwargs): + + super().__init__(num_classes, in_channels, loss_cfg, **kwargs) + self.drop_ratio = drop_ratio + self.std = std + + # NOTE: global pool performance + self.avgpool2d = AdaptiveAvgPool2D((1, 1), data_format=data_format) + + if self.drop_ratio != 0: + self.dropout = Dropout(p=self.drop_ratio) + else: + self.dropout = None + self.fc = Linear( + self.in_channels, + self.num_classes, + weight_attr=ParamAttr(learning_rate=5.0 if fclr5 else 1.0, + regularizer=L2Decay(1e-4)), + bias_attr=ParamAttr(learning_rate=10.0 if fclr5 else 1.0, + regularizer=L2Decay(0.0))) + + def init_weights(self): + """Initiate the FC layer parameters""" + weight_init_(self.fc, + 'Normal', + 'fc_0.w_0', + 'fc_0.b_0', + mean=0., + std=self.std) + + def forward(self, x, num_seg): + """Define how the head is going to run. + + Args: + x (paddle.Tensor): The input data. + num_segs (int): Number of segments. + Returns: + score: (paddle.Tensor) The classification scores for input samples. + """ + + # XXX: check dropout location! + # [N * num_segs, in_channels, 7, 7] + x = self.avgpool2d(x) + # [N * num_segs, in_channels, 1, 1] + x = paddle.reshape(x, [-1, num_seg, x.shape[1]]) + # [N, num_seg, in_channels] + x = paddle.mean(x, axis=1) + # [N, in_channels] + if self.dropout is not None: + x = self.dropout(x) + # [N, in_channels] + x = paddle.reshape(x, shape=[-1, self.in_channels]) + # [N, in_channels] + score = self.fc(x) + # [N, num_class] + # x = F.softmax(x) # NOTE remove + return score diff --git a/paddlevideo/modeling/heads/roi_extractor.py b/paddlevideo/modeling/heads/roi_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..2a6b93beac0a7c6f635e673696c233c146dade80 --- /dev/null +++ b/paddlevideo/modeling/heads/roi_extractor.py @@ -0,0 +1,55 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from . import ops + + +#@register +class RoIAlign(object): + + def __init__(self, + resolution=14, + spatial_scale=0.0625, + sampling_ratio=0, + aligned=False): + super(RoIAlign, self).__init__() + self.resolution = resolution + self.spatial_scale = spatial_scale + self.sampling_ratio = sampling_ratio + self.aligned = aligned + + def __call__(self, feats, roi, rois_num): + roi = paddle.concat(roi) if len(roi) > 1 else roi[0] + rois_num = paddle.to_tensor(rois_num, dtype='int32') + rois_num = paddle.cast(rois_num, dtype='int32') + if len(feats) == 1: + roi_feat = ops.roi_align(feats, + roi, + self.resolution, + self.spatial_scale, + sampling_ratio=self.sampling_ratio, + rois_num=rois_num, + aligned=self.aligned) + else: + rois_feat_list = [] + roi_feat = ops.roi_align(feats, + roi, + self.resolution, + self.spatial_scale, + sampling_ratio=self.sampling_ratio, + rois_num=rois_num, + aligned=self.aligned) + + return roi_feat diff --git a/paddlevideo/modeling/heads/roi_head.py b/paddlevideo/modeling/heads/roi_head.py new file mode 100644 index 0000000000000000000000000000000000000000..be34a33efdb8c9fbbbeacc5f03d26e4cd72f0527 --- /dev/null +++ b/paddlevideo/modeling/heads/roi_head.py @@ -0,0 +1,177 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import paddle +import paddle.nn as nn +from .. import builder +from ..registry import HEADS + + +def bbox2result(bboxes, labels, num_classes, img_shape, thr=0.01): + """Convert detection results to a list of numpy arrays. """ + if len(bboxes) == 0: + return list(np.zeros((num_classes - 1, 0, 5), dtype=np.float32)) + else: + bboxes = bboxes[0] + labels = labels + img_shape_np = img_shape + img_h, img_w = img_shape_np[0][0], img_shape_np[0][1] + + img_w = paddle.cast(img_w, dtype='int32') + img_h = paddle.cast(img_h, dtype='int32') + + bboxes[:, 0::2] /= img_w + bboxes[:, 1::2] /= img_h + + # We only handle multilabel now + assert labels.shape[-1] > 1 + + scores = labels # rename + thr = (thr, ) * num_classes if isinstance(thr, float) else thr + assert scores.shape[1] == num_classes + assert len(thr) == num_classes + + result = [] + for i in range(num_classes - 1): + #step1. 对该类, 每个bbox的得分是否大于阈值 + where = scores[:, i + 1] > thr[i + 1] + + where = paddle.nonzero(where) # index + bboxes_select = paddle.index_select(x=bboxes, index=where) + bboxes_select = bboxes_select[:, :4] + + scores_select = paddle.index_select(x=scores, index=where) + scores_select = scores_select[:, i + 1:i + 2] + + result.append( + #对于step1中得分大于阈值的bbox(可能为空), 将bbox及在该类的score放入result列表. + paddle.concat((bboxes_select, scores_select), axis=1).numpy()) + + return result + + +@HEADS.register() +class AVARoIHead(nn.Layer): + + def __init__(self, + assigner, + sampler, + pos_weight=1.0, + action_thr=0.0, + bbox_roi_extractor=None, + bbox_head=None, + train_cfg=None, + test_cfg=None): + super().__init__() + self.assigner = assigner + self.sampler = sampler + self.pos_weight = pos_weight + self.action_thr = action_thr + self.init_assigner_sampler() + if bbox_head is not None: + self.init_bbox_head(bbox_roi_extractor, bbox_head) + + def init_assigner_sampler(self): + """Initialize assigner and sampler.""" + self.bbox_assigner = None + self.bbox_sampler = None + self.bbox_assigner = builder.build_assigner(self.assigner) + self.bbox_sampler = builder.build_sampler(self.sampler, context=self) + + def init_bbox_head(self, bbox_roi_extractor, bbox_head): + """Initialize ``bbox_head``""" + self.bbox_roi_extractor = builder.build_roi_extractor( + bbox_roi_extractor) + self.bbox_head = builder.build_head(bbox_head) + + def _bbox_forward(self, x, rois, rois_num): + bbox_feat = self.bbox_roi_extractor(x, rois, rois_num) + cls_score, bbox_pred = self.bbox_head( + bbox_feat, rois, rois_num + ) #deal with: when roi's width or height = 0 , roi_align is wrong + bbox_results = dict(cls_score=cls_score, + bbox_pred=bbox_pred, + bbox_feats=bbox_feat) + return bbox_results + + def _bbox_forward_train(self, x, sampling_results, gt_bboxes, gt_labels): + """Run forward function and calculate loss for box head in training.""" + rois = [res.bboxes for res in sampling_results] + rois_num = [res.bboxes.shape[0] for res in sampling_results] + bbox_results = self._bbox_forward(x, rois, rois_num) + bbox_targets = self.bbox_head.get_targets(sampling_results, gt_bboxes, + gt_labels, self.pos_weight) + loss_bbox = self.bbox_head.loss(bbox_results['cls_score'], bbox_targets) + bbox_results.update(loss_bbox=loss_bbox) + return bbox_results + + def train_step(self, x, img_metas, proposal_list, gt_bboxes, gt_labels): + #1. assign gts and sample proposals + num_imgs = len(img_metas[0]) + sampling_results = [] + for i in range(num_imgs): + assign_result = self.bbox_assigner.assign(proposal_list[i], + gt_bboxes[i], + gt_labels[i]) + sampling_result = self.bbox_sampler.sample(assign_result, + proposal_list[i], + gt_bboxes[i], + gt_labels[i]) + sampling_results.append(sampling_result) + + #2. forward and loss + bbox_results = self._bbox_forward_train(x, sampling_results, gt_bboxes, + gt_labels) + losses = dict() + losses.update(bbox_results['loss_bbox']) + + return losses + + def simple_test(self, x, proposal_list, img_shape, rescale=False): + x_shape = x[0].shape + #assert x_shape[0] == 1, 'only accept 1 sample at test mode' + + det_bboxes, det_labels = self.simple_test_bboxes(x, + img_shape, + proposal_list, + self.action_thr, + rescale=rescale) + + bbox_results = bbox2result(det_bboxes, det_labels, + self.bbox_head.num_classes, img_shape, + self.action_thr) + return [bbox_results] + + def simple_test_bboxes(self, + x, + img_shape, + proposals, + action_thr, + rescale=False): + """Test only det bboxes without augmentation.""" + rois = [proposals] + rois_num = [rois[0].shape[0]] + bbox_results = self._bbox_forward(x, rois, rois_num) + cls_score = bbox_results['cls_score'] + crop_quadruple = np.array([0, 0, 1, 1]) + flip = False + det_bboxes, det_labels = self.bbox_head.get_det_bboxes( + rois, + cls_score, + img_shape, + flip=flip, + crop_quadruple=crop_quadruple) + + return det_bboxes, det_labels diff --git a/paddlevideo/modeling/heads/single_straight3d.py b/paddlevideo/modeling/heads/single_straight3d.py new file mode 100644 index 0000000000000000000000000000000000000000..cf8569dd45e37b36d5a957509a53c680284aa042 --- /dev/null +++ b/paddlevideo/modeling/heads/single_straight3d.py @@ -0,0 +1,80 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +import paddle.nn as nn +import numpy as np +from ..registry import ROI_EXTRACTORS +from .roi_extractor import RoIAlign + + +@ROI_EXTRACTORS.register() +class SingleRoIExtractor3D(nn.Layer): + """Extract RoI features from a single level feature map. """ + + def __init__(self, + roi_layer_type='RoIAlign', + featmap_stride=16, + output_size=16, + sampling_ratio=0, + pool_mode='avg', + aligned=True, + with_temporal_pool=True, + with_global=False): + super().__init__() + self.roi_layer_type = roi_layer_type + assert self.roi_layer_type in ['RoIPool', 'RoIAlign'] + self.featmap_stride = featmap_stride + self.spatial_scale = 1. / self.featmap_stride + self.output_size = output_size + self.sampling_ratio = sampling_ratio + self.pool_mode = pool_mode + self.aligned = aligned + self.with_temporal_pool = with_temporal_pool + self.with_global = with_global + + self.roi_layer = RoIAlign(resolution=self.output_size, + spatial_scale=self.spatial_scale, + sampling_ratio=self.sampling_ratio, + aligned=self.aligned) + + def init_weights(self): + pass + + # The shape of feat is N, C, T, H, W + def forward(self, feat, rois, rois_num): + if len(feat) >= 2: + assert self.with_temporal_pool + if self.with_temporal_pool: + xi = 0 + for x in feat: + xi = xi + 1 + y = paddle.mean(x, 2, keepdim=True) + feat = [paddle.mean(x, 2, keepdim=True) for x in feat] + feat = paddle.concat(feat, axis=1) # merge slow and fast + roi_feats = [] + for t in range(feat.shape[2]): + if type(t) == paddle.fluid.framework.Variable: + index = paddle.to_tensor(t) + else: + data_index = np.array([t]).astype('int32') + index = paddle.to_tensor(data_index) + + frame_feat = paddle.index_select(feat, index, axis=2) + frame_feat = paddle.squeeze(frame_feat, + axis=2) #axis=2,避免N=1时, 第一维度被删除. + roi_feat = self.roi_layer(frame_feat, rois, rois_num) + roi_feats.append(roi_feat) + + ret = paddle.stack(roi_feats, axis=2) + return ret diff --git a/paddlevideo/modeling/heads/slowfast_head.py b/paddlevideo/modeling/heads/slowfast_head.py new file mode 100644 index 0000000000000000000000000000000000000000..bd18bafda10d00ef71d860e0f14bd32c1bbeb038 --- /dev/null +++ b/paddlevideo/modeling/heads/slowfast_head.py @@ -0,0 +1,137 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..registry import HEADS +from .base import BaseHead + +import paddle +import paddle.nn.functional as F + +from ..weight_init import weight_init_ + + +@HEADS.register() +class SlowFastHead(BaseHead): + """ + ResNe(X)t 3D head. + This layer performs a fully-connected projection during training, when the + input size is 1x1x1. It performs a convolutional projection during testing + when the input size is larger than 1x1x1. If the inputs are from multiple + different pathways, the inputs will be concatenated after pooling. + """ + def __init__(self, + width_per_group, + alpha, + beta, + num_classes, + num_frames, + crop_size, + dropout_rate, + pool_size_ratio=[[1, 1, 1], [1, 1, 1]], + loss_cfg=dict(name='CrossEntropyLoss'), + multigrid_short=False, + **kwargs): + """ + ResNetBasicHead takes p pathways as input where p in [1, infty]. + + Args: + dim_in (list): the list of channel dimensions of the p inputs to the + ResNetHead. + num_classes (int): the channel dimensions of the p outputs to the + ResNetHead. + pool_size (list): the list of kernel sizes of p spatial temporal + poolings, temporal pool kernel size, spatial pool kernel size, + spatial pool kernel size in order. + dropout_rate (float): dropout rate. If equal to 0.0, perform no + dropout. + """ + super().__init__(num_classes, loss_cfg, **kwargs) + self.multigrid_short = multigrid_short + self.width_per_group = width_per_group + self.alpha = alpha + self.beta = beta + self.num_classes = num_classes + self.num_frames = num_frames + self.crop_size = crop_size + self.dropout_rate = dropout_rate + self.pool_size_ratio = pool_size_ratio + + self.dim_in = [ + self.width_per_group * 32, + self.width_per_group * 32 // self.beta, + ] + self.pool_size = [None, None] if self.multigrid_short else [ + [ + self.num_frames // self.alpha // self.pool_size_ratio[0][0], + self.crop_size // 32 // self.pool_size_ratio[0][1], + self.crop_size // 32 // self.pool_size_ratio[0][2], + ], + [ + self.num_frames // self.pool_size_ratio[1][0], + self.crop_size // 32 // self.pool_size_ratio[1][1], + self.crop_size // 32 // self.pool_size_ratio[1][2], + ], + ] + + assert (len({len(self.pool_size), len(self.dim_in) + }) == 1), "pathway dimensions are not consistent." + self.num_pathways = len(self.pool_size) + + self.dropout = paddle.nn.Dropout(p=self.dropout_rate) + + self.projection = paddle.nn.Linear( + in_features=sum(self.dim_in), + out_features=self.num_classes, + ) + + def init_weights(self): + weight_init_(self.projection, + "Normal", + bias_value=0.0, + mean=0.0, + std=0.01) + + def forward(self, inputs): + assert (len(inputs) == self.num_pathways + ), "Input tensor does not contain {} pathway".format( + self.num_pathways) + pool_out = [] + for pathway in range(self.num_pathways): + if self.pool_size[pathway] is None: + tmp_out = F.adaptive_avg_pool3d(x=inputs[pathway], + output_size=(1, 1, 1), + data_format="NCDHW") + else: + tmp_out = F.avg_pool3d(x=inputs[pathway], + kernel_size=self.pool_size[pathway], + stride=1, + data_format="NCDHW") + pool_out.append(tmp_out) + + x = paddle.concat(x=pool_out, axis=1) + x = paddle.transpose(x=x, perm=(0, 2, 3, 4, 1)) + + # Perform dropout. + if self.dropout_rate > 0.0: + x = self.dropout(x) + + x = self.projection(x) + + # Performs fully convlutional inference. + if not self.training: # attr of base class + x = F.softmax(x, axis=4) + x = paddle.mean(x, axis=[1, 2, 3]) + + x = paddle.reshape(x, shape=(x.shape[0], -1)) + return x diff --git a/paddlevideo/modeling/heads/stgcn_head.py b/paddlevideo/modeling/heads/stgcn_head.py new file mode 100644 index 0000000000000000000000000000000000000000..fc80d6633378e4a820509d9eacf73a54f5823d2b --- /dev/null +++ b/paddlevideo/modeling/heads/stgcn_head.py @@ -0,0 +1,50 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn + +from .base import BaseHead +from ..registry import HEADS +from ..weight_init import weight_init_ + + +@HEADS.register() +class STGCNHead(BaseHead): + """ + Head for ST-GCN model. + Args: + in_channels: int, input feature channels. Default: 256. + num_classes: int, number classes. Default: 10. + """ + def __init__(self, in_channels=256, num_classes=10, **kwargs): + super().__init__(num_classes, in_channels, **kwargs) + self.fcn = nn.Conv2D(in_channels=in_channels, + out_channels=num_classes, + kernel_size=1) + + def init_weights(self): + """Initiate the parameters. + """ + for layer in self.sublayers(): + if isinstance(layer, nn.Conv2D): + weight_init_(layer, 'Normal', std=0.02) + + def forward(self, x): + """Define how the head is going to run. + """ + x = self.fcn(x) + x = paddle.reshape_(x, (x.shape[0], -1)) # N,C,1,1 --> N,C + + return x diff --git a/paddlevideo/modeling/heads/timesformer_head.py b/paddlevideo/modeling/heads/timesformer_head.py new file mode 100644 index 0000000000000000000000000000000000000000..d02a3cca8fc4f3e1de06832f8185f7718e42a179 --- /dev/null +++ b/paddlevideo/modeling/heads/timesformer_head.py @@ -0,0 +1,70 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from paddle.nn import Linear + +from ..registry import HEADS +from ..weight_init import trunc_normal_, weight_init_ +from .base import BaseHead + + +@HEADS.register() +class TimeSformerHead(BaseHead): + """TimeSformerHead Head. + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + std(float): Std(Scale) value in normal initilizar. Default: 0.01. + kwargs (dict, optional): Any keyword argument to initialize. + + """ + def __init__(self, + num_classes, + in_channels, + loss_cfg=dict(name='CrossEntropyLoss'), + std=0.02, + **kwargs): + + super().__init__(num_classes, in_channels, loss_cfg, **kwargs) + self.std = std + self.fc = Linear(self.in_channels, self.num_classes) + + def init_weights(self): + """Initiate the FC layer parameters""" + + weight_init_(self.fc, + 'TruncatedNormal', + 'fc_0.w_0', + 'fc_0.b_0', + mean=0.0, + std=self.std) + # NOTE: Temporarily use trunc_normal_ instead of TruncatedNormal + trunc_normal_(self.fc.weight, std=self.std) + + def forward(self, x): + """Define how the head is going to run. + Args: + x (paddle.Tensor): The input data. + Returns: + score: (paddle.Tensor) The classification scores for input samples. + """ + # XXX: check dropout location! + # x.shape = [N, embed_dim] + + score = self.fc(x) + # [N, num_class] + # x = F.softmax(x) # NOTE remove + return score diff --git a/paddlevideo/modeling/heads/transnetv2_head.py b/paddlevideo/modeling/heads/transnetv2_head.py new file mode 100644 index 0000000000000000000000000000000000000000..2ea67d4d325cc7fa541216fcb6266e3a7f292f1f --- /dev/null +++ b/paddlevideo/modeling/heads/transnetv2_head.py @@ -0,0 +1,45 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .base import BaseHead +from ..registry import HEADS +from ..losses import TransNetV2Loss +from ...metrics.transnetv2_metric import create_scene_based_summaries + +@HEADS.register() +class TransNetV2Head(BaseHead): + """TransNetV2 Head. + """ + def __init__(self, + num_classes, + in_channels, + loss_cfg=dict(name="TransNetV2Loss") + ): + super().__init__(num_classes, + in_channels, + loss_cfg) + + def loss(self, one_hot_pred, one_hot_gt, + many_hot_pred=None, many_hot_gt=None, reg_losses=None): + losses = dict() + loss = self.loss_func(scores, labels, **kwargs) + + f1 = self.get_score(one_hot_pred, one_hot_gt) + losses['f1'] = f1 + losses['loss'] = loss + return losses + + def get_score(self, one_hot_pred, one_hot_gt): + f1 = create_scene_based_summaries(one_hot_pred, one_hot_gt) + return f1 diff --git a/paddlevideo/modeling/heads/tsm_head.py b/paddlevideo/modeling/heads/tsm_head.py new file mode 100644 index 0000000000000000000000000000000000000000..955930168208d609f1dde9511c807048594d1fb6 --- /dev/null +++ b/paddlevideo/modeling/heads/tsm_head.py @@ -0,0 +1,99 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import paddle +from paddle import ParamAttr +from paddle.nn import Linear +import paddle.nn.functional as F +from paddle.regularizer import L2Decay +from .tsn_head import TSNHead +from ..registry import HEADS + +from ..weight_init import weight_init_ + + +@HEADS.register() +class TSMHead(TSNHead): + """ TSM Head + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + drop_ratio(float): drop ratio. Default: 0.5. + std(float): Std(Scale) value in normal initilizar. Default: 0.001. + kwargs (dict, optional): Any keyword argument to initialize. + """ + def __init__(self, + num_classes, + in_channels, + drop_ratio=0.5, + std=0.001, + data_format="NCHW", + **kwargs): + super().__init__(num_classes, + in_channels, + drop_ratio=drop_ratio, + std=std, + data_format=data_format, + **kwargs) + + self.fc = Linear(self.in_channels, + self.num_classes, + weight_attr=ParamAttr(learning_rate=5.0, + regularizer=L2Decay(1e-4)), + bias_attr=ParamAttr(learning_rate=10.0, + regularizer=L2Decay(0.0))) + + assert (data_format in [ + 'NCHW', 'NHWC' + ]), f"data_format must be 'NCHW' or 'NHWC', but got {data_format}" + + self.data_format = data_format + + self.stdv = std + + def init_weights(self): + """Initiate the FC layer parameters""" + weight_init_(self.fc, 'Normal', 'fc_0.w_0', 'fc_0.b_0', std=self.stdv) + + def forward(self, x, num_seg): + """Define how the tsm-head is going to run. + + Args: + x (paddle.Tensor): The input data. + num_segs (int): Number of segments. + Returns: + score: (paddle.Tensor) The classification scores for input samples. + """ + # x.shape = [N * num_segs, in_channels, 7, 7] + + x = self.avgpool2d(x) # [N * num_segs, in_channels, 1, 1] + + if self.dropout is not None: + x = self.dropout(x) # [N * num_seg, in_channels, 1, 1] + + if self.data_format == 'NCHW': + x = paddle.reshape(x, x.shape[:2]) + else: + x = paddle.reshape(x, x.shape[::3]) + score = self.fc(x) # [N * num_seg, num_class] + score = paddle.reshape( + score, [-1, num_seg, score.shape[1]]) # [N, num_seg, num_class] + score = paddle.mean(score, axis=1) # [N, num_class] + score = paddle.reshape(score, + shape=[-1, self.num_classes]) # [N, num_class] + # score = F.softmax(score) #NOTE remove + return score diff --git a/paddlevideo/modeling/heads/tsn_head.py b/paddlevideo/modeling/heads/tsn_head.py new file mode 100644 index 0000000000000000000000000000000000000000..f2f906bce9240b6fcd79b9db90e271366a64e270 --- /dev/null +++ b/paddlevideo/modeling/heads/tsn_head.py @@ -0,0 +1,93 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from paddle.nn import AdaptiveAvgPool2D, Linear, Dropout + +from .base import BaseHead +from ..registry import HEADS +from ..weight_init import weight_init_ + + +@HEADS.register() +class TSNHead(BaseHead): + """TSN Head. + + Args: + num_classes (int): The number of classes to be classified. + in_channels (int): The number of channles in input feature. + loss_cfg (dict): Config for building config. Default: dict(name='CrossEntropyLoss'). + drop_ratio(float): drop ratio. Default: 0.4. + std(float): Std(Scale) value in normal initilizar. Default: 0.01. + kwargs (dict, optional): Any keyword argument to initialize. + + """ + def __init__(self, + num_classes, + in_channels, + loss_cfg=dict(name='CrossEntropyLoss'), + drop_ratio=0.4, + std=0.01, + data_format="NCHW", + **kwargs): + + super().__init__(num_classes, in_channels, loss_cfg, **kwargs) + self.drop_ratio = drop_ratio + self.std = std + + #NOTE: global pool performance + self.avgpool2d = AdaptiveAvgPool2D((1, 1), data_format=data_format) + + if self.drop_ratio != 0: + self.dropout = Dropout(p=self.drop_ratio) + else: + self.dropout = None + + self.fc = Linear(self.in_channels, self.num_classes) + + def init_weights(self): + """Initiate the FC layer parameters""" + + weight_init_(self.fc, + 'Normal', + 'fc_0.w_0', + 'fc_0.b_0', + mean=0., + std=self.std) + + def forward(self, x, num_seg): + """Define how the head is going to run. + Args: + x (paddle.Tensor): The input data. + num_segs (int): Number of segments. + Returns: + score: (paddle.Tensor) The classification scores for input samples. + """ + + #XXX: check dropout location! + # [N * num_segs, in_channels, 7, 7] + + x = self.avgpool2d(x) + # [N * num_segs, in_channels, 1, 1] + x = paddle.reshape(x, [-1, num_seg, x.shape[1]]) + # [N, num_seg, in_channels] + x = paddle.mean(x, axis=1) + # [N, in_channels] + if self.dropout is not None: + x = self.dropout(x) + # [N, in_channels] + score = self.fc(x) + # [N, num_class] + #x = F.softmax(x) #NOTE remove + return score diff --git a/paddlevideo/modeling/losses/__init__.py b/paddlevideo/modeling/losses/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1410b95b6df724a083b2ba09c02c137fe916c0d9 --- /dev/null +++ b/paddlevideo/modeling/losses/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .base import BaseWeightedLoss +from .bmn_loss import BMNLoss +from .cross_entropy_loss import CrossEntropyLoss +from .depth_loss import ADDSLoss +from .transnetv2_loss import TransNetV2Loss +from .actbert_loss import ActBertLoss +from .asrf_loss import ASRFLoss + +__all__ = [ + 'CrossEntropyLoss', 'BMNLoss', 'TransNetV2Loss', 'ActBertLoss', 'ADDSLoss', + 'BaseWeightedLoss', 'ASRFLoss' +] diff --git a/paddlevideo/modeling/losses/actbert_loss.py b/paddlevideo/modeling/losses/actbert_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..10ffea6e650097420cba320c4e53e75a1cbfa21f --- /dev/null +++ b/paddlevideo/modeling/losses/actbert_loss.py @@ -0,0 +1,75 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from ..registry import LOSSES +from .base import BaseWeightedLoss + + +@LOSSES.register() +class ActBertLoss(BaseWeightedLoss): + """Loss for ActBert model + """ + def __init__(self, vocab_size=30522, a_target_size=700): + super().__init__() + self.vocab_size = vocab_size + self.a_target_size = a_target_size + self.loss_fct = nn.CrossEntropyLoss(ignore_index=-1) + self.vis_criterion = nn.KLDivLoss(reduction="none") + + def forward(self, prediction_scores_t, prediction_scores_v, prediction_scores_a, seq_relationship_score, \ + text_labels, image_label, image_target, action_label, next_sentence_label): + """ + Args: + text_label: text label(with mask). Shape: [batch_size, seqence_length] + image_label: image label(with mask). Shape: [batch_size, region_length] + image_target: label of image feature distribution, + Shape: [batch_size, region_length-1, num_image_class](minus 1 for xxx). + action label: action label(with mask), Shape: [batch_size, action_length] + next_sentence_label: is next sentence or not. Shape: [batch_size] + """ + prediction_scores_v = prediction_scores_v[:, + 1:] #8,37,1601 --> 8,36,1601 + + img_loss = self.vis_criterion( + F.log_softmax(prediction_scores_v, axis=2), + image_target #8,36,1601 + ) + masked_img_loss = paddle.sum( + img_loss * (image_label == 1).unsqueeze(2).astype('float32')) / max( + paddle.sum((image_label == 1).astype('float32')), 1e-6) + + masked_text_loss = self.loss_fct( + prediction_scores_t.reshape([-1, self.vocab_size]), #8,36,30522 + text_labels.reshape([-1]), #8,36 # label -1 will be ignored + ) + + masked_action_loss = self.loss_fct( + prediction_scores_a.reshape([-1, self.a_target_size]), #8,5,700 + action_label.reshape([-1]), #8,5 + ) + + next_sentence_loss = self.loss_fct( + seq_relationship_score.reshape([-1, 2]), + next_sentence_label.reshape([-1]) #8,2 + ) + + total_loss = masked_text_loss.unsqueeze(0) + masked_img_loss.unsqueeze( + 0) + masked_action_loss.unsqueeze(0) + next_sentence_loss.unsqueeze( + 0) + return total_loss diff --git a/paddlevideo/modeling/losses/asrf_loss.py b/paddlevideo/modeling/losses/asrf_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..ce5d6b1adc25fb38ea5f973aa1f33cd11e32752a --- /dev/null +++ b/paddlevideo/modeling/losses/asrf_loss.py @@ -0,0 +1,401 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://github.com/yiskw713/asrf/libs/loss_fn/__init__.py + +import numpy as np +import pandas as pd +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import sys +import os + +from ..registry import LOSSES + + +class TMSE(nn.Layer): + """ + Temporal MSE Loss Function + Proposed in Y. A. Farha et al. MS-TCN: Multi-Stage Temporal Convolutional Network for ActionSegmentation in CVPR2019 + arXiv: https://arxiv.org/pdf/1903.01945.pdf + """ + + def __init__(self, threshold=4, ignore_index=255): + super().__init__() + self.threshold = threshold + self.ignore_index = ignore_index + self.mse = nn.MSELoss(reduction="none") + + def forward(self, preds, gts): + + total_loss = 0.0 + batch_size = preds.shape[0] + for pred, gt in zip(preds, gts): + pred = paddle.gather(pred, + paddle.nonzero(gt != self.ignore_index)[:, 0]) + + loss = self.mse(F.log_softmax(pred[:, 1:], axis=1), + F.log_softmax(pred[:, :-1], axis=1)) + + loss = paddle.clip(loss, min=0, max=self.threshold**2) + total_loss += paddle.mean(loss) + + return total_loss / batch_size + + +class GaussianSimilarityTMSE(nn.Layer): + """ + Temporal MSE Loss Function with Gaussian Similarity Weighting + """ + + def __init__(self, threshold=4, sigma=1.0, ignore_index=255): + super().__init__() + self.threshold = threshold + self.ignore_index = ignore_index + self.mse = nn.MSELoss(reduction="none") + self.sigma = sigma + + def forward(self, preds, gts, sim_index): + """ + Args: + preds: the output of model before softmax. (N, C, T) + gts: Ground Truth. (N, T) + sim_index: similarity index. (N, C, T) + Return: + the value of Temporal MSE weighted by Gaussian Similarity. + """ + total_loss = 0.0 + batch_size = preds.shape[0] + for pred, gt, sim in zip(preds, gts, sim_index): + pred = paddle.gather(pred, + paddle.nonzero(gt != self.ignore_index)[:, 0], + axis=1) + sim = paddle.gather(sim, + paddle.nonzero(gt != self.ignore_index)[:, 0], + axis=1) + + # calculate gaussian similarity + diff = sim[:, 1:] - sim[:, :-1] + similarity = paddle.exp( + (-1 * paddle.norm(diff, axis=0)) / (2 * self.sigma**2)) + + # calculate temporal mse + loss = self.mse(F.log_softmax(pred[:, 1:], axis=1), + F.log_softmax(pred[:, :-1], axis=1)) + loss = paddle.clip(loss, min=0, max=self.threshold**2) + + # gaussian similarity weighting + loss = similarity * loss + + total_loss += paddle.mean(loss) + + return total_loss / batch_size + + +class FocalLoss(nn.Layer): + + def __init__(self, + weight=None, + size_average=True, + batch_average=True, + ignore_index=255, + gamma=2.0, + alpha=0.25): + super().__init__() + + self.gamma = gamma + self.alpha = alpha + self.batch_average = batch_average + self.criterion = nn.CrossEntropyLoss(weight=weight, + ignore_index=ignore_index, + size_average=size_average) + + def forward(self, logit, target): + n, _, _ = logit.size() + + logpt = -self.criterion(logit, target.long()) + pt = paddle.exp(logpt) + + if self.alpha is not None: + logpt *= self.alpha + + loss = -((1 - pt)**self.gamma) * logpt + + if self.batch_average: + loss /= n + + return loss + + +class ActionSegmentationLoss(nn.Layer): + """ + Loss Function for Action Segmentation + You can choose the below loss functions and combine them. + - Cross Entropy Loss (CE) + - Focal Loss + - Temporal MSE (TMSE) + - Gaussian Similarity TMSE (GSTMSE) + """ + + def __init__(self, + num_classes, + file_path, + label_path, + ce=True, + focal=True, + tmse=False, + gstmse=False, + weight=None, + threshold=4., + ignore_index=255, + ce_weight=1.0, + focal_weight=1.0, + tmse_weight=0.15, + gstmse_weight=0.15): + super().__init__() + self.criterions = [] + self.weights = [] + + self.num_classes = num_classes + self.file_path = file_path + self.label_path = label_path + if weight: + class_weight = self.get_class_weight() + else: + class_weight = None + + if ce: + self.criterions.append( + nn.CrossEntropyLoss(weight=class_weight, + ignore_index=ignore_index)) + self.weights.append(ce_weight) + + if focal: + self.criterions.append(FocalLoss(ignore_index=ignore_index)) + self.weights.append(focal_weight) + + if tmse: + self.criterions.append( + TMSE(threshold=threshold, ignore_index=ignore_index)) + self.weights.append(tmse_weight) + + if gstmse: + self.criterions.append( + GaussianSimilarityTMSE(threshold=threshold, + ignore_index=ignore_index)) + self.weights.append(gstmse_weight) + + if len(self.criterions) == 0: + print("You have to choose at least one loss function.") + sys.exit(1) + + def get_class_weight(self): + """ + Class weight for CrossEntropy + Class weight is calculated in the way described in: + D. Eigen and R. Fergus, “Predicting depth, surface normals and semantic labels with a common multi-scale convolutional architecture,” in ICCV, + openaccess: https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Eigen_Predicting_Depth_Surface_ICCV_2015_paper.pdf + """ + # load file list + file_ptr = open(self.file_path, 'r') + info = file_ptr.read().split('\n')[:-1] + file_ptr.close() + + nums = [0 for i in range(self.num_classes)] + for i in range(len(info)): + video_name = info[i] + file_name = video_name.split('.')[0] + ".npy" + label_file_path = os.path.join(self.label_path, file_name) + label = np.load(label_file_path).astype(np.int64) + num, cnt = np.unique(label, return_counts=True) + for n, c in zip(num, cnt): + nums[n] += c + + class_num = paddle.to_tensor(nums, dtype="float32") + total = class_num.sum().item() + frequency = class_num / total + median = paddle.median(frequency) + class_weight = median / frequency + return class_weight + + def forward(self, preds, gts, sim_index): + """ + Args: + preds: paddle.float (N, C, T). + gts: paddle.int64 (N, T). + sim_index: paddle.float (N, C', T). + """ + loss = 0.0 + for criterion, weight in zip(self.criterions, self.weights): + if isinstance(criterion, GaussianSimilarityTMSE): + loss += weight * criterion(preds, gts, sim_index) + elif isinstance(criterion, nn.CrossEntropyLoss): + preds_t = paddle.transpose(preds, perm=[0, 2, 1]) + loss += weight * criterion(preds_t, gts) + else: + loss += weight * criterion(preds, gts) + + return loss + + +class BoundaryRegressionLoss(nn.Layer): + """ + Boundary Regression Loss + bce: Binary Cross Entropy Loss for Boundary Prediction + mse: Mean Squared Error + """ + + def __init__(self, + file_path, + label_path, + bce=True, + focal=False, + mse=False, + weight=None, + pos_weight=None): + super().__init__() + + self.criterions = [] + self.file_path = file_path + self.label_path = label_path + + pos_weight = self.get_pos_weight() + + if bce: + self.criterions.append( + nn.BCEWithLogitsLoss(weight=weight, pos_weight=pos_weight)) + + if focal: + self.criterions.append(FocalLoss()) + + if mse: + self.criterions.append(nn.MSELoss()) + + if len(self.criterions) == 0: + print("You have to choose at least one loss function.") + sys.exit(1) + + def get_pos_weight(self, norm=None): + """ + pos_weight for binary cross entropy with logits loss + pos_weight is defined as reciprocal of ratio of positive samples in the dataset + """ + # load file list + file_ptr = open(self.file_path, 'r') + info = file_ptr.read().split('\n')[:-1] + file_ptr.close() + + n_classes = 2 # boundary or not + nums = [0 for i in range(n_classes)] + for i in range(len(info)): + video_name = info[i] + file_name = video_name.split('.')[0] + ".npy" + label_file_path = os.path.join(self.label_path, file_name) + label = np.load(label_file_path).astype(np.int64) + num, cnt = np.unique(label, return_counts=True) + for n, c in zip(num, cnt): + nums[n] += c + + pos_ratio = nums[1] / sum(nums) + pos_weight = 1 / pos_ratio + + if norm is not None: + pos_weight /= norm + + return paddle.to_tensor(pos_weight, dtype="float32") + + def forward(self, preds, gts): + """ + Args: + preds: paddle.float (N, 1, T). + gts: paddle.float (N, 1, T). + """ + loss = 0.0 + batch_size = float(preds.shape[0]) + + for criterion in self.criterions: + for pred, gt in zip(preds, gts): + loss += criterion(pred, gt) + + return loss / batch_size + + +@LOSSES.register() +class ASRFLoss(nn.Layer): + + def __init__(self, + lambda_bound_loss, + num_classes, + file_path, + label_path, + boundary_path, + ce=True, + asl_focal=True, + tmse=False, + gstmse=False, + asl_weight=None, + threshold=4., + ignore_index=255, + ce_weight=1.0, + focal_weight=1.0, + tmse_weight=0.15, + gstmse_weight=0.15, + bce=True, + brl_focal=False, + mse=False, + brl_weight=None): + super().__init__() + self.criterion_cls = ActionSegmentationLoss(ce=ce, + focal=asl_focal, + tmse=tmse, + gstmse=gstmse, + weight=asl_weight, + threshold=threshold, + ignore_index=ignore_index, + ce_weight=ce_weight, + focal_weight=focal_weight, + tmse_weight=tmse_weight, + gstmse_weight=gstmse_weight, + file_path=file_path, + label_path=label_path, + num_classes=num_classes) + self.criterion_boundary = BoundaryRegressionLoss( + bce=bce, + focal=brl_focal, + mse=mse, + weight=brl_weight, + file_path=file_path, + label_path=boundary_path) + self.lambda_bound_loss = lambda_bound_loss + + def forward(self, x, output_cls, label, outputs_boundary, boundary): + loss = 0.0 + if isinstance(output_cls, list): + n = len(output_cls) + for out in output_cls: + loss += self.criterion_cls(out, label, x) / n + else: + loss += self.criterion_cls(output_cls, label, x) + + if isinstance(outputs_boundary, list): + n = len(outputs_boundary) + for out in outputs_boundary: + loss += self.lambda_bound_loss * self.criterion_boundary( + out, boundary) / n + else: + loss += self.lambda_bound_loss * self.criterion_boundary( + outputs_boundary, boundary) + + return loss diff --git a/paddlevideo/modeling/losses/base.py b/paddlevideo/modeling/losses/base.py new file mode 100644 index 0000000000000000000000000000000000000000..7284252e6c9d6de4b4cdb29b8eb50058b859751b --- /dev/null +++ b/paddlevideo/modeling/losses/base.py @@ -0,0 +1,49 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod +import paddle +import paddle.nn as nn + +#XXX use _forward?? or forward?? +class BaseWeightedLoss(nn.Layer): + """Base class for loss. + + All subclass should overwrite the ``_forward()`` method which returns the + normal loss without loss weights. + + Args: + loss_weight (float): Factor scalar multiplied on the loss. + Default: 1.0. + """ + + def __init__(self, loss_weight=1.0): + super().__init__() + self.loss_weight = loss_weight + + @abstractmethod + def _forward(self, *args, **kwargs): + pass + + def forward(self, *args, **kwargs): + """Defines the computation performed at every call. + Args: + *args: The positional arguments for the corresponding + loss. + **kwargs: The keyword arguments for the corresponding + loss. + Returns: + paddle.Tensor: The calculated loss. + """ + return self._forward(*args, **kwargs) * self.loss_weight diff --git a/paddlevideo/modeling/losses/bmn_loss.py b/paddlevideo/modeling/losses/bmn_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..e4348501349967c94e0f7f2923bcdf0f73b75e02 --- /dev/null +++ b/paddlevideo/modeling/losses/bmn_loss.py @@ -0,0 +1,155 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import paddle +import paddle.nn.functional as F + +from ..registry import LOSSES +from .base import BaseWeightedLoss + + +@LOSSES.register() +class BMNLoss(BaseWeightedLoss): + """Loss for BMN model + Args: + tscale (int): sequence length, default 100. + dscale (int): max duration length, default 100. + """ + def __init__(self, dscale, tscale): + super().__init__() + self.dscale = dscale + self.tscale = tscale + + def _get_mask(self, dscale, tscale): + bm_mask = [] + for idx in range(dscale): + mask_vector = [1 for i in range(tscale - idx) + ] + [0 for i in range(idx)] + bm_mask.append(mask_vector) + bm_mask = np.array(bm_mask, dtype='float32') + bm_mask = paddle.to_tensor(bm_mask) + bm_mask.stop_gradient = True + return bm_mask + + def tem_loss_func(self, pred_start, pred_end, gt_start, gt_end): + def bi_loss(pred_score, gt_label, datatype): + pred_score = paddle.reshape(x=pred_score, shape=[-1]) + gt_label = paddle.reshape(x=gt_label, shape=[-1]) + gt_label.stop_gradient = True + pmask = paddle.cast(x=(gt_label > 0.5), dtype=datatype) + num_entries = paddle.cast(paddle.shape(pmask), dtype=datatype) + num_positive = paddle.cast(paddle.sum(pmask), dtype=datatype) + ratio = num_entries / num_positive + coef_0 = 0.5 * ratio / (ratio - 1) + coef_1 = 0.5 * ratio + epsilon = 0.000001 + loss_pos = paddle.multiply(paddle.log(pred_score + epsilon), pmask) + loss_pos = coef_1 * paddle.mean(loss_pos) + loss_neg = paddle.multiply(paddle.log(1.0 - pred_score + epsilon), + (1.0 - pmask)) + loss_neg = coef_0 * paddle.mean(loss_neg) + loss = -1 * (loss_pos + loss_neg) + return loss + + loss_start = bi_loss(pred_start, gt_start, pred_start.dtype) + loss_end = bi_loss(pred_end, gt_end, pred_start.dtype) + loss = loss_start + loss_end + return loss + + def pem_reg_loss_func(self, pred_score, gt_iou_map, mask): + gt_iou_map = paddle.multiply(gt_iou_map, mask) + + u_hmask = paddle.cast(x=gt_iou_map > 0.7, dtype=pred_score.dtype) + u_mmask = paddle.logical_and(gt_iou_map <= 0.7, gt_iou_map > 0.3) + u_mmask = paddle.cast(x=u_mmask, dtype=pred_score.dtype) + u_lmask = paddle.logical_and(gt_iou_map <= 0.3, gt_iou_map >= 0.) + u_lmask = paddle.cast(x=u_lmask, dtype=pred_score.dtype) + u_lmask = paddle.multiply(u_lmask, mask) + + num_h = paddle.cast(paddle.sum(u_hmask), dtype=pred_score.dtype) + num_m = paddle.cast(paddle.sum(u_mmask), dtype=pred_score.dtype) + num_l = paddle.cast(paddle.sum(u_lmask), dtype=pred_score.dtype) + + r_m = num_h / num_m + u_smmask = paddle.uniform(shape=[ + gt_iou_map.shape[1], gt_iou_map.shape[2] + ], + min=0.0, + max=1.0).astype(pred_score.dtype) + u_smmask = paddle.multiply(u_mmask, u_smmask) + u_smmask = paddle.cast(x=(u_smmask > (1. - r_m)), + dtype=pred_score.dtype) + + r_l = num_h / num_l + u_slmask = paddle.uniform(shape=[ + gt_iou_map.shape[1], gt_iou_map.shape[2] + ], + min=0.0, + max=1.0).astype(pred_score.dtype) + u_slmask = paddle.multiply(u_lmask, u_slmask) + u_slmask = paddle.cast(x=(u_slmask > (1. - r_l)), + dtype=pred_score.dtype) + + weights = u_hmask + u_smmask + u_slmask + weights.stop_gradient = True + loss = F.square_error_cost(pred_score, gt_iou_map) + loss = paddle.multiply(loss, weights) + loss = 0.5 * paddle.sum(loss) / paddle.sum(weights) + + return loss + + def pem_cls_loss_func(self, pred_score, gt_iou_map, mask): + gt_iou_map = paddle.multiply(gt_iou_map, mask) + gt_iou_map.stop_gradient = True + pmask = paddle.cast(x=(gt_iou_map > 0.9), dtype=pred_score.dtype) + nmask = paddle.cast(x=(gt_iou_map <= 0.9), dtype=pred_score.dtype) + nmask = paddle.multiply(nmask, mask) + + num_positive = paddle.sum(pmask) + num_entries = num_positive + paddle.sum(nmask) + ratio = num_entries / num_positive + coef_0 = 0.5 * ratio / (ratio - 1) + coef_1 = 0.5 * ratio + epsilon = 0.000001 + loss_pos = paddle.multiply(paddle.log(pred_score + epsilon), pmask) + loss_pos = coef_1 * paddle.sum(loss_pos) + loss_neg = paddle.multiply(paddle.log(1.0 - pred_score + epsilon), + nmask) + loss_neg = coef_0 * paddle.sum(loss_neg) + loss = -1 * (loss_pos + loss_neg) / num_entries + return loss + + def forward(self, pred_bm, pred_start, pred_end, gt_iou_map, gt_start, + gt_end): + pred_bm_reg = paddle.squeeze(paddle.slice(pred_bm, + axes=[1], + starts=[0], + ends=[1]), + axis=[1]) + pred_bm_cls = paddle.squeeze(paddle.slice(pred_bm, + axes=[1], + starts=[1], + ends=[2]), + axis=[1]) + + bm_mask = self._get_mask(self.dscale, self.tscale) + + pem_reg_loss = self.pem_reg_loss_func(pred_bm_reg, gt_iou_map, bm_mask) + pem_cls_loss = self.pem_cls_loss_func(pred_bm_cls, gt_iou_map, bm_mask) + + tem_loss = self.tem_loss_func(pred_start, pred_end, gt_start, gt_end) + + loss = tem_loss + 10 * pem_reg_loss + pem_cls_loss + return loss diff --git a/paddlevideo/modeling/losses/cross_entropy_loss.py b/paddlevideo/modeling/losses/cross_entropy_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..953f77c0791ed12b1626f0e1089fa9fdab81c386 --- /dev/null +++ b/paddlevideo/modeling/losses/cross_entropy_loss.py @@ -0,0 +1,36 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn.functional as F + +from ..registry import LOSSES +from .base import BaseWeightedLoss + + +@LOSSES.register() +class CrossEntropyLoss(BaseWeightedLoss): + """Cross Entropy Loss.""" + def _forward(self, score, labels, **kwargs): + """Forward function. + Args: + score (paddle.Tensor): The class score. + labels (paddle.Tensor): The ground truth labels. + kwargs: Any keyword argument to be used to calculate + CrossEntropy loss. + Returns: + loss (paddle.Tensor): The returned CrossEntropy loss. + """ + loss = F.cross_entropy(score, labels, **kwargs) + return loss diff --git a/paddlevideo/modeling/losses/depth_loss.py b/paddlevideo/modeling/losses/depth_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..ba9a2cb04efb697052f7412108520cb8707862b5 --- /dev/null +++ b/paddlevideo/modeling/losses/depth_loss.py @@ -0,0 +1,290 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn as nn + +from ..registry import LOSSES +from .base import BaseWeightedLoss + + +def get_smooth_loss(disp, img): + """Computes the smoothness loss for a disparity image + The color image is used for edge-aware smoothness + """ + grad_disp_x = paddle.abs(disp[:, :, :, :-1] - disp[:, :, :, 1:]) + grad_disp_y = paddle.abs(disp[:, :, :-1, :] - disp[:, :, 1:, :]) + + grad_img_x = paddle.mean(paddle.abs(img[:, :, :, :-1] - img[:, :, :, 1:]), + 1, + keepdim=True) + grad_img_y = paddle.mean(paddle.abs(img[:, :, :-1, :] - img[:, :, 1:, :]), + 1, + keepdim=True) + + grad_disp_x *= paddle.exp(-grad_img_x) + grad_disp_y *= paddle.exp(-grad_img_y) + + return grad_disp_x.mean() + grad_disp_y.mean() + + +class DiffLoss(nn.Layer): + def __init__(self): + super(DiffLoss, self).__init__() + + def forward(self, input1, input2): + batch_size = input1.shape[0] + input1 = input1.reshape([batch_size, -1]) + input2 = input2.reshape([batch_size, -1]) + + input1_l2 = input1 + input2_l2 = input2 + + diff_loss = 0 + dim = input1.shape[1] + for i in range(input1.shape[0]): + diff_loss = diff_loss + paddle.mean( + ((input1_l2[i:i + 1, :].mm(input2_l2[i:i + 1, :].T)).pow(2)) / + dim) + + diff_loss = diff_loss / input1.shape[0] + + return diff_loss + + +class MSE(nn.Layer): + def __init__(self): + super(MSE, self).__init__() + + def forward(self, pred, real): + diffs = paddle.add(real, -pred) + n = paddle.numel(diffs) + mse = paddle.sum(diffs.pow(2)) / n + + return mse + + +class SIMSE(nn.Layer): + def __init__(self): + super(SIMSE, self).__init__() + + def forward(self, pred, real): + diffs = paddle.add(real, -pred) + n = paddle.numel(diffs) + simse = paddle.sum(diffs).pow(2) / (n**2) + + return simse + + +class SSIM(nn.Layer): + """Layer to compute the SSIM loss between a pair of images + """ + def __init__(self): + super(SSIM, self).__init__() + self.mu_x_pool = nn.AvgPool2D(3, 1, exclusive=False) + self.mu_y_pool = nn.AvgPool2D(3, 1, exclusive=False) + self.sig_x_pool = nn.AvgPool2D(3, 1, exclusive=False) + self.sig_y_pool = nn.AvgPool2D(3, 1, exclusive=False) + self.sig_xy_pool = nn.AvgPool2D(3, 1, exclusive=False) + + self.refl = nn.Pad2D(1, mode='reflect') + + self.C1 = 0.01**2 + self.C2 = 0.03**2 + + def forward(self, x, y): + x = self.refl(x) + y = self.refl(y) + + mu_x = self.mu_x_pool(x) + mu_y = self.mu_y_pool(y) + + sigma_x = self.sig_x_pool(x**2) - mu_x**2 + sigma_y = self.sig_y_pool(y**2) - mu_y**2 + sigma_xy = self.sig_xy_pool(x * y) - mu_x * mu_y + + SSIM_n = (2 * mu_x * mu_y + self.C1) * (2 * sigma_xy + self.C2) + SSIM_d = (mu_x**2 + mu_y**2 + self.C1) * (sigma_x + sigma_y + self.C2) + + return paddle.clip((1 - SSIM_n / SSIM_d) / 2, 0, 1) + + +@LOSSES.register() +class ADDSLoss(BaseWeightedLoss): + def __init__(self, avg_reprojection, disparity_smoothness, no_ssim): + super(ADDSLoss, self).__init__() + self.avg_reprojection = avg_reprojection + self.disparity_smoothness = disparity_smoothness + self.no_ssim = no_ssim + + self.loss_diff = DiffLoss() + self.loss_recon1 = MSE() + self.loss_recon2 = SIMSE() + self.loss_similarity = MSE() + + def compute_reprojection_loss(self, pred, target): + """Computes reprojection loss between a batch of predicted and target images + """ + abs_diff = paddle.abs(target - pred) + l1_loss = abs_diff.mean(1, True) + + if not self.no_ssim: + self.ssim = SSIM() + + if self.no_ssim: + reprojection_loss = l1_loss + else: + ssim_loss = self.ssim(pred, target).mean(1, True) + reprojection_loss = 0.85 * ssim_loss + 0.15 * l1_loss + + return reprojection_loss + + def compute_losses(self, inputs, outputs, is_night): + """Compute the reprojection and smoothness losses for a minibatch + """ + losses = {} + total_loss = 0 + + for scale in outputs['scales']: + loss = 0 + reprojection_losses = [] + + source_scale = 0 + + disp = outputs[("disp", scale)] + if is_night: + color = inputs[("color_n", 0, scale)] + target = inputs[("color_n", 0, source_scale)] + else: + color = inputs[("color", 0, scale)] + target = inputs[("color", 0, source_scale)] + + for frame_id in outputs['frame_ids'][1:]: + pred = outputs[("color", frame_id, scale)] + reprojection_losses.append( + self.compute_reprojection_loss(pred, target)) + + reprojection_losses = paddle.concat(reprojection_losses, 1) + + identity_reprojection_losses = [] + for frame_id in outputs['frame_ids'][1:]: + if is_night: + pred = inputs[("color_n", frame_id, source_scale)] + else: + pred = inputs[("color", frame_id, source_scale)] + identity_reprojection_losses.append( + self.compute_reprojection_loss(pred, target)) + + identity_reprojection_losses = paddle.concat( + identity_reprojection_losses, 1) + + if self.avg_reprojection: + identity_reprojection_loss = identity_reprojection_losses.mean( + 1, keepdim=True) + else: + # save both images, and do min all at once below + identity_reprojection_loss = identity_reprojection_losses + + if self.avg_reprojection: + reprojection_loss = reprojection_losses.mean(1, keepdim=True) + else: + reprojection_loss = reprojection_losses + + # add random numbers to break ties + identity_reprojection_loss = identity_reprojection_loss + paddle.randn( + identity_reprojection_loss.shape) * 0.00001 + + combined = paddle.concat( + (identity_reprojection_loss, reprojection_loss), axis=1) + if combined.shape[1] == 1: + to_optimise = combined + else: + to_optimise = paddle.min(combined, axis=1) + + loss = loss + to_optimise.mean() + + mean_disp = disp.mean(2, True).mean(3, True) + norm_disp = disp / (mean_disp + 1e-7) + smooth_loss = get_smooth_loss(norm_disp, color) + + loss = loss + self.disparity_smoothness * smooth_loss / (2**scale) + total_loss = total_loss + loss + losses["loss/{}".format(scale)] = loss + + total_loss /= len(outputs['scales']) + losses["loss"] = total_loss + return losses + + def forward(self, inputs, outputs): + + losses_day = self.compute_losses(inputs, outputs, 'day') + losses_night = self.compute_losses(inputs, outputs['outputs_night'], + 'night') + + loss = 0 + losses = [] + # diff + target_diff1 = 0.5 * self.loss_diff( + outputs['result'][0], outputs['result'][2]) # 10 when batchsize=1 + target_diff2 = 0.5 * self.loss_diff(outputs['result_night'][0], + outputs['result_night'][2]) + losses.append(target_diff1) + losses.append(target_diff2) + loss = loss + target_diff1 + loss = loss + target_diff2 + + target_diff3 = 1 * self.loss_diff( + outputs['result'][1], outputs['result'][3]) # 10 when batchsize=1 + target_diff4 = 1 * self.loss_diff(outputs['result_night'][1], + outputs['result_night'][3]) + losses.append(target_diff3) + losses.append(target_diff4) + loss = loss + target_diff3 + loss = loss + target_diff4 + + # recon + target_mse = 1 * self.loss_recon1(outputs['result'][5], + inputs["color_aug", 0, 0]) + loss = loss + target_mse + + target_simse = 1 * self.loss_recon2(outputs['result'][5], + inputs["color_aug", 0, 0]) + loss = loss + target_simse + + losses.append(target_mse) + losses.append(target_simse) + target_mse_night = 1 * self.loss_recon1(outputs['result_night'][5], + inputs["color_n_aug", 0, 0]) + loss = loss + target_mse_night + + target_simse_night = 1 * self.loss_recon2(outputs['result_night'][5], + inputs["color_n_aug", 0, 0]) + loss = loss + target_simse_night + + losses.append(target_mse_night) + losses.append(target_simse_night) + + # depth loss + pseudo_label = outputs[("disp", 0)].detach() + depth_loss = 1 * self.loss_similarity( + outputs['outputs_night'][("disp", 0)], pseudo_label) + loss = loss + depth_loss + + losses.append(depth_loss) + + outputs['loss'] = loss + losses_day['loss'] + losses_night['loss'] + outputs['losses_day'] = losses_day['loss'] + outputs['losses_night'] = losses_night['loss'] + + return outputs diff --git a/paddlevideo/modeling/losses/transnetv2_loss.py b/paddlevideo/modeling/losses/transnetv2_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..624c46852857e714009a033d52933f66cb1f95c7 --- /dev/null +++ b/paddlevideo/modeling/losses/transnetv2_loss.py @@ -0,0 +1,56 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import paddle.nn.functional as F +from ..registry import LOSSES +from .base import BaseWeightedLoss + + +@LOSSES.register() +class TransNetV2Loss(BaseWeightedLoss): + """Loss for TransNetV2 model + """ + def __init__(self, transition_weight=5.0, many_hot_loss_weight=0.1): + self.transition_weight = transition_weight + self.many_hot_loss_weight = many_hot_loss_weight + super().__init__() + + def _forward(self, one_hot_pred, one_hot_gt, + many_hot_pred=None, many_hot_gt=None, reg_losses=None): + assert transition_weight != 1 + + one_hot_pred = one_hot_pred[:, :, 0] + + one_hot_gt = one_hot_gt.astype('float32') + one_hot_loss = F.binary_cross_entropy_with_logits(logit=one_hot_pred, label=one_hot_gt, reduction='none') + + one_hot_loss *= 1 + one_hot_gt * (transition_weight - 1) + + one_hot_loss = paddle.mean(one_hot_loss) + + many_hot_loss = 0. + if many_hot_loss_weight != 0. and many_hot_pred is not None: + many_hot_loss = many_hot_loss_weight * paddle.mean( + F.binary_cross_entropy_with_logits(logit=many_hot_pred[:, :, 0], + label=many_hot_gt.astype('float32'), reduction='none')) + + total_loss = one_hot_loss + many_hot_loss + + if reg_losses is not None: + for name, value in reg_losses.items(): + if value is not None: + total_loss += value + + return total_loss \ No newline at end of file diff --git a/paddlevideo/modeling/registry.py b/paddlevideo/modeling/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..b8140e1c2ede0cd3bf08c2f8108dcbe48bcf9f2d --- /dev/null +++ b/paddlevideo/modeling/registry.py @@ -0,0 +1,31 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..utils import Registry + +BACKBONES = Registry('backbone') +HEADS = Registry('head') +RECOGNIZERS = Registry('recognizer') +SEGMENTERS = Registry('Segmenters') +LOCALIZERS = Registry('localizer') +PARTITIONERS = Registry('partitioner') +LOSSES = Registry('loss') +ROI_EXTRACTORS = Registry('roi_extractor') +DETECTORS = Registry('detectors') +BBOX_ASSIGNERS = Registry('bbox_assigner') +BBOX_SAMPLERS = Registry('bbox_sampler') +BBOX_CODERS = Registry('bbox_coder') +ESTIMATORS = Registry('estimator') +MULTIMODAL = Registry('multimodal') +SEGMENT = Registry('segment') diff --git a/paddlevideo/modeling/samplers/__init__.py b/paddlevideo/modeling/samplers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0cf7f15e5fe624698fdca9751b312187a4999a64 --- /dev/null +++ b/paddlevideo/modeling/samplers/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .random_sampler import RandomSampler + +__all__ = ['RandomSampler'] diff --git a/paddlevideo/modeling/samplers/random_sampler.py b/paddlevideo/modeling/samplers/random_sampler.py new file mode 100644 index 0000000000000000000000000000000000000000..7b9f417886e83cb0a81ef370141ed0f8ca64c429 --- /dev/null +++ b/paddlevideo/modeling/samplers/random_sampler.py @@ -0,0 +1,146 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +import numpy as np +from ..registry import BBOX_SAMPLERS + +class SamplingResult(): + """Bbox sampling result. """ + + def __init__(self, pos_inds, neg_inds, bboxes, gt_bboxes, assign_result, + gt_flags): + self.pos_inds = pos_inds + self.neg_inds = neg_inds + self.pos_bboxes = paddle.index_select(bboxes,pos_inds) + + # neg_inds may be empty + if neg_inds.shape[0]!=0: + self.neg_bboxes = paddle.index_select(bboxes,neg_inds) + else: + self.neg_bboxes=None + + self.pos_is_gt = paddle.index_select(gt_flags,pos_inds) + self.num_gts = gt_bboxes.shape[0] + self.pos_assigned_gt_inds = paddle.index_select(assign_result.gt_inds,pos_inds) - 1 + + if gt_bboxes.numel().numpy()[0] == 0: + assert self.pos_assigned_gt_inds.numel() == 0 + self.pos_gt_bboxes = paddle.empty_like(gt_bboxes).view(-1, 4) + else: + if len(gt_bboxes.shape) < 2: + gt_bboxes = gt_bboxes.view(-1, 4) + + self.pos_gt_bboxes = paddle.index_select(gt_bboxes, self.pos_assigned_gt_inds) + + if assign_result.labels is not None: + self.pos_gt_labels = paddle.index_select(assign_result.labels, pos_inds) + else: + self.pos_gt_labels = None + + @property + def bboxes(self): + if self.neg_bboxes is not None: + ret = paddle.concat([self.pos_bboxes, self.neg_bboxes]) + else: + # neg bbox may be empty + ret = self.pos_bboxes + return ret + + + +@BBOX_SAMPLERS.register() +class RandomSampler(): + def __init__(self, + num, + pos_fraction, + neg_pos_ub=-1, + add_gt_as_proposals=True, + **kwargs): + self.num = num + self.pos_fraction = pos_fraction + self.neg_pos_ub = neg_pos_ub + self.add_gt_as_proposals = add_gt_as_proposals + + def sample(self, + assign_result, + bboxes, + gt_bboxes, + gt_labels=None, + **kwargs): + """Sample positive and negative bboxes. """ + + if len(bboxes.shape) < 2: + bboxes = bboxes[None, :] + + bboxes = bboxes[:, :4] + + gt_flags = paddle.full([bboxes.shape[0], ], 0, dtype='int32') + if self.add_gt_as_proposals and len(gt_bboxes) > 0: + if gt_labels is None: + raise ValueError( + 'gt_labels must be given when add_gt_as_proposals is True') + bboxes = paddle.concat([gt_bboxes, bboxes]) + assign_result.add_gt_(gt_labels) + gt_ones = paddle.full([gt_bboxes.shape[0], ], 1, dtype='int32') + gt_flags = paddle.concat([gt_ones, gt_flags]) + + #1. 得到正样本的数量, inds + num_expected_pos = int(self.num * self.pos_fraction) + pos_inds = self._sample_pos( assign_result, num_expected_pos, bboxes=bboxes, **kwargs) + pos_inds = paddle.to_tensor(np.unique(pos_inds.numpy())) + + #2. 得到负样本的数量, inds + num_sampled_pos = pos_inds.numel() + num_expected_neg = self.num - num_sampled_pos + neg_inds = self._sample_neg( + assign_result, num_expected_neg, bboxes=bboxes, **kwargs) + neg_inds = paddle.to_tensor(np.unique(neg_inds.numpy())) + + #3. 得到sampling result + sampling_result = SamplingResult(pos_inds, neg_inds, bboxes, gt_bboxes, + assign_result, gt_flags) + return sampling_result + def random_choice(self, gallery, num): + """Random select some elements from the gallery. """ + assert len(gallery) >= num + + perm = paddle.arange(gallery.numel())[:num] + perm = paddle.randperm(gallery.numel())[:num] + rand_inds = paddle.index_select(gallery, perm) + return rand_inds + + def _sample_pos(self, assign_result, num_expected, **kwargs): + """Randomly sample some positive samples.""" + #1.首先看一下给的bboxes里面有哪些label是大于0的 得到了他们的index + pos_inds = paddle.nonzero(assign_result.gt_inds, as_tuple=False) + + #2. 只要这个pos_inds的数目不是0个 这些就都可以是positive sample + # 当pos_inds的数目小于num_expected(想要的sample的最大数目), 就直接用这个pos_inds + # 反之就从这么多index里随机采样num_expected个出来 + if pos_inds.numel().numpy()[0] != 0: + pos_inds = pos_inds.squeeze() + if pos_inds.numel().numpy()[0] <= num_expected: + return pos_inds + else: + return self.random_choice(pos_inds, num_expected) + + def _sample_neg(self, assign_result, num_expected, **kwargs): + """Randomly sample some negative samples.""" + neg_inds = paddle.nonzero(assign_result.gt_inds == 0, as_tuple=False) + if neg_inds.numel().numpy()[0] != 0: + neg_inds = neg_inds.squeeze() + if (neg_inds.numel().numpy()[0]) <= num_expected.numpy()[0]: + return neg_inds + else: + return self.random_choice(neg_inds, num_expected) diff --git a/paddlevideo/modeling/weight_init.py b/paddlevideo/modeling/weight_init.py new file mode 100644 index 0000000000000000000000000000000000000000..4722895265b3bfa1962e959de9dfdbe3ced6d1fc --- /dev/null +++ b/paddlevideo/modeling/weight_init.py @@ -0,0 +1,157 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import paddle +import paddle.nn.initializer as init +import numpy as np +from scipy import special + + +def weight_init_(layer, + func, + weight_name=None, + bias_name=None, + bias_value=0.0, + **kwargs): + """ + In-place params init function. + Usage: + .. code-block:: python + + import paddle + import numpy as np + + data = np.ones([3, 4], dtype='float32') + linear = paddle.nn.Linear(4, 4) + input = paddle.to_tensor(data) + print(linear.weight) + linear(input) + + weight_init_(linear, 'Normal', 'fc_w0', 'fc_b0', std=0.01, mean=0.1) + print(linear.weight) + """ + + if hasattr(layer, 'weight') and layer.weight is not None: + getattr(init, func)(**kwargs)(layer.weight) + if weight_name is not None: + # override weight name + layer.weight.name = weight_name + + if hasattr(layer, 'bias') and layer.bias is not None: + init.Constant(bias_value)(layer.bias) + if bias_name is not None: + # override bias name + layer.bias.name = bias_name + + +def _no_grad_trunc_normal_(tensor, mean, std, a, b): + def norm_cdf(x): + # Computes standard normal cumulative distribution function + return (1. + math.erf(x / math.sqrt(2.))) / 2. + + if (mean < a - 2 * std) or (mean > b + 2 * std): + print("mean is more than 2 std from [a, b] in nn.init.trunc_normal_. " + "The distribution of values may be incorrect.") + + with paddle.no_grad(): + # Values are generated by using a truncated uniform distribution and + # then using the inverse CDF for the normal distribution. + # Get upper and lower cdf values + l = norm_cdf((a - mean) / std) + u = norm_cdf((b - mean) / std) + + # Uniformly fill tensor with values from [l, u], then translate to [2l-1, 2u-1]. + tmp = np.random.uniform(2 * l - 1, 2 * u - 1, + size=list(tensor.shape)).astype(np.float32) + + # Use inverse cdf transform for normal distribution to get truncated + # standard normal + tmp = special.erfinv(tmp) + + # Transform to proper mean, std + tmp *= (std * math.sqrt(2.0)) + tmp += mean + + # Clamp to ensure it's in the proper range + tmp = np.clip(tmp, a, b) + tensor.set_value(paddle.to_tensor(tmp)) + + return tensor + + +def _calculate_fan_in_and_fan_out(tensor): + dimensions = tensor.dim() + if dimensions < 2: + raise ValueError( + "Fan in and fan out can not be computed for tensor with fewer than 2 dimensions" + ) + + num_input_fmaps = tensor.shape[1] + num_output_fmaps = tensor.shape[0] + receptive_field_size = 1 + if tensor.dim() > 2: + receptive_field_size = tensor[0][0].numel() + fan_in = num_input_fmaps * receptive_field_size + fan_out = num_output_fmaps * receptive_field_size + + return fan_in, fan_out + + +def trunc_normal_(tensor, mean=0., std=1., a=-2., b=2.): + return _no_grad_trunc_normal_(tensor, mean, std, a, b) + + +def kaiming_normal_(tensor, a=0., mode='fan_in', nonlinearity='leaky_relu'): + def _calculate_correct_fan(tensor, mode): + mode = mode.lower() + valid_modes = ['fan_in', 'fan_out'] + if mode not in valid_modes: + raise ValueError( + "Mode {} not supported, please use one of {}".format( + mode, valid_modes)) + + fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor) + return fan_in if mode == 'fan_in' else fan_out + + def calculate_gain(nonlinearity, param=None): + linear_fns = [ + 'linear', 'conv1d', 'conv2d', 'conv3d', 'conv_transpose1d', + 'conv_transpose2d', 'conv_transpose3d' + ] + if nonlinearity in linear_fns or nonlinearity == 'sigmoid': + return 1 + elif nonlinearity == 'tanh': + return 5.0 / 3 + elif nonlinearity == 'relu': + return math.sqrt(2.0) + elif nonlinearity == 'leaky_relu': + if param is None: + negative_slope = 0.01 + elif not isinstance(param, bool) and isinstance( + param, int) or isinstance(param, float): + negative_slope = param + else: + raise ValueError( + "negative_slope {} not a valid number".format(param)) + return math.sqrt(2.0 / (1 + negative_slope**2)) + else: + raise ValueError("Unsupported nonlinearity {}".format(nonlinearity)) + + fan = _calculate_correct_fan(tensor, mode) + gain = calculate_gain(nonlinearity, a) + std = gain / math.sqrt(fan) + with paddle.no_grad(): + paddle.nn.initializer.Normal(0, std)(tensor) + return tensor diff --git a/paddlevideo/solver/__init__.py b/paddlevideo/solver/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..01cf9cdd763493911412d8943bf19066dd52e88b --- /dev/null +++ b/paddlevideo/solver/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .optimizer import build_optimizer +from .lr import build_lr diff --git a/paddlevideo/solver/custom_lr.py b/paddlevideo/solver/custom_lr.py new file mode 100644 index 0000000000000000000000000000000000000000..bbf8d742af7ac9387ddccca70efca68e3a4f7f57 --- /dev/null +++ b/paddlevideo/solver/custom_lr.py @@ -0,0 +1,338 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from paddle.optimizer.lr import * +import numpy as np +""" +PaddleVideo Learning Rate Schedule: +You can use paddle.optimizer.lr +or define your custom_lr in this file. +""" + + +class CustomWarmupCosineDecay(LRScheduler): + r""" + We combine warmup and stepwise-cosine which is used in slowfast model. + + Args: + warmup_start_lr (float): start learning rate used in warmup stage. + warmup_epochs (int): the number epochs of warmup. + cosine_base_lr (float|int, optional): base learning rate in cosine schedule. + max_epoch (int): total training epochs. + num_iters(int): number iterations of each epoch. + last_epoch (int, optional): The index of last epoch. Can be set to restart training. Default: -1, means initial learning rate. + verbose (bool, optional): If ``True``, prints a message to stdout for each update. Default: ``False`` . + Returns: + ``CosineAnnealingDecay`` instance to schedule learning rate. + """ + + def __init__(self, + warmup_start_lr, + warmup_epochs, + cosine_base_lr, + max_epoch, + num_iters, + last_epoch=-1, + verbose=False): + self.warmup_start_lr = warmup_start_lr + self.warmup_epochs = warmup_epochs + self.cosine_base_lr = cosine_base_lr + self.max_epoch = max_epoch + self.num_iters = num_iters + #call step() in base class, last_lr/last_epoch/base_lr will be update + super(CustomWarmupCosineDecay, self).__init__(last_epoch=last_epoch, + verbose=verbose) + + def step(self, epoch=None): + """ + ``step`` should be called after ``optimizer.step`` . It will update the learning rate in optimizer according to current ``epoch`` . + The new learning rate will take effect on next ``optimizer.step`` . + Args: + epoch (int, None): specify current epoch. Default: None. Auto-increment from last_epoch=-1. + Returns: + None + """ + if epoch is None: + if self.last_epoch == -1: + self.last_epoch += 1 + else: + self.last_epoch += 1 / self.num_iters # update step with iters + else: + self.last_epoch = epoch + self.last_lr = self.get_lr() + + if self.verbose: + print('Epoch {}: {} set learning rate to {}.'.format( + self.last_epoch, self.__class__.__name__, self.last_lr)) + + def _lr_func_cosine(self, cur_epoch, cosine_base_lr, max_epoch): + return cosine_base_lr * (math.cos(math.pi * cur_epoch / max_epoch) + + 1.0) * 0.5 + + def get_lr(self): + """Define lr policy""" + lr = self._lr_func_cosine(self.last_epoch, self.cosine_base_lr, + self.max_epoch) + lr_end = self._lr_func_cosine(self.warmup_epochs, self.cosine_base_lr, + self.max_epoch) + + # Perform warm up. + if self.last_epoch < self.warmup_epochs: + lr_start = self.warmup_start_lr + alpha = (lr_end - lr_start) / self.warmup_epochs + lr = self.last_epoch * alpha + lr_start + return lr + + +class CustomWarmupPiecewiseDecay(LRScheduler): + r""" + This op combine warmup and stepwise-cosine which is used in slowfast model. + + Args: + warmup_start_lr (float): start learning rate used in warmup stage. + warmup_epochs (int): the number epochs of warmup. + step_base_lr (float|int, optional): base learning rate in step schedule. + max_epoch (int): total training epochs. + num_iters(int): number iterations of each epoch. + last_epoch (int, optional): The index of last epoch. Can be set to restart training. Default: -1, means initial learning rate. + verbose (bool, optional): If ``True``, prints a message to stdout for each update. Default: ``False`` . + Returns: + ``CustomWarmupPiecewiseDecay`` instance to schedule learning rate. + """ + + def __init__(self, + warmup_start_lr, + warmup_epochs, + step_base_lr, + lrs, + gamma, + steps, + max_epoch, + num_iters, + last_epoch=0, + verbose=False): + self.warmup_start_lr = warmup_start_lr + self.warmup_epochs = warmup_epochs + self.step_base_lr = step_base_lr + self.lrs = lrs + self.gamma = gamma + self.steps = steps + self.max_epoch = max_epoch + self.num_iters = num_iters + self.last_epoch = last_epoch + self.last_lr = self.warmup_start_lr # used in first iter + self.verbose = verbose + self._var_name = None + + def step(self, epoch=None, rebuild=False): + """ + ``step`` should be called after ``optimizer.step`` . It will update the learning rate in optimizer according to current ``epoch`` . + The new learning rate will take effect on next ``optimizer.step`` . + Args: + epoch (int, None): specify current epoch. Default: None. Auto-increment from last_epoch=-1. + Returns: + None + """ + if epoch is None: + if not rebuild: + self.last_epoch += 1 / self.num_iters # update step with iters + else: + self.last_epoch = epoch + self.last_lr = self.get_lr() + + if self.verbose: + print( + 'step Epoch {}: {} set learning rate to {}.self.num_iters={}, 1/self.num_iters={}' + .format(self.last_epoch, self.__class__.__name__, self.last_lr, + self.num_iters, 1 / self.num_iters)) + + def _lr_func_steps_with_relative_lrs(self, cur_epoch, lrs, base_lr, steps, + max_epoch): + # get step index + steps = steps + [max_epoch] + for ind, step in enumerate(steps): + if cur_epoch < step: + break + if self.verbose: + print( + '_lr_func_steps_with_relative_lrs, cur_epoch {}: {}, steps {}, ind {}, step{}, max_epoch{}' + .format(cur_epoch, self.__class__.__name__, steps, ind, step, + max_epoch)) + + return lrs[ind - 1] * base_lr + + def get_lr(self): + """Define lr policy""" + lr = self._lr_func_steps_with_relative_lrs( + self.last_epoch, + self.lrs, + self.step_base_lr, + self.steps, + self.max_epoch, + ) + lr_end = self._lr_func_steps_with_relative_lrs( + self.warmup_epochs, + self.lrs, + self.step_base_lr, + self.steps, + self.max_epoch, + ) + + # Perform warm up. + if self.last_epoch < self.warmup_epochs: + lr_start = self.warmup_start_lr + alpha = (lr_end - lr_start) / self.warmup_epochs + lr = self.last_epoch * alpha + lr_start + if self.verbose: + print( + 'get_lr, Epoch {}: {}, lr {}, lr_end {}, self.lrs{}, self.step_base_lr{}, self.steps{}, self.max_epoch{}' + .format(self.last_epoch, self.__class__.__name__, lr, lr_end, + self.lrs, self.step_base_lr, self.steps, + self.max_epoch)) + + return lr + + +class CustomPiecewiseDecay(PiecewiseDecay): + + def __init__(self, **kargs): + kargs.pop('num_iters') + super().__init__(**kargs) + + +class CustomWarmupCosineStepDecay(LRScheduler): + + def __init__(self, + warmup_iters, + warmup_ratio=0.1, + min_lr=0, + base_lr=3e-5, + max_epoch=30, + last_epoch=-1, + num_iters=None, + verbose=False): + + self.warmup_ratio = warmup_ratio + self.min_lr = min_lr + self.warmup_epochs = warmup_iters + self.warmup_iters = warmup_iters * num_iters + self.cnt_iters = 0 + self.cnt_epoch = 0 + self.num_iters = num_iters + self.tot_iters = max_epoch * num_iters + self.max_epoch = max_epoch + self.cosine_base_lr = base_lr # initial lr for all param groups + self.regular_lr = self.get_regular_lr() + super().__init__(last_epoch=last_epoch, verbose=verbose) + + def annealing_cos(self, start, end, factor, weight=1): + cos_out = math.cos(math.pi * factor) + 1 + return end + 0.5 * weight * (start - end) * cos_out + + def get_regular_lr(self): + progress = self.cnt_epoch + max_progress = self.max_epoch + target_lr = self.min_lr + return self.annealing_cos(self.cosine_base_lr, target_lr, progress / + max_progress) # self.cosine_base_lr + + def get_warmup_lr(self, cur_iters): + k = (1 - cur_iters / self.warmup_iters) * (1 - self.warmup_ratio) + warmup_lr = self.regular_lr * (1 - k) # 3e-5 * (1-k) + return warmup_lr + + def step(self, epoch=None): + self.regular_lr = self.get_regular_lr() + self.last_lr = self.get_lr() + self.cnt_epoch = (self.cnt_iters + + 1) // self.num_iters # update step with iters + self.cnt_iters += 1 + + if self.verbose: + print('Epoch {}: {} set learning rate to {}.'.format( + self.last_epoch, self.__class__.__name__, self.last_lr)) + + def get_lr(self): + """Define lr policy""" + cur_iter = self.cnt_iters + if cur_iter >= self.warmup_iters: + return self.regular_lr + else: + warmup_lr = self.get_warmup_lr(cur_iter) + return warmup_lr + + +class CustomWarmupAdjustDecay(LRScheduler): + r""" + We combine warmup and stepwise-cosine which is used in slowfast model. + + Args: + step_base_lr (float): start learning rate used in warmup stage. + warmup_epochs (int): the number epochs of warmup. + lr_decay_rate (float|int, optional): base learning rate decay rate. + step (int): step in change learning rate. + last_epoch (int, optional): The index of last epoch. Can be set to restart training. Default: -1, means initial learning rate. + verbose (bool, optional): If ``True``, prints a message to stdout for each update. Default: ``False`` . + Returns: + ``CosineAnnealingDecay`` instance to schedule learning rate. + """ + + def __init__(self, + step_base_lr, + warmup_epochs, + lr_decay_rate, + boundaries, + num_iters=None, + last_epoch=-1, + verbose=False): + self.step_base_lr = step_base_lr + self.warmup_epochs = warmup_epochs + self.lr_decay_rate = lr_decay_rate + self.boundaries = boundaries + self.num_iters = num_iters + #call step() in base class, last_lr/last_epoch/base_lr will be update + super(CustomWarmupAdjustDecay, self).__init__(last_epoch=last_epoch, + verbose=verbose) + + def step(self, epoch=None): + """ + ``step`` should be called after ``optimizer.step`` . It will update the learning rate in optimizer according to current ``epoch`` . + The new learning rate will take effect on next ``optimizer.step`` . + Args: + epoch (int, None): specify current epoch. Default: None. Auto-increment from last_epoch=-1. + Returns: + None + """ + if epoch is None: + if self.last_epoch == -1: + self.last_epoch += 1 + else: + self.last_epoch += 1 / self.num_iters # update step with iters + else: + self.last_epoch = epoch + + self.last_lr = self.get_lr() + + if self.verbose: + print('Epoch {}: {} set learning rate to {}.'.format( + self.last_epoch, self.__class__.__name__, self.last_lr)) + + def get_lr(self): + if self.last_epoch < self.warmup_epochs: + lr = self.step_base_lr * (self.last_epoch + 1) / self.warmup_epochs + else: + lr = self.step_base_lr * (self.lr_decay_rate**np.sum( + self.last_epoch >= np.array(self.boundaries))) + return lr diff --git a/paddlevideo/solver/lr.py b/paddlevideo/solver/lr.py new file mode 100644 index 0000000000000000000000000000000000000000..3a56fad16e9ea33395e98998629b6fae958f7353 --- /dev/null +++ b/paddlevideo/solver/lr.py @@ -0,0 +1,52 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict + +from paddle.optimizer.lr import LRScheduler + +from . import custom_lr + + +def build_lr(cfg: Dict, num_iters: int) -> LRScheduler: + """Build a learning rate scheduler accroding to ```OPTIMIZER``` configuration, and it always pass into the optimizer. + In configuration: + learning_rate: + name: 'PiecewiseDecay' + boundaries: [20, 60] + values: [0.00025, 0.000025, 0.0000025] + + Args: + cfg (Dict): learning rate configuration. + num_iters (int): The number of iterations that may be used when calculating the learning rate + + Returns: + LRScheduler: learning rate scheduler. + """ + + cfg_copy = cfg.copy() + + #when learning_rate is LRScheduler + if cfg_copy.get('learning_rate') and isinstance(cfg_copy['learning_rate'], + dict): + cfg_copy['learning_rate'] = build_lr( + cfg_copy['learning_rate'], + num_iters) #not support only inner iter_step + + lr_name = cfg_copy.pop('name') + if cfg_copy.get('iter_step'): + cfg_copy['num_iters'] = num_iters + cfg_copy.pop('iter_step') + + return getattr(custom_lr, lr_name)(**cfg_copy) diff --git a/paddlevideo/solver/optimizer.py b/paddlevideo/solver/optimizer.py new file mode 100644 index 0000000000000000000000000000000000000000..46ff916c46b7dae925d67d97b792b7f5fd8aab46 --- /dev/null +++ b/paddlevideo/solver/optimizer.py @@ -0,0 +1,132 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +from typing import Dict + +import paddle +from paddle.optimizer.lr import LRScheduler +from paddle.regularizer import L1Decay, L2Decay +from paddlevideo.utils import get_logger + + +def build_optimizer(cfg: Dict, + lr_scheduler: LRScheduler, + model: paddle.nn.Layer, + use_amp: bool = False, + amp_level: str = None) -> paddle.optimizer.Optimizer: + """Build an optimizer and learning rate scheduler to optimize parameters accroding to ```OPTIMIZER``` field in configuration. + + In configuration: + OPTIMIZER: + name: Momentum + momentum: 0.9 + weight_decay: 0.001 + or + + OPTIMIZER: + name: Momentum + momentum: 0.9 + weight_decay: + name: "L1" + value: 0.001 + + Momentum optimizer will be applied to optimize network and L1Decay regularizer will be applied to avoid overfit. + + OPTIMIZER: + name: Adam + weight_decay: + name: "L2" + value: 0.001 + + Adam optimizer will be applied to optimize network and L2Decay regularizer will applied to avoid overfit. + + Refer to ```https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/regularizer/L2Decay_en.html``` for more details. + + Args: + cfg (Dict): optimizer configuration. + lr_scheduler (LRScheduler): learning rate scheduler. + model (paddle.nn.Layer, optional): model which contains parameters to be optimized. Defaults to None. + use_amp (bool, optional): Whether use amp. Defaults to False. + amp_level (str, optional): amp level when amp is enabled. Defaults to None. + + + Returns: + paddle.optimizer.Optimizer: an optimizer for the input model. + """ + logger = get_logger("paddlevideo") + cfg_copy = cfg.copy() + # NOTE: check none and illegal cfg!!! + opt_name = cfg_copy.pop('name') + # deal with weight decay + if cfg_copy.get('weight_decay'): + if isinstance(cfg_copy.get('weight_decay'), + float): # just an float factor + cfg_copy['weight_decay'] = cfg_copy.get('weight_decay') + elif 'L1' in cfg_copy.get('weight_decay').get( + 'name').upper(): # specify L2 wd and it's float factor + cfg_copy['weight_decay'] = L1Decay( + cfg_copy.get('weight_decay').get('value')) + elif 'L2' in cfg_copy.get('weight_decay').get( + 'name').upper(): # specify L1 wd and it's float factor + cfg_copy['weight_decay'] = L2Decay( + cfg_copy.get('weight_decay').get('value')) + else: + raise ValueError + + # deal with grad clip + if cfg_copy.get('grad_clip'): + if isinstance(cfg_copy.get('grad_clip'), float): + cfg_copy['grad_clip'] = cfg_copy.get('grad_clip').get('value') + elif 'global' in cfg_copy.get('grad_clip').get('name').lower(): + cfg_copy['grad_clip'] = paddle.nn.ClipGradByGlobalNorm( + cfg_copy.get('grad_clip').get('value')) + else: + raise ValueError + + # Set for optimizers that cannot be applied to l2decay, i.e. AdamW + if cfg_copy.get('no_weight_decay_name'): + no_weight_decay_name = cfg_copy.pop('no_weight_decay_name') + no_weight_decay_name_list = no_weight_decay_name.split(' ') + + # NOTE: use param.name not name + no_weight_decay_param_list = [ + param.name for name, param in model.named_parameters() + if any(key_word in name for key_word in no_weight_decay_name_list) + ] # get the full param name of no weight decay + + _apply_decay_param_fun = lambda name: name not in no_weight_decay_param_list + cfg_copy['apply_decay_param_fun'] = _apply_decay_param_fun + logger.info( + f"No weight Decay list :({len(no_weight_decay_param_list)})", + no_weight_decay_param_list) + + cfg_copy.pop('learning_rate') + + # set multi_precision + optimizer_setting = { + 'learning_rate': lr_scheduler, + 'parameters': model.parameters(), + **cfg_copy + } + optimizer_init_args = inspect.getargspec( + getattr(paddle.optimizer, opt_name).__init__).args + if use_amp and amp_level == "O2" and "multi_precision" in optimizer_init_args: + # support "multi_precision" arg in optimizer's __init__ function. + optimizer_setting.update({"multi_precision": True}) + logger.info( + "Set multi_precision=True for optimizer when use_amp=True and amp_level='O2'" + ) + + return getattr(paddle.optimizer, opt_name)(**optimizer_setting) diff --git a/paddlevideo/tasks/__init__.py b/paddlevideo/tasks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4d43f0955d20ed75c1b3438503b7b56a1819b034 --- /dev/null +++ b/paddlevideo/tasks/__init__.py @@ -0,0 +1,20 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .train import train_model +from .test import test_model +from .train_dali import train_dali +from .train_multigrid import train_model_multigrid + +__all__ = ['train_model', 'test_model', 'train_dali', 'train_model_multigrid'] diff --git a/paddlevideo/tasks/test.py b/paddlevideo/tasks/test.py new file mode 100644 index 0000000000000000000000000000000000000000..5b4d89cf00f9aafd8cc46cd7d8c330907aa8223f --- /dev/null +++ b/paddlevideo/tasks/test.py @@ -0,0 +1,87 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +from paddlevideo.utils import get_logger, load + +from ..loader.builder import build_dataloader, build_dataset +from ..metrics import build_metric +from ..modeling.builder import build_model + +logger = get_logger("paddlevideo") + + +@paddle.no_grad() +def test_model(cfg, weights, parallel=True): + """Test model entry + + Args: + cfg (dict): configuration. + weights (str): weights path to load. + parallel (bool): Whether to do multi-cards testing. Default: True. + + """ + # 1. Construct model. + if cfg.MODEL.get('backbone') and cfg.MODEL.backbone.get('pretrained'): + cfg.MODEL.backbone.pretrained = '' # disable pretrain model init + model = build_model(cfg.MODEL) + + if parallel: + model = paddle.DataParallel(model) + + # 2. Construct dataset and dataloader. + cfg.DATASET.test.test_mode = True + dataset = build_dataset((cfg.DATASET.test, cfg.PIPELINE.test)) + batch_size = cfg.DATASET.get("test_batch_size", 8) + + if cfg.get('use_npu'): + places = paddle.set_device('npu') + else: + places = paddle.set_device('gpu') + + # default num worker: 0, which means no subprocess will be created + num_workers = cfg.DATASET.get('num_workers', 0) + num_workers = cfg.DATASET.get('test_num_workers', num_workers) + dataloader_setting = dict(batch_size=batch_size, + num_workers=num_workers, + places=places, + drop_last=False, + shuffle=False) + + data_loader = build_dataloader( + dataset, **dataloader_setting) if cfg.model_name not in ['CFBI' + ] else dataset + + model.eval() + + state_dicts = load(weights) + model.set_state_dict(state_dicts) + + # add params to metrics + cfg.METRIC.data_size = len(dataset) + cfg.METRIC.batch_size = batch_size + Metric = build_metric(cfg.METRIC) + + if cfg.MODEL.framework == "FastRCNN": + Metric.set_dataset_info(dataset.info, len(dataset)) + + for batch_id, data in enumerate(data_loader): + if cfg.model_name in [ + 'CFBI' + ]: #for VOS task, dataset for video and dataloader for frames in each video + Metric.update(batch_id, data, model) + else: + outputs = model(data, mode='test') + Metric.update(batch_id, data, outputs) + Metric.accumulate() diff --git a/paddlevideo/tasks/train.py b/paddlevideo/tasks/train.py new file mode 100644 index 0000000000000000000000000000000000000000..90da6efc4ee7ca0b4012406ada6729eb6bbb503f --- /dev/null +++ b/paddlevideo/tasks/train.py @@ -0,0 +1,398 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path as osp +import time + +import paddle +import paddle.amp as amp +import paddle.distributed as dist +import paddle.distributed.fleet as fleet +from paddlevideo.utils import (add_profiler_step, build_record, get_logger, + load, log_batch, log_epoch, mkdir, save) + +from ..loader.builder import build_dataloader, build_dataset +from ..metrics.ava_utils import collect_results_cpu +from ..modeling.builder import build_model +from ..solver import build_lr, build_optimizer +from ..utils import do_preciseBN + + +def train_model(cfg, + weights=None, + parallel=True, + validate=True, + use_amp=False, + amp_level=None, + max_iters=None, + use_fleet=False, + profiler_options=None): + """Train model entry + + Args: + cfg (dict): configuration. + weights (str, optional): weights path for finetuning. Defaults to None. + parallel (bool, optional): whether multi-cards training. Defaults to True. + validate (bool, optional): whether to do evaluation. Defaults to True. + use_amp (bool, optional): whether to use automatic mixed precision during training. Defaults to False. + amp_level (str, optional): amp optmization level, must be 'O1' or 'O2' when use_amp is True. Defaults to None. + max_iters (int, optional): max running iters in an epoch. Defaults to None. + use_fleet (bool, optional): whether to use fleet. Defaults to False. + profiler_options (str, optional): configuration for the profiler function. Defaults to None. + + """ + if use_fleet: + fleet.init(is_collective=True) + + logger = get_logger("paddlevideo") + batch_size = cfg.DATASET.get('batch_size', 8) + valid_batch_size = cfg.DATASET.get('valid_batch_size', batch_size) + + # gradient accumulation settings + use_gradient_accumulation = cfg.get('GRADIENT_ACCUMULATION', None) + if use_gradient_accumulation and dist.get_world_size() >= 1: + global_batch_size = cfg.GRADIENT_ACCUMULATION.get( + 'global_batch_size', None) + num_gpus = dist.get_world_size() + + assert isinstance( + global_batch_size, int + ), f"global_batch_size must be int, but got {type(global_batch_size)}" + assert batch_size <= global_batch_size, \ + f"global_batch_size({global_batch_size}) must not be less than batch_size({batch_size})" + + cur_global_batch_size = batch_size * num_gpus # The number of batches calculated by all GPUs at one time + assert global_batch_size % cur_global_batch_size == 0, \ + f"The global batchsize({global_batch_size}) must be divisible by cur_global_batch_size({cur_global_batch_size})" + cfg.GRADIENT_ACCUMULATION[ + "num_iters"] = global_batch_size // cur_global_batch_size + # The number of iterations required to reach the global batchsize + logger.info( + f"Using gradient accumulation training strategy, " + f"global_batch_size={global_batch_size}, " + f"num_gpus={num_gpus}, " + f"num_accumulative_iters={cfg.GRADIENT_ACCUMULATION.num_iters}") + + if cfg.get('use_npu'): + places = paddle.set_device('npu') + else: + places = paddle.set_device('gpu') + + # default num worker: 0, which means no subprocess will be created + num_workers = cfg.DATASET.get('num_workers', 0) + valid_num_workers = cfg.DATASET.get('valid_num_workers', num_workers) + model_name = cfg.model_name + output_dir = cfg.get("output_dir", f"./output/{model_name}") + mkdir(output_dir) + + # 1. Construct model + model = build_model(cfg.MODEL) + + # 2. Construct dataset and dataloader for training and evaluation + train_dataset = build_dataset((cfg.DATASET.train, cfg.PIPELINE.train)) + train_dataloader_setting = dict(batch_size=batch_size, + num_workers=num_workers, + collate_fn_cfg=cfg.get('MIX', None), + places=places) + train_loader = build_dataloader(train_dataset, **train_dataloader_setting) + + if validate: + valid_dataset = build_dataset((cfg.DATASET.valid, cfg.PIPELINE.valid)) + validate_dataloader_setting = dict( + batch_size=valid_batch_size, + num_workers=valid_num_workers, + places=places, + drop_last=False, + shuffle=cfg.DATASET.get( + 'shuffle_valid', + False) # NOTE: attention_LSTM needs to shuffle valid data. + ) + valid_loader = build_dataloader(valid_dataset, + **validate_dataloader_setting) + + # 3. Construct learning rate scheduler(lr) and optimizer + lr = build_lr(cfg.OPTIMIZER.learning_rate, len(train_loader)) + optimizer = build_optimizer(cfg.OPTIMIZER, + lr, + model=model, + use_amp=use_amp, + amp_level=amp_level) + + # 4. Construct scalar and convert parameters for amp(optional) + if use_amp: + scaler = amp.GradScaler(init_loss_scaling=2.0**16, + incr_every_n_steps=2000, + decr_every_n_nan_or_inf=1) + # convert model parameters to fp16 when amp_level is O2(pure fp16) + model, optimizer = amp.decorate(models=model, + optimizers=optimizer, + level=amp_level, + save_dtype='float32') + # NOTE: save_dtype is set to float32 now. + logger.info(f"Training in amp mode, amp_level={amp_level}.") + else: + assert amp_level is None, f"amp_level must be None when training in fp32 mode, but got {amp_level}." + logger.info("Training in fp32 mode.") + + # 5. Resume(optional) + resume_epoch = cfg.get("resume_epoch", 0) + if resume_epoch: + filename = osp.join(output_dir, + model_name + f"_epoch_{resume_epoch:05d}") + resume_model_dict = load(filename + '.pdparams') + resume_opt_dict = load(filename + '.pdopt') + model.set_state_dict(resume_model_dict) + optimizer.set_state_dict(resume_opt_dict) + logger.info("Resume from checkpoint: {}".format(filename)) + + # 6. Finetune(optional) + if weights: + assert resume_epoch == 0, f"Conflict occurs when finetuning, please switch resume function off by setting resume_epoch to 0 or not indicating it." + model_dict = load(weights) + model.set_state_dict(model_dict) + logger.info("Finetune from checkpoint: {}".format(weights)) + + # 7. Parallelize(optional) + if parallel: + model = paddle.DataParallel(model) + + if use_fleet: + model = fleet.distributed_model(model) + optimizer = fleet.distributed_optimizer(optimizer) + + # 8. Train Model + best = 0.0 + for epoch in range(0, cfg.epochs): + if epoch < resume_epoch: + logger.info( + f"| epoch: [{epoch + 1}] <= resume_epoch: [{resume_epoch}], continue..." + ) + continue + model.train() + + record_list = build_record(cfg.MODEL) + tic = time.time() + for i, data in enumerate(train_loader): + """Next two line of code only used in test_tipc, + ignore it most of the time""" + if max_iters is not None and i >= max_iters: + break + + record_list['reader_time'].update(time.time() - tic) + + # Collect performance information when profiler_options is activate + add_profiler_step(profiler_options) + + # 8.1 forward + # AMP # + if use_amp: + with amp.auto_cast(custom_black_list={"reduce_mean"}, + level=amp_level): + outputs = model(data, mode='train') + avg_loss = outputs['loss'] + if use_gradient_accumulation: + # clear grad at when epoch begins + if i == 0: + optimizer.clear_grad() + # Loss normalization + avg_loss /= cfg.GRADIENT_ACCUMULATION.num_iters + # Loss scaling + scaled = scaler.scale(avg_loss) + # 8.2 backward + scaled.backward() + # 8.3 minimize + if (i + 1) % cfg.GRADIENT_ACCUMULATION.num_iters == 0: + scaler.minimize(optimizer, scaled) + optimizer.clear_grad() + else: # general case + # Loss scaling + scaled = scaler.scale(avg_loss) + # 8.2 backward + scaled.backward() + # 8.3 minimize + scaler.minimize(optimizer, scaled) + optimizer.clear_grad() + else: + outputs = model(data, mode='train') + avg_loss = outputs['loss'] + if use_gradient_accumulation: + # clear grad at when epoch begins + if i == 0: + optimizer.clear_grad() + # Loss normalization + avg_loss /= cfg.GRADIENT_ACCUMULATION.num_iters + # 8.2 backward + avg_loss.backward() + # 8.3 minimize + if (i + 1) % cfg.GRADIENT_ACCUMULATION.num_iters == 0: + optimizer.step() + optimizer.clear_grad() + else: # general case + # 8.2 backward + avg_loss.backward() + # 8.3 minimize + optimizer.step() + optimizer.clear_grad() + + # log record + record_list['lr'].update(optimizer.get_lr(), batch_size) + for name, value in outputs.items(): + if name in record_list: + record_list[name].update(value, batch_size) + + record_list['batch_time'].update(time.time() - tic) + tic = time.time() + + if i % cfg.get("log_interval", 10) == 0: + ips = "ips: {:.5f} instance/sec.".format( + batch_size / record_list["batch_time"].val) + log_batch(record_list, i, epoch + 1, cfg.epochs, "train", ips) + + # learning rate iter step + if cfg.OPTIMIZER.learning_rate.get("iter_step"): + lr.step() + + # learning rate epoch step + if not cfg.OPTIMIZER.learning_rate.get("iter_step"): + lr.step() + + ips = "avg_ips: {:.5f} instance/sec.".format( + batch_size * record_list["batch_time"].count / + record_list["batch_time"].sum) + log_epoch(record_list, epoch + 1, "train", ips) + + def evaluate(best): + model.eval() + results = [] + record_list = build_record(cfg.MODEL) + record_list.pop('lr') + tic = time.time() + if parallel: + rank = dist.get_rank() + # single_gpu_test and multi_gpu_test + for i, data in enumerate(valid_loader): + """Next two line of code only used in test_tipc, + ignore it most of the time""" + if max_iters is not None and i >= max_iters: + break + + if use_amp: + with amp.auto_cast(custom_black_list={"reduce_mean"}, + level=amp_level): + outputs = model(data, mode='valid') + else: + outputs = model(data, mode='valid') + + if cfg.MODEL.framework == "FastRCNN": + results.extend(outputs) + + # log_record + if cfg.MODEL.framework != "FastRCNN": + for name, value in outputs.items(): + if name in record_list: + record_list[name].update(value, batch_size) + + record_list['batch_time'].update(time.time() - tic) + tic = time.time() + + if i % cfg.get("log_interval", 10) == 0: + ips = "ips: {:.5f} instance/sec.".format( + valid_batch_size / record_list["batch_time"].val) + log_batch(record_list, i, epoch + 1, cfg.epochs, "val", ips) + + if cfg.MODEL.framework == "FastRCNN": + if parallel: + results = collect_results_cpu(results, len(valid_dataset)) + if not parallel or (parallel and rank == 0): + eval_res = valid_dataset.evaluate(results) + for name, value in eval_res.items(): + record_list[name].update(value, valid_batch_size) + + ips = "avg_ips: {:.5f} instance/sec.".format( + valid_batch_size * record_list["batch_time"].count / + record_list["batch_time"].sum) + log_epoch(record_list, epoch + 1, "val", ips) + + best_flag = False + if cfg.MODEL.framework == "FastRCNN" and (not parallel or + (parallel and rank == 0)): + if record_list["mAP@0.5IOU"].val > best: + best = record_list["mAP@0.5IOU"].val + best_flag = True + return best, best_flag + + # forbest2, cfg.MODEL.framework != "FastRCNN": + for top_flag in ['hit_at_one', 'top1', 'rmse', "F1@0.50"]: + if record_list.get(top_flag): + if top_flag != 'rmse' and record_list[top_flag].avg > best: + best = record_list[top_flag].avg + best_flag = True + elif top_flag == 'rmse' and ( + best == 0.0 or record_list[top_flag].avg < best): + best = record_list[top_flag].avg + best_flag = True + + return best, best_flag + + # use precise bn to improve acc + if cfg.get("PRECISEBN") and (epoch % cfg.PRECISEBN.preciseBN_interval + == 0 or epoch == cfg.epochs - 1): + do_preciseBN( + model, train_loader, parallel, + min(cfg.PRECISEBN.num_iters_preciseBN, len(train_loader)), + use_amp, amp_level) + + # 9. Validation + if validate and (epoch % cfg.get("val_interval", 1) == 0 + or epoch == cfg.epochs - 1): + with paddle.no_grad(): + best, save_best_flag = evaluate(best) + # save best + if save_best_flag: + save(optimizer.state_dict(), + osp.join(output_dir, model_name + "_best.pdopt")) + save(model.state_dict(), + osp.join(output_dir, model_name + "_best.pdparams")) + if model_name == "AttentionLstm": + logger.info( + f"Already save the best model (hit_at_one){best}") + elif cfg.MODEL.framework == "FastRCNN": + logger.info( + f"Already save the best model (mAP@0.5IOU){int(best * 10000) / 10000}" + ) + elif cfg.MODEL.framework == "DepthEstimator": + logger.info( + f"Already save the best model (rmse){int(best * 10000) / 10000}" + ) + elif cfg.MODEL.framework in ['MSTCN', 'ASRF']: + logger.info( + f"Already save the best model (F1@0.50){int(best * 10000) / 10000}" + ) + else: + logger.info( + f"Already save the best model (top1 acc){int(best * 10000) / 10000}" + ) + + # 10. Save model and optimizer + if epoch % cfg.get("save_interval", 1) == 0 or epoch == cfg.epochs - 1: + save( + optimizer.state_dict(), + osp.join(output_dir, + model_name + f"_epoch_{epoch + 1:05d}.pdopt")) + save( + model.state_dict(), + osp.join(output_dir, + model_name + f"_epoch_{epoch + 1:05d}.pdparams")) + + logger.info(f'training {model_name} finished') diff --git a/paddlevideo/tasks/train_dali.py b/paddlevideo/tasks/train_dali.py new file mode 100644 index 0000000000000000000000000000000000000000..8dd0a20f53fdb8035aad302461a03c3e5d34c01b --- /dev/null +++ b/paddlevideo/tasks/train_dali.py @@ -0,0 +1,143 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import os.path as osp + +import paddle +from ..modeling.builder import build_model +from ..solver import build_lr, build_optimizer +from ..utils import do_preciseBN +from paddlevideo.utils import get_logger, coloring +from paddlevideo.utils import (AverageMeter, build_record, log_batch, log_epoch, + save, load, mkdir) +from paddlevideo.loader import TSN_Dali_loader, get_input_data +""" +We only supported DALI training for TSN model now. +""" + + +def train_dali(cfg, weights=None, parallel=True): + """Train model entry + + Args: + cfg (dict): configuration. + weights (str): weights path for finetuning. + parallel (bool): Whether multi-cards training. Default: True. + + """ + + logger = get_logger("paddlevideo") + batch_size = cfg.DALI_LOADER.get('batch_size', 8) + places = paddle.set_device('gpu') + model_name = cfg.model_name + output_dir = cfg.get("output_dir", f"./output/{model_name}") + mkdir(output_dir) + + # 1. Construct model + model = build_model(cfg.MODEL) + if parallel: + model = paddle.DataParallel(model) + + # 2. Construct dali dataloader + train_loader = TSN_Dali_loader(cfg.DALI_LOADER).build_dali_reader() + + # 3. Construct solver. + lr = build_lr(cfg.OPTIMIZER.learning_rate, None) + optimizer = build_optimizer(cfg.OPTIMIZER, lr, model=model) + + # Resume + resume_epoch = cfg.get("resume_epoch", 0) + if resume_epoch: + filename = osp.join(output_dir, + model_name + f"_epoch_{resume_epoch:05d}") + resume_model_dict = load(filename + '.pdparams') + resume_opt_dict = load(filename + '.pdopt') + model.set_state_dict(resume_model_dict) + optimizer.set_state_dict(resume_opt_dict) + + # Finetune: + if weights: + assert resume_epoch == 0, f"Conflict occurs when finetuning, please switch resume function off by setting resume_epoch to 0 or not indicating it." + model_dict = load(weights) + model.set_state_dict(model_dict) + + # 4. Train Model + for epoch in range(0, cfg.epochs): + if epoch < resume_epoch: + logger.info( + f"| epoch: [{epoch+1}] <= resume_epoch: [{ resume_epoch}], continue... " + ) + continue + model.train() + record_list = build_record(cfg.MODEL) + tic = time.time() + for i, data in enumerate(train_loader): + data = get_input_data(data) + record_list['reader_time'].update(time.time() - tic) + # 4.1 forward + outputs = model(data, mode='train') + # 4.2 backward + avg_loss = outputs['loss'] + avg_loss.backward() + # 4.3 minimize + optimizer.step() + optimizer.clear_grad() + + # log record + record_list['lr'].update(optimizer._global_learning_rate(), + batch_size) + for name, value in outputs.items(): + record_list[name].update(value, batch_size) + + record_list['batch_time'].update(time.time() - tic) + tic = time.time() + + if i % cfg.get("log_interval", 10) == 0: + ips = "ips: {:.5f} instance/sec.".format( + batch_size / record_list["batch_time"].val) + log_batch(record_list, i, epoch + 1, cfg.epochs, "train", ips) + + # learning rate iter step + if cfg.OPTIMIZER.learning_rate.get("iter_step"): + lr.step() + + # learning rate epoch step + if not cfg.OPTIMIZER.learning_rate.get("iter_step"): + lr.step() + + ips = "ips: {:.5f} instance/sec.".format( + batch_size * record_list["batch_time"].count / + record_list["batch_time"].sum) + log_epoch(record_list, epoch + 1, "train", ips) + + # use precise bn to improve acc + if cfg.get("PRECISEBN") and (epoch % cfg.PRECISEBN.preciseBN_interval + == 0 or epoch == cfg.epochs - 1): + do_preciseBN( + model, train_loader, parallel, + min(cfg.PRECISEBN.num_iters_preciseBN, len(train_loader))) + + # 5. Save model and optimizer + if epoch % cfg.get("save_interval", 1) == 0 or epoch == cfg.epochs - 1: + save( + optimizer.state_dict(), + osp.join(output_dir, + model_name + f"_epoch_{epoch+1:05d}.pdopt")) + save( + model.state_dict(), + osp.join(output_dir, + model_name + f"_epoch_{epoch+1:05d}.pdparams")) + + logger.info(f'training {model_name} finished') diff --git a/paddlevideo/tasks/train_multigrid.py b/paddlevideo/tasks/train_multigrid.py new file mode 100644 index 0000000000000000000000000000000000000000..66e0065a71738e4aaf0c240aa75207077a882028 --- /dev/null +++ b/paddlevideo/tasks/train_multigrid.py @@ -0,0 +1,335 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import os.path as osp + +import paddle +import paddle.distributed as dist + +from ..loader.builder import build_dataloader, build_dataset +from ..modeling.builder import build_model +from ..solver import build_lr, build_optimizer +from ..utils import do_preciseBN +from paddlevideo.utils import get_logger, coloring +from paddlevideo.utils import (AverageMeter, build_record, log_batch, log_epoch, + save, load, mkdir) +from paddlevideo.utils.multigrid import MultigridSchedule, aggregate_sub_bn_stats, subn_load, subn_save, is_eval_epoch + + +def construct_loader(cfg, places, validate, precise_bn, num_iters_precise_bn, + world_size): + batch_size = cfg.DATASET.get('batch_size', 2) + train_dataset = build_dataset((cfg.DATASET.train, cfg.PIPELINE.train)) + precise_bn_dataloader_setting = dict( + batch_size=batch_size, + num_workers=cfg.DATASET.get('num_workers', 0), + places=places, + ) + if precise_bn: + cfg.DATASET.train.num_samples_precise_bn = num_iters_precise_bn * batch_size * world_size + precise_bn_dataset = build_dataset( + (cfg.DATASET.train, cfg.PIPELINE.train)) + precise_bn_loader = build_dataloader(precise_bn_dataset, + **precise_bn_dataloader_setting) + cfg.DATASET.train.num_samples_precise_bn = None + else: + precise_bn_loader = None + + if cfg.MULTIGRID.SHORT_CYCLE: + # get batch size list in short cycle schedule + bs_factor = [ + int( + round((float( + cfg.PIPELINE.train.transform[1]['MultiCrop']['target_size']) + / (s * cfg.MULTIGRID.default_crop_size))**2)) + for s in cfg.MULTIGRID.short_cycle_factors + ] + batch_sizes = [ + batch_size * bs_factor[0], + batch_size * bs_factor[1], + batch_size, + ] + train_dataloader_setting = dict( + batch_size=batch_sizes, + multigrid=True, + num_workers=cfg.DATASET.get('num_workers', 0), + places=places, + ) + else: + train_dataloader_setting = precise_bn_dataloader_setting + + train_loader = build_dataloader(train_dataset, **train_dataloader_setting) + if validate: + valid_dataset = build_dataset((cfg.DATASET.valid, cfg.PIPELINE.valid)) + validate_dataloader_setting = dict(batch_size=batch_size, + num_workers=cfg.DATASET.get( + 'num_workers', 0), + places=places, + drop_last=False, + shuffle=False) + valid_loader = build_dataloader(valid_dataset, + **validate_dataloader_setting) + else: + valid_loader = None + + return train_loader, valid_loader, precise_bn_loader + + +def build_trainer(cfg, places, parallel, validate, precise_bn, + num_iters_precise_bn, world_size): + """ + Build training model and its associated tools, including optimizer, + dataloaders and meters. + Args: + cfg (CfgNode): configs. + Returns: + model: training model. + optimizer: optimizer. + train_loader: training data loader. + val_loader: validatoin data loader. + precise_bn_loader: training data loader for computing + precise BN. + """ + model = build_model(cfg.MODEL) + if parallel: + model = paddle.DataParallel(model) + + train_loader, valid_loader, precise_bn_loader = \ + construct_loader(cfg, + places, + validate, + precise_bn, + num_iters_precise_bn, + world_size, + ) + + lr = build_lr(cfg.OPTIMIZER.learning_rate, len(train_loader)) + optimizer = build_optimizer(cfg.OPTIMIZER, lr, model=model) + + return ( + model, + lr, + optimizer, + train_loader, + valid_loader, + precise_bn_loader, + ) + + +def train_model_multigrid(cfg, world_size=1, validate=True): + """Train model entry + + Args: + cfg (dict): configuration. + parallel (bool): Whether multi-card training. Default: True + validate (bool): Whether to do evaluation. Default: False. + + """ + # Init multigrid. + multigrid = None + if cfg.MULTIGRID.LONG_CYCLE or cfg.MULTIGRID.SHORT_CYCLE: + multigrid = MultigridSchedule() + cfg = multigrid.init_multigrid(cfg) + if cfg.MULTIGRID.LONG_CYCLE: + cfg, _ = multigrid.update_long_cycle(cfg, cur_epoch=0) + multi_save_epoch = [i[-1] - 1 for i in multigrid.schedule] + + parallel = world_size != 1 + logger = get_logger("paddlevideo") + batch_size = cfg.DATASET.get('batch_size', 2) + + if cfg.get('use_npu'): + places = paddle.set_device('npu') + else: + places = paddle.set_device('gpu') + + model_name = cfg.model_name + output_dir = cfg.get("output_dir", f"./output/{model_name}") + mkdir(output_dir) + local_rank = dist.ParallelEnv().local_rank + precise_bn = cfg.get("PRECISEBN") + num_iters_precise_bn = cfg.PRECISEBN.num_iters_preciseBN + + # 1. Construct model + model = build_model(cfg.MODEL) + if parallel: + model = paddle.DataParallel(model) + + # 2. Construct dataloader + train_loader, valid_loader, precise_bn_loader = \ + construct_loader(cfg, + places, + validate, + precise_bn, + num_iters_precise_bn, + world_size, + ) + + # 3. Construct optimizer + lr = build_lr(cfg.OPTIMIZER.learning_rate, len(train_loader)) + optimizer = build_optimizer(cfg.OPTIMIZER, + lr, + parameter_list=model.parameters()) + + # Resume + resume_epoch = cfg.get("resume_epoch", 0) + if resume_epoch: + filename = osp.join( + output_dir, + model_name + str(local_rank) + '_' + f"{resume_epoch:05d}") + subn_load(model, filename, optimizer) + + # 4. Train Model + best = 0. + total_epochs = int(cfg.epochs * cfg.MULTIGRID.epoch_factor) + for epoch in range(total_epochs): + if epoch < resume_epoch: + logger.info( + f"| epoch: [{epoch+1}] <= resume_epoch: [{ resume_epoch}], continue... " + ) + continue + + if cfg.MULTIGRID.LONG_CYCLE: + cfg, changed = multigrid.update_long_cycle(cfg, epoch) + if changed: + logger.info("====== Rebuild model/optimizer/loader =====") + ( + model, + lr, + optimizer, + train_loader, + valid_loader, + precise_bn_loader, + ) = build_trainer(cfg, places, parallel, validate, precise_bn, + num_iters_precise_bn, world_size) + + #load checkpoint after re-build model + if epoch != 0: + #epoch no need to -1, haved add 1 when save + filename = osp.join( + output_dir, + model_name + str(local_rank) + '_' + f"{(epoch):05d}") + subn_load(model, filename, optimizer) + #update lr last epoch, not to use saved params + lr.last_epoch = epoch + lr.step(rebuild=True) + + model.train() + record_list = build_record(cfg.MODEL) + tic = time.time() + for i, data in enumerate(train_loader): + record_list['reader_time'].update(time.time() - tic) + # 4.1 forward + outputs = model(data, mode='train') + # 4.2 backward + avg_loss = outputs['loss'] + avg_loss.backward() + # 4.3 minimize + optimizer.step() + optimizer.clear_grad() + + # log record + record_list['lr'].update( + optimizer._global_learning_rate().numpy()[0], batch_size) + for name, value in outputs.items(): + record_list[name].update(value.numpy()[0], batch_size) + record_list['batch_time'].update(time.time() - tic) + tic = time.time() + + if i % cfg.get("log_interval", 10) == 0: + ips = "ips: {:.5f} instance/sec.".format( + batch_size / record_list["batch_time"].val) + log_batch(record_list, i, epoch + 1, total_epochs, "train", ips) + + # learning rate iter step + if cfg.OPTIMIZER.learning_rate.get("iter_step"): + lr.step() + + # learning rate epoch step + if not cfg.OPTIMIZER.learning_rate.get("iter_step"): + lr.step() + + ips = "ips: {:.5f} instance/sec.".format( + batch_size * record_list["batch_time"].count / + record_list["batch_time"].sum) + log_epoch(record_list, epoch + 1, "train", ips) + + def evaluate(best): + model.eval() + record_list = build_record(cfg.MODEL) + record_list.pop('lr') + tic = time.time() + for i, data in enumerate(valid_loader): + outputs = model(data, mode='valid') + + # log_record + for name, value in outputs.items(): + record_list[name].update(value.numpy()[0], batch_size) + + record_list['batch_time'].update(time.time() - tic) + tic = time.time() + + if i % cfg.get("log_interval", 10) == 0: + ips = "ips: {:.5f} instance/sec.".format( + batch_size / record_list["batch_time"].val) + log_batch(record_list, i, epoch + 1, total_epochs, "val", + ips) + + ips = "ips: {:.5f} instance/sec.".format( + batch_size * record_list["batch_time"].count / + record_list["batch_time"].sum) + log_epoch(record_list, epoch + 1, "val", ips) + + best_flag = False + if record_list.get('top1') and record_list['top1'].avg > best: + best = record_list['top1'].avg + best_flag = True + return best, best_flag + + # use precise bn to improve acc + if is_eval_epoch(cfg, epoch, total_epochs, multigrid.schedule): + logger.info(f"do precise BN in {epoch+1} ...") + do_preciseBN(model, precise_bn_loader, parallel, + min(num_iters_precise_bn, len(precise_bn_loader))) + + # aggregate sub_BN stats + logger.info("Aggregate sub_BatchNorm stats...") + aggregate_sub_bn_stats(model) + + # 5. Validation + if is_eval_epoch(cfg, epoch, total_epochs, multigrid.schedule): + logger.info(f"eval in {epoch+1} ...") + with paddle.no_grad(): + best, save_best_flag = evaluate(best) + # save best + if save_best_flag: + save(optimizer.state_dict(), + osp.join(output_dir, model_name + "_best.pdopt")) + save(model.state_dict(), + osp.join(output_dir, model_name + "_best.pdparams")) + logger.info( + f"Already save the best model (top1 acc){int(best * 10000) / 10000}" + ) + + # 6. Save model and optimizer + if is_eval_epoch( + cfg, epoch, + total_epochs, multigrid.schedule) or epoch % cfg.get( + "save_interval", 10) == 0 or epoch in multi_save_epoch: + logger.info("[Save parameters] ======") + subn_save(output_dir, model_name + str(local_rank) + '_', epoch + 1, + model, optimizer) + + logger.info(f'training {model_name} finished') diff --git a/paddlevideo/utils/__init__.py b/paddlevideo/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d18561d76e9424e06f596a2baa92ca2b7fe430cc --- /dev/null +++ b/paddlevideo/utils/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .registry import Registry +from .build_utils import build +from .config import * +from .logger import setup_logger, coloring, get_logger +from .record import AverageMeter, build_record, log_batch, log_epoch +from .dist_utils import get_dist_info, main_only +from .save_load import save, load, load_ckpt, mkdir +from .precise_bn import do_preciseBN +from .profiler import add_profiler_step +__all__ = ['Registry', 'build'] diff --git a/paddlevideo/utils/build_utils.py b/paddlevideo/utils/build_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..73c0ca46bae477837411a010459a4ed08549d2ee --- /dev/null +++ b/paddlevideo/utils/build_utils.py @@ -0,0 +1,35 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def build(cfg, registry, key='name'): + """Build a module from config dict. + Args: + cfg (dict): Config dict. It should at least contain the key. + registry (XXX): The registry to search the type from. + key (str): the key. + Returns: + obj: The constructed object. + """ + + assert isinstance(cfg, dict) and key in cfg + + cfg_copy = cfg.copy() + obj_type = cfg_copy.pop(key) + + obj_cls = registry.get(obj_type) + if obj_cls is None: + raise KeyError('{} is not in the {} registry'.format( + obj_type, registry.name)) + return obj_cls(**cfg_copy) diff --git a/paddlevideo/utils/config.py b/paddlevideo/utils/config.py new file mode 100644 index 0000000000000000000000000000000000000000..f4d794116f5011486f5c0fc9276681d36fdcf531 --- /dev/null +++ b/paddlevideo/utils/config.py @@ -0,0 +1,174 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import yaml +from paddlevideo.utils.logger import coloring, get_logger, setup_logger + +__all__ = ['get_config'] + +logger = setup_logger("./", name="paddlevideo", level="INFO") + + +class AttrDict(dict): + def __getattr__(self, key): + return self[key] + + def __setattr__(self, key, value): + if key in self.__dict__: + self.__dict__[key] = value + else: + self[key] = value + + +def create_attr_dict(yaml_config): + from ast import literal_eval + for key, value in yaml_config.items(): + if type(value) is dict: + yaml_config[key] = value = AttrDict(value) + if isinstance(value, str): + try: + value = literal_eval(value) + except BaseException: + pass + if isinstance(value, AttrDict): + create_attr_dict(yaml_config[key]) + else: + yaml_config[key] = value + + +def parse_config(cfg_file): + """Load a config file into AttrDict""" + with open(cfg_file, 'r') as fopen: + yaml_config = AttrDict(yaml.load(fopen, Loader=yaml.SafeLoader)) + create_attr_dict(yaml_config) + return yaml_config + + +def print_dict(d, delimiter=0): + """ + Recursively visualize a dict and + indenting acrrording by the relationship of keys. + """ + placeholder = "-" * 60 + for k, v in sorted(d.items()): + if isinstance(v, dict): + logger.info("{}{} : ".format(delimiter * " ", coloring(k, + "HEADER"))) + print_dict(v, delimiter + 4) + elif isinstance(v, list) and len(v) >= 1 and isinstance(v[0], dict): + logger.info("{}{} : ".format(delimiter * " ", + coloring(str(k), "HEADER"))) + for value in v: + print_dict(value, delimiter + 4) + else: + logger.info("{}{} : {}".format(delimiter * " ", + coloring(k, "HEADER"), + coloring(v, "OKGREEN"))) + + if k.isupper(): + logger.info(placeholder) + + +def print_config(config): + """ + visualize configs + Arguments: + config: configs + """ + print_dict(config) + + +def check_config(config): + """ + Check config + """ + pass + + +def override(dl, ks, v): + """ + Recursively replace dict of list + Args: + dl(dict or list): dict or list to be replaced + ks(list): list of keys + v(str): value to be replaced + """ + def str2num(v): + try: + return eval(v) + except Exception: + return v + + assert isinstance(dl, (list, dict)), ("{} should be a list or a dict") + assert len(ks) > 0, ('lenght of keys should larger than 0') + if isinstance(dl, list): + k = str2num(ks[0]) + if len(ks) == 1: + assert k < len(dl), ('index({}) out of range({})'.format(k, dl)) + dl[k] = str2num(v) + else: + override(dl[k], ks[1:], v) + else: + if len(ks) == 1: + #assert ks[0] in dl, ('{} is not exist in {}'.format(ks[0], dl)) + if not ks[0] in dl: + logger.warning('A new filed ({}) detected!'.format(ks[0], dl)) + dl[ks[0]] = str2num(v) + else: + assert ks[0] in dl, ( + '({}) doesn\'t exist in {}, a new dict field is invalid'.format( + ks[0], dl)) + override(dl[ks[0]], ks[1:], v) + + +def override_config(config, options=None): + """ + Recursively override the config + Args: + config(dict): dict to be replaced + options(list): list of pairs(key0.key1.idx.key2=value) + such as: [ + epochs=20', + 'PIPELINE.train.transform.1.ResizeImage.resize_short=300' + ] + Returns: + config(dict): replaced config + """ + if options is not None: + for opt in options: + assert isinstance(opt, + str), ("option({}) should be a str".format(opt)) + assert "=" in opt, ( + "option({}) should contain a =" + "to distinguish between key and value".format(opt)) + pair = opt.split('=') + assert len(pair) == 2, ("there can be only a = in the option") + key, value = pair + keys = key.split('.') + override(config, keys, value) + + return config + + +def get_config(fname, overrides=None, show=True): + """ + Read config from file + """ + assert os.path.exists(fname), ('config file({}) is not exist'.format(fname)) + config = parse_config(fname) + override_config(config, overrides) + if show: + print_config(config) + check_config(config) + return config diff --git a/paddlevideo/utils/dist_utils.py b/paddlevideo/utils/dist_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7659e88c127aa2312777c5cdd3d0afbdcdf07e6c --- /dev/null +++ b/paddlevideo/utils/dist_utils.py @@ -0,0 +1,30 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import functools + +import paddle +import paddle.distributed as dist + +def get_dist_info(): + world_size = dist.get_world_size() + rank = dist.get_rank() + return rank, world_size + +def main_only(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + rank, _ = get_dist_info() + if rank == 0: + return func(*args, **kwargs) + return wrapper diff --git a/paddlevideo/utils/logger.py b/paddlevideo/utils/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..e9791b89b68ddd302e32f350ab7b96eda9f4ce36 --- /dev/null +++ b/paddlevideo/utils/logger.py @@ -0,0 +1,113 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +import sys +import datetime + +from paddle.distributed import ParallelEnv + + + +Color = { + 'RED': '\033[31m', + 'HEADER': '\033[35m', # deep purple + 'PURPLE': '\033[95m', # purple + 'OKBLUE': '\033[94m', + 'OKGREEN': '\033[92m', + 'WARNING': '\033[93m', + 'FAIL': '\033[91m', + 'ENDC': '\033[0m' +} + + +def coloring(message, color="OKGREEN"): + assert color in Color.keys() + if os.environ.get('COLORING', True): + return Color[color] + str(message) + Color["ENDC"] + else: + return message + + +logger_initialized = [] + + +def setup_logger(output=None, name="paddlevideo", level="INFO"): + """ + Initialize the paddlevideo logger and set its verbosity level to "INFO". + Args: + output (str): a file name or a directory to save log. If None, will not save log file. + If ends with ".txt" or ".log", assumed to be a file name. + Otherwise, logs will be saved to `output/log.txt`. + name (str): the root module name of this logger + Returns: + logging.Logger: a logger + """ + def time_zone(sec, fmt): + real_time = datetime.datetime.now() + return real_time.timetuple() + logging.Formatter.converter = time_zone + + logger = logging.getLogger(name) + if level == "INFO": + logger.setLevel(logging.INFO) + elif level=="DEBUG": + logger.setLevel(logging.DEBUG) + logger.propagate = False + + if level == "DEBUG": + plain_formatter = logging.Formatter( + "[%(asctime)s] %(name)s %(levelname)s: %(message)s", + datefmt="%m/%d %H:%M:%S") + else: + plain_formatter = logging.Formatter( + "[%(asctime)s] %(message)s", + datefmt="%m/%d %H:%M:%S") + # stdout logging: master only + local_rank = ParallelEnv().local_rank + if local_rank == 0: + ch = logging.StreamHandler(stream=sys.stdout) + ch.setLevel(logging.DEBUG) + formatter = plain_formatter + ch.setFormatter(formatter) + logger.addHandler(ch) + + # file logging: all workers + if output is not None: + if output.endswith(".txt") or output.endswith(".log"): + filename = output + else: + filename = os.path.join(output, ".log.txt") + if local_rank > 0: + filename = filename + ".rank{}".format(local_rank) + + # PathManager.mkdirs(os.path.dirname(filename)) + os.makedirs(os.path.dirname(filename), exist_ok=True) + + # fh = logging.StreamHandler(_cached_log_stream(filename) + fh = logging.FileHandler(filename, mode='a') + fh.setLevel(logging.DEBUG) + fh.setFormatter(plain_formatter) + logger.addHandler(fh) + logger_initialized.append(name) + return logger + + +def get_logger(name, output=None): + logger = logging.getLogger(name) + if name in logger_initialized: + return logger + + return setup_logger(name=name, output=name) diff --git a/paddlevideo/utils/multigrid/__init__.py b/paddlevideo/utils/multigrid/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..10295b59b7ccce4504b73a9b315f836d6c07a777 --- /dev/null +++ b/paddlevideo/utils/multigrid/__init__.py @@ -0,0 +1,10 @@ +from .multigrid import MultigridSchedule +from .batchnorm_helper import get_norm, aggregate_sub_bn_stats +from .short_sampler import DistributedShortSampler +from .save_load_helper import subn_save, subn_load +from .interval_helper import is_eval_epoch + +__all__ = [ + 'MultigridSchedule', 'get_norm', 'aggregate_sub_bn_stats', + 'DistributedShortSampler', 'subn_save', 'subn_load', 'is_eval_epoch' +] diff --git a/paddlevideo/utils/multigrid/batchnorm_helper.py b/paddlevideo/utils/multigrid/batchnorm_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..e39b067d863ec7fe49111fbd7cfcbe1f4a28e3b5 --- /dev/null +++ b/paddlevideo/utils/multigrid/batchnorm_helper.py @@ -0,0 +1,142 @@ +from functools import partial +import paddle + + +def get_norm(bn_norm_type, bn_num_splits): + """ + Args: + cfg (CfgNode): model building configs, details are in the comments of + the config file. + Returns: + nn.Layer: the normalization layer. + """ + if bn_norm_type == "batchnorm": + return paddle.nn.BatchNorm3D + elif bn_norm_type == "sub_batchnorm": + return partial(SubBatchNorm3D, num_splits=bn_num_splits) + else: + raise NotImplementedError( + "Norm type {} is not supported".format(bn_norm_type)) + + +def aggregate_sub_bn_stats(model): + """ + Recursively find all SubBN modules and aggregate sub-BN stats. + Args: + model (nn.Layer): model to be aggregate sub-BN stats + Returns: + count (int): number of SubBN module found. + """ + count = 0 + for child in model.children(): + if isinstance(child, SubBatchNorm3D): + child.aggregate_stats() + count += 1 + else: + count += aggregate_sub_bn_stats(child) + return count + + +class SubBatchNorm3D(paddle.nn.Layer): + """ + Implement based on paddle2.0. + The standard BN layer computes stats across all examples in a GPU. In some + cases it is desirable to compute stats across only a subset of examples + SubBatchNorm3D splits the batch dimension into N splits, and run BN on + each of them separately (so that the stats are computed on each subset of + examples (1/N of batch) independently. During evaluation, it aggregates + the stats from all splits into one BN. + """ + def __init__(self, num_splits, **args): + """ + Args: + num_splits (int): number of splits. + args (list): list of args + """ + super(SubBatchNorm3D, self).__init__() + self.num_splits = num_splits + self.num_features = args["num_features"] + self.weight_attr = args["weight_attr"] + self.bias_attr = args["bias_attr"] + + # Keep only one set of weight and bias (outside). + if self.weight_attr == False: + self.weight = self.create_parameter( + attr=None, + shape=[self.num_features], + default_initializer=paddle.nn.initializer.Constant(1.0)) + self.weight.stop_gradient = True + else: + self.weight = self.create_parameter( + attr=self.weight_attr, + shape=[self.num_features], + default_initializer=paddle.nn.initializer.Constant(1.0)) + self.weight.stop_gradient = self.weight_attr is not None \ + and self.weight_attr.learning_rate == 0. + + if self.bias_attr == False: + self.bias = self.create_parameter(attr=None, + shape=[self.num_features], + is_bias=True) + self.bias.stop_gradient = True + else: + self.bias = self.create_parameter(attr=self.bias_attr, + shape=[self.num_features], + is_bias=True) + self.bias.stop_gradient = self.bias_attr is not None \ + and self.bias_attr.learning_rate == 0. + + # set weights and bias fixed (inner). + args["weight_attr"] = False + args["bias_attr"] = False + self.bn = paddle.nn.BatchNorm3D(**args) + # update number of features used in split_bn + args["num_features"] = self.num_features * self.num_splits + self.split_bn = paddle.nn.BatchNorm3D(**args) + + def _get_aggregated_mean_std(self, means, stds, n): + """ + Calculate the aggregated mean and stds. + Use the method of update mean and std when merge multi-part data. + Args: + means (tensor): mean values. + stds (tensor): standard deviations. + n (int): number of sets of means and stds. + """ + mean = paddle.sum(paddle.reshape(means, (n, -1)), axis=0) / n + std = (paddle.sum(paddle.reshape(stds, (n, -1)), axis=0) / n + + paddle.sum(paddle.reshape( + paddle.pow((paddle.reshape(means, (n, -1)) - mean), 2), + (n, -1)), + axis=0) / n) + return mean, std + + def aggregate_stats(self): + """ + Synchronize running_mean, and running_var to self.bn. + Call this before eval, then call model.eval(); + When eval, forward function will call self.bn instead of self.split_bn, + During this time the running_mean, and running_var of self.bn has been obtained from + self.split_bn. + """ + if self.split_bn.training: + bn_mean_tensor, bn_variance_tensor = self._get_aggregated_mean_std( + self.split_bn._mean, + self.split_bn._variance, + self.num_splits, + ) + self.bn._mean.set_value(bn_mean_tensor) + self.bn._variance.set_value(bn_variance_tensor) + + def forward(self, x): + if self.training: + n, c, t, h, w = x.shape + x = paddle.reshape( + x, (n // self.num_splits, c * self.num_splits, t, h, w)) + x = self.split_bn(x) + x = paddle.reshape(x, (n, c, t, h, w)) + else: + x = self.bn(x) + x = paddle.multiply(x, paddle.reshape(self.weight, (-1, 1, 1, 1))) + x = paddle.add(x, paddle.reshape(self.bias, (-1, 1, 1, 1))) + return x diff --git a/paddlevideo/utils/multigrid/interval_helper.py b/paddlevideo/utils/multigrid/interval_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..2df4bc702df4f8f22f12223ced5de5d7ff2babf0 --- /dev/null +++ b/paddlevideo/utils/multigrid/interval_helper.py @@ -0,0 +1,19 @@ +def is_eval_epoch(cfg, cur_epoch, total_epochs, multigrid_schedule): + """ + Determine if the model should be evaluated at the current epoch. + Args: + cfg (CfgNode): configs. Details can be found in + slowfast/config/defaults.py + cur_epoch (int): current epoch. + multigrid_schedule (List): schedule for multigrid training. + """ + if cur_epoch + 1 == total_epochs: + return True + if multigrid_schedule is not None: + prev_epoch = 0 + for s in multigrid_schedule: + if cur_epoch < s[-1]: + period = max( + (s[-1] - prev_epoch) // cfg.MULTIGRID.EVAL_FREQ + 1, 1) + return (s[-1] - 1 - cur_epoch) % period == 0 + prev_epoch = s[-1] diff --git a/paddlevideo/utils/multigrid/multigrid.py b/paddlevideo/utils/multigrid/multigrid.py new file mode 100644 index 0000000000000000000000000000000000000000..a296a0608d682e56bd503953284faa403c5f97c8 --- /dev/null +++ b/paddlevideo/utils/multigrid/multigrid.py @@ -0,0 +1,233 @@ +"""Functions for multigrid training.""" + +import numpy as np + + +class MultigridSchedule(object): + """ + This class defines multigrid training schedule and update cfg accordingly. + """ + def init_multigrid(self, cfg): + """ + Update cfg based on multigrid settings. + Args: + cfg (configs): configs that contains training and multigrid specific + hyperparameters. + Returns: + cfg (configs): the updated cfg. + """ + self.schedule = None + # We may modify cfg.DATASET.batch_size, cfg.PIPELINE.train.decode_sampler.num_frames, and + # cfg.PIPELINE.train.transform[1]['MultiCrop']['target_size'] during training, so we store their original + # value in cfg and use them as global variables. + cfg.MULTIGRID.default_batch_size = cfg.DATASET.batch_size # total bs,64 + cfg.MULTIGRID.default_temporal_size = cfg.PIPELINE.train.decode_sampler.num_frames # 32 + cfg.MULTIGRID.default_crop_size = cfg.PIPELINE.train.transform[1][ + 'MultiCrop']['target_size'] # 224 + + if cfg.MULTIGRID.LONG_CYCLE: + self.schedule = self.get_long_cycle_schedule(cfg) + cfg.OPTIMIZER.learning_rate.steps = [0] + [ + s[-1] for s in self.schedule + ] + # Fine-tuning phase. + cfg.OPTIMIZER.learning_rate.steps[-1] = ( + cfg.OPTIMIZER.learning_rate.steps[-2] + + cfg.OPTIMIZER.learning_rate.steps[-1]) // 2 + cfg.OPTIMIZER.learning_rate.lrs = [ + cfg.OPTIMIZER.learning_rate.gamma**s[0] * s[1][0] + for s in self.schedule + ] + # Fine-tuning phase. + cfg.OPTIMIZER.learning_rate.lrs = cfg.OPTIMIZER.learning_rate.lrs[:-1] + [ + cfg.OPTIMIZER.learning_rate.lrs[-2], + cfg.OPTIMIZER.learning_rate.lrs[-1], + ] + + cfg.OPTIMIZER.learning_rate.max_epoch = self.schedule[-1][-1] + + elif cfg.MULTIGRID.SHORT_CYCLE: + cfg.OPTIMIZER.learning_rate.steps = [ + int(s * cfg.MULTIGRID.epoch_factor) + for s in cfg.OPTIMIZER.learning_rate.steps + ] + cfg.OPTIMIZER.learning_rate.max_epoch = int( + cfg.OPTIMIZER.learning_rate.max_epoch * + cfg.OPTIMIZER.learning_rate.max_epoch) + return cfg + + def update_long_cycle(self, cfg, cur_epoch): + """ + Before every epoch, check if long cycle shape should change. If it + should, update cfg accordingly. + Args: + cfg (configs): configs that contains training and multigrid specific + hyperparameters. + cur_epoch (int): current epoch index. + Returns: + cfg (configs): the updated cfg. + changed (bool): whether to change long cycle shape at this epoch + """ + base_b, base_t, base_s = get_current_long_cycle_shape( + self.schedule, cur_epoch) + if base_s != cfg.PIPELINE.train.transform[1]['MultiCrop'][ + 'target_size'] or base_t != cfg.PIPELINE.train.decode_sampler.num_frames: + #NOTE Modify + # no need to modify, used by pool_size in head, None when multigrid + # cfg.MODEL.head.num_frames = base_t + # cfg.MODEL.head.crop_size = base_s + cfg.PIPELINE.train.decode_sampler.num_frames = base_t + cfg.PIPELINE.train.transform[1]['MultiCrop']['target_size'] = base_s + cfg.DATASET.batch_size = base_b * cfg.MULTIGRID.default_batch_size #change bs + + bs_factor = (float(cfg.DATASET.batch_size) / + cfg.MULTIGRID.bn_base_size) + + if bs_factor == 1: #single bs == bn_base_size (== 8) + cfg.MODEL.backbone.bn_norm_type = "batchnorm" + else: + cfg.MODEL.backbone.bn_norm_type = "sub_batchnorm" + cfg.MODEL.backbone.bn_num_splits = int(bs_factor) + + cfg.MULTIGRID.long_cycle_sampling_rate = cfg.PIPELINE.train.decode_sampler.sampling_rate * ( + cfg.MULTIGRID.default_temporal_size // base_t) + print("Long cycle updates:") + print("\tbn_norm_type: {}".format(cfg.MODEL.backbone.bn_norm_type)) + if cfg.MODEL.backbone.bn_norm_type == "sub_batchnorm": + print("\tbn_num_splits: {}".format( + cfg.MODEL.backbone.bn_num_splits)) + print("\tTRAIN.batch_size[single card]: {}".format( + cfg.DATASET.batch_size)) + print("\tDATA.NUM_FRAMES x LONG_CYCLE_SAMPLING_RATE: {}x{}".format( + cfg.PIPELINE.train.decode_sampler.num_frames, + cfg.MULTIGRID.long_cycle_sampling_rate)) + print("\tDATA.train_crop_size: {}".format( + cfg.PIPELINE.train.transform[1]['MultiCrop']['target_size'])) + return cfg, True + else: + return cfg, False + + def get_long_cycle_schedule(self, cfg): + """ + Based on multigrid hyperparameters, define the schedule of a long cycle. + Args: + cfg (configs): configs that contains training and multigrid specific + hyperparameters. + Returns: + schedule (list): Specifies a list long cycle base shapes and their + corresponding training epochs. + """ + + steps = cfg.OPTIMIZER.learning_rate.steps + + default_size = float( + cfg.PIPELINE.train.decode_sampler.num_frames * + cfg.PIPELINE.train.transform[1]['MultiCrop']['target_size']** + 2) # 32 * 224 * 224 C*H*W + default_iters = steps[-1] # 196 + + # Get shapes and average batch size for each long cycle shape. + avg_bs = [] + all_shapes = [] + # for t_factor, s_factor in cfg.MULTIGRID.long_cycle_factors: + for item in cfg.MULTIGRID.long_cycle_factors: + t_factor, s_factor = item["value"] + base_t = int( + round(cfg.PIPELINE.train.decode_sampler.num_frames * t_factor)) + base_s = int( + round( + cfg.PIPELINE.train.transform[1]['MultiCrop']['target_size'] + * s_factor)) + if cfg.MULTIGRID.SHORT_CYCLE: + shapes = [ + [ + base_t, + cfg.MULTIGRID.default_crop_size * + cfg.MULTIGRID.short_cycle_factors[0], + ], + [ + base_t, + cfg.MULTIGRID.default_crop_size * + cfg.MULTIGRID.short_cycle_factors[1], + ], + [base_t, base_s], + ] #first two is short_cycle, last is the base long_cycle + else: + shapes = [[base_t, base_s]] + + # (T, S) -> (B, T, S) + shapes = [[ + int(round(default_size / (s[0] * s[1] * s[1]))), s[0], s[1] + ] for s in shapes] + avg_bs.append(np.mean([s[0] for s in shapes])) + all_shapes.append(shapes) + + # Get schedule regardless of cfg.MULTIGRID.epoch_factor. + total_iters = 0 + schedule = [] + for step_index in range(len(steps) - 1): + step_epochs = steps[step_index + 1] - steps[step_index] + + for long_cycle_index, shapes in enumerate(all_shapes): + #ensure each of 4 sequences run the same num of iters + cur_epochs = (step_epochs * avg_bs[long_cycle_index] / + sum(avg_bs)) + + # get cur_iters from cur_epochs + cur_iters = cur_epochs / avg_bs[long_cycle_index] + total_iters += cur_iters + schedule.append((step_index, shapes[-1], cur_epochs)) + + iter_saving = default_iters / total_iters # ratio between default iters and real iters + + final_step_epochs = cfg.OPTIMIZER.learning_rate.max_epoch - steps[-1] + + # We define the fine-tuning phase to have the same amount of iteration + # saving as the rest of the training. + #final_step_epochs / iter_saving make fine-tune having the same iters as training + ft_epochs = final_step_epochs / iter_saving * avg_bs[-1] + + # schedule.append((step_index + 1, all_shapes[-1][2], ft_epochs)) + schedule.append((step_index + 1, all_shapes[-1][-1], ft_epochs)) + + # Obtrain final schedule given desired cfg.MULTIGRID.epoch_factor. + x = (cfg.OPTIMIZER.learning_rate.max_epoch * + cfg.MULTIGRID.epoch_factor / sum(s[-1] for s in schedule)) + + final_schedule = [] + total_epochs = 0 + for s in schedule: + epochs = s[2] * x + total_epochs += epochs + final_schedule.append((s[0], s[1], int(round(total_epochs)))) + print_schedule(final_schedule) + return final_schedule + + +def print_schedule(schedule): + """ + Log schedule. + """ + print( + "Long_cycle_index\tBase_shape(bs_factor,temporal_size,crop_size)\tEpochs" + ) + for s in schedule: + print("{}\t\t\t{}\t\t\t\t\t{}".format(s[0], s[1], s[2])) + + +def get_current_long_cycle_shape(schedule, epoch): + """ + Given a schedule and epoch index, return the long cycle base shape. + Args: + schedule (configs): configs that contains training and multigrid specific + hyperparameters. + cur_epoch (int): current epoch index. + Returns: + shapes (list): A list describing the base shape in a long cycle: + [batch size relative to default, + number of frames, spatial dimension]. + """ + for s in schedule: + if epoch < s[-1]: + return s[1] + return schedule[-1][1] diff --git a/paddlevideo/utils/multigrid/save_load_helper.py b/paddlevideo/utils/multigrid/save_load_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..94a52d58b1deebafc41f78aff6e9a95e1322b71f --- /dev/null +++ b/paddlevideo/utils/multigrid/save_load_helper.py @@ -0,0 +1,237 @@ +import os +import numpy as np +import paddle +import copy + + +def sub_to_normal_bn(sd): + """ + When save, Convert the Sub-BN paprameters to normal BN parameters in a state dict. + There are two copies of BN layers in a Sub-BN implementation: `bn.bn` and + `bn.split_bn`. `bn.split_bn` is used during training and + "compute_precise_bn". Before saving or evaluation, its stats are copied to + `bn.bn`. We rename `bn.bn` to `bn` and store it to be consistent with normal + BN layers. + Args: + sd (OrderedDict): a dict of parameters which might contain Sub-BN + parameters. + Returns: + new_sd (OrderedDict): a dict with Sub-BN parameters reshaped to + normal parameters. + """ + modifications = [ + ("bn.bn._mean", "bn._mean"), + ("bn.bn._variance", "bn._variance"), + ] + to_remove = ["bn.bn.", ".split_bn."] + key_list = list(sd.keys()) #odict_keys to list + for key in key_list: + for before, after in modifications: + if key.endswith(before): + new_key = key.split(before)[0] + after + sd[new_key] = sd.pop(key) + + for rm in to_remove: + if rm in key and key in sd: + del sd[key] + + +def normal_to_sub_bn(checkpoint_sd, model_sd): + """ + When load, Convert BN parameters to Sub-BN parameters if model contains Sub-BNs. + Args: + checkpoint_sd (OrderedDict): source dict of parameters. + model_sd (OrderedDict): target dict of parameters. + Returns: + new_sd (OrderedDict): converted dict of parameters. + """ + for key in model_sd: + if key not in checkpoint_sd: + # not to replace bn.weight and bn.bias + if "bn.split_bn." in key and "bn.weight" not in key and "bn.bias" not in key: + load_key = key.replace("bn.split_bn.", "bn.") + bn_key = key.replace("bn.split_bn.", "bn.bn.") + checkpoint_sd[key] = checkpoint_sd.pop(load_key) + checkpoint_sd[bn_key] = checkpoint_sd[key] + + # match the shape of bn.split_bn._xx + # model_sd: split_bn.rm.shape = num_feature*num_split + # checkpoint_sd: split_bn.rm.shape = bn.rm.shape = num_feature + for key in model_sd: + if key in checkpoint_sd: + model_blob_shape = model_sd[key].shape #bn.split_bn + c2_blob_shape = checkpoint_sd[key].shape #bn.bn + + if (len(model_blob_shape) == 1 and len(c2_blob_shape) == 1 + and model_blob_shape[0] > c2_blob_shape[0] + and model_blob_shape[0] % c2_blob_shape[0] == 0): + before_shape = checkpoint_sd[key].shape + checkpoint_sd[key] = np.concatenate( + [checkpoint_sd[key]] * + (model_blob_shape[0] // c2_blob_shape[0])) + if 'split_bn' not in key: #split_bn is excepted + print("{} {} -> {}".format(key, before_shape, + checkpoint_sd[key].shape)) + return checkpoint_sd + + +def mapping_opt_dict(opt_dict, model_key_list): + """ + Paddle Name schedule: conv_1.w -> conv_2.w + Sometimes: sub_bn -> bn + when re-build model, we desire the parameter name to be coincident, + but the parameters name index will be added, as conv_1 to conv_2, not conv_1. + It will raise error if we set old saved parameters to new created optimizer. + as conv_2 cannot find in state_dict(only conv_1). + Args: + opt_dict: optimizer state dict, including the name and value of parameters gradient. + model_key_list: the parameters name list of re-build model. + Return: optimizer state dict with modified keys + """ + def get_name_info(PNAME, PN_key_list, key_list): + min_index = float('inf') + max_index = 0 + for name in PN_key_list[1:]: + for key in key_list: + if name in key: + index = int(key.split('.')[0].split(name)[-1]) + if index < min_index: + min_index = index + if index > max_index: + max_index = index + num_name = max_index - min_index + 1 + PNAME[name].append((min_index, max_index, num_name)) + min_index = float('inf') + max_index = 0 + + PNAME = { + "LR_Scheduler": [], + "conv3d_": [], + "linear_": [], + "sub_batch_norm3d_": [], + "batch_norm3d_": [], + } + + pd_key_list = list(opt_dict.keys()) + print("The number of parameters in saved optimizer state dict = {}".format( + len(pd_key_list))) + print("The number of parameters in re-build model list = {}".format( + len(model_key_list))) + # 1 may be LR_Scheduler + PN_key_list = list(PNAME.keys()) + + # get the number of each PNAME + get_name_info(PNAME, PN_key_list, pd_key_list) + get_name_info(PNAME, PN_key_list, model_key_list) + print("[Parameters info] prefix: min_index, max_index, number_params: \n", + PNAME) + + # whether to change name of bn layer + change_name = False + if PNAME["sub_batch_norm3d_"][0][-1] == -float('inf'): + PN_key_list.remove("sub_batch_norm3d_") + if PNAME["sub_batch_norm3d_"][1][-1] != -float('inf'): + print( + "Optimizer state dict saved bn, but Re-build model use sub_bn, changed name!" + ) + change_name = True + else: + print("Optimizer state dict saved bn, and Re-build model use bn") + else: + PN_key_list.remove("batch_norm3d_") + if PNAME["sub_batch_norm3d_"][1][-1] == -float('inf'): + print( + "Optimizer state dict saved sub_bn, but Re-build model use bn, changed name!" + ) + change_name = True + else: + print( + "Optimizer state dict saved sub_bn, Re-build model use sub_bn") + + #update key name + # sub_bn -> bn name mapping, pre-define dict + change_dict = { + "sub_batch_norm3d_": "batch_norm3d_", + "batch_norm3d_": "sub_batch_norm3d_" + } + for key in pd_key_list: + for name in PN_key_list[1:]: + if key.startswith(name): + start = change_dict[name] if ( + change_name and "batch_norm" in name) else name + str_index = key.split('.')[0].split(name)[-1] + index = int(str_index) + new_index = str(index + + (PNAME[start][1][0] - PNAME[name][0][0])) + end = key.split('.')[-1] + update_key = start + new_index + '.' + end + opt_dict[update_key] = opt_dict.pop(key) + + return opt_dict + + +def subn_save(save_dir, name_prefix, epoch, video_model, optimizer): + if not os.path.isdir(save_dir): + os.makedirs(save_dir) + model_path = os.path.join(save_dir, name_prefix + "{:05d}".format(epoch)) + model_dict = video_model.state_dict() + sub_to_normal_bn(model_dict) + opti_dict = optimizer.state_dict() + paddle.save(model_dict, model_path + '.pdparams') + paddle.save(opti_dict, model_path + '.pdopt') + print('[Saved Epoch {} parameters and optimizer state ]'.format(epoch)) + + +def subn_load(model, ck_path, optimizer=None): + """ + Load the checkpoint from the given file. + Args: + model (model): model to load the weights from the checkpoint. + optimizer (optim, optional): optimizer to load the historical state. + ck_path (str): checkpoint path + Returns: + (int): the number of training epoch of the checkpoint. + """ + + assert os.path.exists(ck_path + ".pdparams"), \ + "Given dir {}.pdparams not exist.".format(ck_path) + print("load checkpint from {}.pdparams".format(ck_path)) + + model_dict = model.state_dict() + checkpoint_dict = paddle.load(ck_path + ".pdparams") + # checkpoint_dict = copy.deepcopy(checkpoint_dict_orig) #not modify when multi card + pre_train_dict = normal_to_sub_bn(checkpoint_dict, model_dict) + + # Match pre-trained weights that have same shape as current model. + pre_train_dict_match = { + k: v + for k, v in pre_train_dict.items() + if k in model_dict and tuple(v.shape) == tuple(model_dict[k].shape) + } + + # Weights that do not have match from the pre-trained model. + not_load_layers = [ + k for k in model_dict.keys() if k not in pre_train_dict_match.keys() + ] + # Log weights that are not loaded with the pre-trained weights. + if not_load_layers: + for k in not_load_layers: + if 'bn.weight' not in k and 'bn.bias' not in k: + print("Network weights {} not loaded.".format(k)) + + # Load pre-trained weights. + model.set_state_dict(pre_train_dict_match) + + if optimizer: + assert os.path.exists(ck_path + ".pdopt"), \ + "Given dir {}.pdopt not exist.".format(ck_path) + print("load checkpint from {}.pdopt".format(ck_path)) + opt_dict = paddle.load(ck_path + ".pdopt") + # get parameters that required gradient from re-build model + model_key_list = [] + for param in model.parameters(): + if param.stop_gradient == False: + model_key_list.append(param.name) + + new_opt_dict = mapping_opt_dict(opt_dict, model_key_list) + optimizer.set_state_dict(new_opt_dict) diff --git a/paddlevideo/utils/multigrid/short_sampler.py b/paddlevideo/utils/multigrid/short_sampler.py new file mode 100644 index 0000000000000000000000000000000000000000..0004dace4f33bee731c9163bf4d2b876413eb952 --- /dev/null +++ b/paddlevideo/utils/multigrid/short_sampler.py @@ -0,0 +1,147 @@ +from __future__ import print_function +from __future__ import division + +import numpy as np +import math + +from paddle.io import BatchSampler + +__all__ = ["DistributedShortSampler"] + + +class DistributedShortSampler(BatchSampler): + """Sampler that restricts data loading to a subset of the dataset. + In such case, each process can pass a DistributedBatchSampler instance + as a DataLoader sampler, and load a subset of the original dataset that + is exclusive to it. + .. note:: + Batch size is dynamic changed following short cycle schedule. + + Args: + dataset(paddle.io.Dataset): this could be a `paddle.io.Dataset` implement + or other python object which implemented + `__len__` for BatchSampler to get sample + number of data source. + batch_sizes(list): batch size list of one cycle. + num_replicas(int, optional): porcess number in distributed training. + If :attr:`num_replicas` is None, :attr:`num_replicas` will be + retrieved from :code:`paddle.fluid.dygraph.parallel.ParallenEnv`. + Default None. + rank(int, optional): the rank of the current process among :attr:`num_replicas` + processes. If :attr:`rank` is None, :attr:`rank` is retrieved from + :code:`paddle.fluid.dygraph.parallel.ParallenEnv`. Default None. + shuffle(bool): whther to shuffle indices order before genrating + batch indices. Default False. + drop_last(bool): whether drop the last incomplete batch dataset size + is not divisible by the batch size. Default False + """ + def __init__(self, + dataset, + batch_sizes, + num_replicas=None, + rank=None, + shuffle=False, + drop_last=False): + self.dataset = dataset + + assert any(isinstance(batch_size, int) and batch_size > 0 for batch_size in batch_sizes), \ + "batch_size should be a positive integer" + self.batch_sizes = batch_sizes + self.len_batch_sizes = len(self.batch_sizes) + assert isinstance(shuffle, bool), \ + "shuffle should be a boolean value" + self.shuffle = shuffle + assert isinstance(drop_last, bool), \ + "drop_last should be a boolean number" + + from paddle.distributed import ParallelEnv + + if num_replicas is not None: + assert isinstance(num_replicas, int) and num_replicas > 0, \ + "num_replicas should be a positive integer" + self.nranks = num_replicas + else: + self.nranks = ParallelEnv().nranks + + if rank is not None: + assert isinstance(rank, int) and rank >= 0, \ + "rank should be a non-negative integer" + self.local_rank = rank + else: + self.local_rank = ParallelEnv().local_rank + + self.drop_last = drop_last + self.epoch = 0 + self.num_samples = int(math.ceil(len(self.dataset) * 1.0 / self.nranks)) + self.total_size = self.num_samples * self.nranks + + def __iter__(self): + num_samples = len(self.dataset) + indices = np.arange(num_samples).tolist() + indices += indices[:(self.total_size - + len(indices))] #completion last iter + assert len(indices) == self.total_size + if self.shuffle: + np.random.RandomState(self.epoch).shuffle(indices) + self.epoch += 1 + + # subsample + def _get_indices_by_batch_size(indices): + total_batch_size = sum(self.batch_sizes) + subsampled_indices = [] + last_batch_size = self.total_size % ( + total_batch_size * self.nranks) #number samples of last batch + assert last_batch_size % self.nranks == 0 + last_local_batch_size = last_batch_size // self.nranks + + for i in range(self.local_rank * total_batch_size, + len(indices) - last_batch_size, + total_batch_size * self.nranks): + subsampled_indices.extend(indices[i:i + total_batch_size]) + + indices = indices[len(indices) - last_batch_size:] + subsampled_indices.extend( + indices[self.local_rank * + last_local_batch_size:(self.local_rank + 1) * + last_local_batch_size]) + return subsampled_indices + + if self.nranks > 1: + indices = _get_indices_by_batch_size(indices) + + assert len(indices) == self.num_samples #index length in each card + _sample_iter = iter(indices) + + batch_indices = [] + counter = 0 + batch_size = self.batch_sizes[0] + for idx in _sample_iter: + batch_indices.append( + (idx, counter % + self.len_batch_sizes)) #to be used in dataloader get_item + if len(batch_indices) == batch_size: + yield batch_indices + counter += 1 + batch_size = self.batch_sizes[counter % self.len_batch_sizes] + batch_indices = [] + if not self.drop_last and len(batch_indices) > 0: + yield batch_indices + + def __len__(self): + avg_batch_size = sum(self.batch_sizes) / float(self.len_batch_sizes) + if self.drop_last: + return int(np.floor(self.num_samples / avg_batch_size)) + else: + return int(np.ceil(self.num_samples / avg_batch_size)) + + def set_epoch(self, epoch): + """ + Sets the epoch number. When :attr:`shuffle=True`, this number is used + as seeds of random numbers. By default, users may not set this, all + replicas (workers) use a different random ordering for each epoch. + If set same number at each epoch, this sampler will yield the same + ordering at all epoches. + Arguments: + epoch (int): Epoch number. + """ + self.epoch = epoch diff --git a/paddlevideo/utils/precise_bn.py b/paddlevideo/utils/precise_bn.py new file mode 100644 index 0000000000000000000000000000000000000000..1e6660d4c3b23ac95784662fb9d1eaca8aa9aed5 --- /dev/null +++ b/paddlevideo/utils/precise_bn.py @@ -0,0 +1,99 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import paddle +import itertools + +from paddlevideo.utils import get_logger +logger = get_logger("paddlevideo") +""" +Implement precise bn, which is useful for improving accuracy. +""" + + +@paddle.no_grad() # speed up and save CUDA memory +def do_preciseBN(model, + data_loader, + parallel, + num_iters=200, + use_amp=False, + amp_level=None): + """ + Recompute and update the batch norm stats to make them more precise. During + training both BN stats and the weight are changing after every iteration, so + the running average can not precisely reflect the actual stats of the + current model. + In this function, the BN stats are recomputed with fixed weights, to make + the running average more precise. Specifically, it computes the true average + of per-batch mean/variance instead of the running average. + This is useful to improve validation accuracy. + Args: + model: the model whose bn stats will be recomputed + data_loader: an iterator. Produce data as input to the model + num_iters: number of iterations to compute the stats. + Return: + the model with precise mean and variance in bn layers. + """ + bn_layers_list = [ + m for m in model.sublayers() + if any((isinstance(m, bn_type) + for bn_type in (paddle.nn.BatchNorm1D, paddle.nn.BatchNorm2D, + paddle.nn.BatchNorm3D))) and m.training + ] + if len(bn_layers_list) == 0: + return + + # moving_mean=moving_mean*momentum+batch_mean*(1.−momentum) + # we set momentum=0. to get the true mean and variance during forward + momentum_actual = [bn._momentum for bn in bn_layers_list] + for bn in bn_layers_list: + bn._momentum = 0. + + running_mean = [paddle.zeros_like(bn._mean) + for bn in bn_layers_list] # pre-ignore + running_var = [paddle.zeros_like(bn._variance) for bn in bn_layers_list] + + ind = -1 + for ind, data in enumerate(itertools.islice(data_loader, num_iters)): + logger.info("doing precise BN {} / {}...".format(ind + 1, num_iters)) + + if parallel: + if use_amp: + with paddle.amp.auto_cast(custom_black_list={"reduce_mean"}, + level=amp_level): + model._layers.train_step(data) + else: + model._layers.train_step(data) + else: + if use_amp: + with paddle.amp.auto_cast(custom_black_list={"reduce_mean"}, + level=amp_level): + model.train_step(data) + else: + model.train_step(data) + + for i, bn in enumerate(bn_layers_list): + # Accumulates the bn stats. + running_mean[i] += (bn._mean - running_mean[i]) / (ind + 1) + running_var[i] += (bn._variance - running_var[i]) / (ind + 1) + + assert ind == num_iters - 1, ( + "update_bn_stats is meant to run for {} iterations, but the dataloader stops at {} iterations." + .format(num_iters, ind)) + + # Sets the precise bn stats. + for i, bn in enumerate(bn_layers_list): + bn._mean.set_value(running_mean[i]) + bn._variance.set_value(running_var[i]) + bn._momentum = momentum_actual[i] diff --git a/paddlevideo/utils/profiler.py b/paddlevideo/utils/profiler.py new file mode 100644 index 0000000000000000000000000000000000000000..a75018907f203e33df387889ff735c36273a2143 --- /dev/null +++ b/paddlevideo/utils/profiler.py @@ -0,0 +1,109 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import paddle + +# A global variable to record the number of calling times for profiler +# functions. It is used to specify the tracing range of training steps. +_profiler_step_id = 0 + +# A global variable to avoid parsing from string every time. +_profiler_options = None + + +class ProfilerOptions(object): + """ + Use a string to initialize a ProfilerOptions. + The string should be in the format: "key1=value1;key2=value;key3=value3". + For example: + "profile_path=model.profile" + "batch_range=[50, 60]; profile_path=model.profile" + "batch_range=[50, 60]; tracer_option=OpDetail; profile_path=model.profile" + + ProfilerOptions supports following key-value pair: + batch_range - a integer list, e.g. [100, 110]. + state - a string, the optional values are 'CPU', 'GPU' or 'All'. + sorted_key - a string, the optional values are 'calls', 'total', + 'max', 'min' or 'ave. + tracer_option - a string, the optional values are 'Default', 'OpDetail', + 'AllOpDetail'. + profile_path - a string, the path to save the serialized profile data, + which can be used to generate a timeline. + exit_on_finished - a boolean. + """ + def __init__(self, options_str): + assert isinstance(options_str, str) + + self._options = { + 'batch_range': [10, 20], + 'state': 'All', + 'sorted_key': 'total', + 'tracer_option': 'Default', + 'profile_path': '/tmp/profile', + 'exit_on_finished': True + } + self._parse_from_string(options_str) + + def _parse_from_string(self, options_str): + for kv in options_str.replace(' ', '').split(';'): + key, value = kv.split('=') + if key == 'batch_range': + value_list = value.replace('[', '').replace(']', '').split(',') + value_list = list(map(int, value_list)) + if len(value_list) >= 2 and value_list[0] >= 0 and value_list[ + 1] > value_list[0]: + self._options[key] = value_list + elif key == 'exit_on_finished': + self._options[key] = value.lower() in ("yes", "true", "t", "1") + elif key in [ + 'state', 'sorted_key', 'tracer_option', 'profile_path' + ]: + self._options[key] = value + + def __getitem__(self, name): + if self._options.get(name, None) is None: + raise ValueError( + "ProfilerOptions does not have an option named %s." % name) + return self._options[name] + + +def add_profiler_step(options_str: str = None) -> None: + """Enable the operator-level timing using PaddlePaddle's profiler. + The profiler uses a independent variable to count the profiler steps. + One call of this function is treated as a profiler step. + + Args: + options_str (str, optional): a string to initialize the ProfilerOptions. Defaults to None. + """ + if options_str is None: + return + + global _profiler_step_id + global _profiler_options + + if _profiler_options is None: + _profiler_options = ProfilerOptions(options_str) + + if _profiler_step_id == _profiler_options['batch_range'][0]: + paddle.utils.profiler.start_profiler(_profiler_options['state'], + _profiler_options['tracer_option']) + elif _profiler_step_id == _profiler_options['batch_range'][1]: + paddle.utils.profiler.stop_profiler(_profiler_options['sorted_key'], + _profiler_options['profile_path']) + if _profiler_options['exit_on_finished']: + sys.exit(0) + + _profiler_step_id += 1 diff --git a/paddlevideo/utils/record.py b/paddlevideo/utils/record.py new file mode 100644 index 0000000000000000000000000000000000000000..13e35e81810459d0eae868aaac875d0f078cbad4 --- /dev/null +++ b/paddlevideo/utils/record.py @@ -0,0 +1,148 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import OrderedDict + +import paddle + +from .logger import coloring, get_logger + +logger = get_logger("paddlevideo") + +__all__ = ['AverageMeter', 'build_record', 'log_batch', 'log_epoch'] + + +def build_record(cfg): + record_list = [ + ("loss", AverageMeter('loss', '7.5f')), + ("lr", AverageMeter('lr', 'f', need_avg=False)), + ] + if 'Recognizer1D' in cfg.framework: #TODO: required specify str in framework + record_list.append(("hit_at_one", AverageMeter("hit_at_one", '.5f'))) + record_list.append(("perr", AverageMeter("perr", '.5f'))) + record_list.append(("gap", AverageMeter("gap", '.5f'))) + elif 'Recognizer' in cfg.framework: + record_list.append(("top1", AverageMeter("top1", '.5f'))) + record_list.append(("top5", AverageMeter("top5", '.5f'))) + elif 'FastRCNN' in cfg.framework: + record_list.append( + ("recall@thr=0.5", AverageMeter("recall@thr=0.5", '.5f'))) + record_list.append(("prec@thr=0.5", AverageMeter("prec@thr=0.5", + '.5f'))) + record_list.append(("recall@top3", AverageMeter("recall@top3", '.5f'))) + record_list.append(("prec@top3", AverageMeter("prec@top3", '.5f'))) + record_list.append(("recall@top5", AverageMeter("recall@top5", '.5f'))) + record_list.append(("prec@top5", AverageMeter("prec@top5", '.5f'))) + record_list.append(("mAP@0.5IOU", AverageMeter("mAP@0.5IOU", '.5f'))) + elif 'DepthEstimator' in cfg.framework: + record_list.append(("abs_rel", AverageMeter("abs_rel", '.5f'))) + record_list.append(("sq_rel", AverageMeter("sq_rel", '.5f'))) + record_list.append(("rmse", AverageMeter("rmse", '.5f'))) + record_list.append(("rmse_log", AverageMeter("rmse_log", '.5f'))) + record_list.append(("a1", AverageMeter("a1", '.5f'))) + record_list.append(("a2", AverageMeter("a2", '.5f'))) + record_list.append(("a3", AverageMeter("a3", '.5f'))) + record_list.append(("losses_day", AverageMeter("losses_day", '.5f'))) + record_list.append(("losses_night", AverageMeter("losses_night", + '.5f'))) + elif 'MSTCN' in cfg.framework or 'ASRF' in cfg.framework: + record_list.append(("F1@0.50", AverageMeter("F1@0.50", '.5f'))) + + record_list.append(("batch_time", AverageMeter('batch_cost', '.5f'))) + record_list.append(("reader_time", AverageMeter('reader_cost', '.5f'))) + record_list = OrderedDict(record_list) + return record_list + + +class AverageMeter(object): + """ + Computes and stores the average and current value + """ + + def __init__(self, name='', fmt='f', need_avg=True): + self.name = name + self.fmt = fmt + self.need_avg = need_avg + self.reset() + + def reset(self): + """ reset """ + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + """ update """ + if isinstance(val, paddle.Tensor): + val = val.numpy()[0] + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + @property + def total(self): + return '{self.name}_sum: {self.sum:{self.fmt}}'.format(self=self) + + @property + def total_minute(self): + return '{self.name}_sum: {s:{self.fmt}} min'.format(s=self.sum / 60, + self=self) + + @property + def mean(self): + return '{self.name}_avg: {self.avg:{self.fmt}}'.format( + self=self) if self.need_avg else '' + + @property + def value(self): + return '{self.name}: {self.val:{self.fmt}}'.format(self=self) + + +def log_batch(metric_list, batch_id, epoch_id, total_epoch, mode, ips): + batch_cost = str(metric_list['batch_time'].value) + ' sec,' + reader_cost = str(metric_list['reader_time'].value) + ' sec,' + + metric_values = [] + for m in metric_list: + if not (m == 'batch_time' or m == 'reader_time'): + metric_values.append(metric_list[m].value) + metric_str = ' '.join([str(v) for v in metric_values]) + epoch_str = "epoch:[{:>3d}/{:<3d}]".format(epoch_id, total_epoch) + step_str = "{:s} step:{:<4d}".format(mode, batch_id) + + logger.info("{:s} {:s} {:s} {:s} {:s} {}".format( + coloring(epoch_str, "HEADER") if batch_id == 0 else epoch_str, + coloring(step_str, "PURPLE"), coloring(metric_str, 'OKGREEN'), + coloring(batch_cost, "OKGREEN"), coloring(reader_cost, 'OKGREEN'), ips)) + + +def log_epoch(metric_list, epoch, mode, ips): + batch_cost = 'avg_' + str(metric_list['batch_time'].value) + ' sec,' + reader_cost = 'avg_' + str(metric_list['reader_time'].value) + ' sec,' + batch_sum = str(metric_list['batch_time'].total) + ' sec,' + + metric_values = [] + for m in metric_list: + if not (m == 'batch_time' or m == 'reader_time'): + metric_values.append(metric_list[m].mean) + metric_str = ' '.join([str(v) for v in metric_values]) + + end_epoch_str = "END epoch:{:<3d}".format(epoch) + + logger.info("{:s} {:s} {:s} {:s} {:s} {:s} {}".format( + coloring(end_epoch_str, "RED"), coloring(mode, "PURPLE"), + coloring(metric_str, "OKGREEN"), coloring(batch_cost, "OKGREEN"), + coloring(reader_cost, "OKGREEN"), coloring(batch_sum, "OKGREEN"), ips)) diff --git a/paddlevideo/utils/registry.py b/paddlevideo/utils/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..81b76bd51f55013cb45f2b923c3e518bfb218d53 --- /dev/null +++ b/paddlevideo/utils/registry.py @@ -0,0 +1,96 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Registry(object): + """ + The registry that provides name -> object mapping, to support third-party users' custom modules. + + To register an object: + + .. code-block:: python + + BACKBONES = Registry('backbone') + @BACKBONES.register() + class ResNet: + pass + Or: + .. code-block:: python + + BACKBONES = Registry('backbone') + class ResNet: + pass + BACKBONES.register(ResNet) + + Usage: To build a module. + + .. code-block:: python + backbone_name = "ResNet" + b = BACKBONES.get(backbone_name)() + + """ + def __init__(self, name): + """ + Args: + name (str): the name of this registry + """ + self._name = name + self._obj_map = {} + + def __contains__(self, key): + return self._obj_map.get(key) is not None + + def _do_register(self, name, obj): + assert ( + name not in self._obj_map + ), "An object named '{}' was already registered in '{}' registry!".format( + name, self._name) + self._obj_map[name] = obj + + def register(self, obj=None, name=None): + """ + Register the given object under the the name `obj.__name__`. + Can be used as either a decorator or not. See docstring of this class for usage. + """ + if obj is None: + # used as a decorator + def deco(func_or_class, name=name): + if name is None: + name = func_or_class.__name__ + self._do_register(name, func_or_class) + return func_or_class + + return deco + + # used as a function call + if name is None: + name = obj.__name__ + self._do_register(name, obj) + + def get(self, name): + """Get the registry record. + + Args: + name (str): The class name. + + Returns: + ret: The class. + """ + ret = self._obj_map.get(name) + if ret is None: + raise KeyError( + "No object named '{}' found in '{}' registry!".format( + name, self._name)) + + return ret diff --git a/paddlevideo/utils/save_load.py b/paddlevideo/utils/save_load.py new file mode 100644 index 0000000000000000000000000000000000000000..71465cbb5f5a44d3741eeca287587e9c073714de --- /dev/null +++ b/paddlevideo/utils/save_load.py @@ -0,0 +1,302 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import os.path as osp +import time + +import paddle +import paddle.nn.functional as F +from paddlevideo.utils import get_logger, main_only +from tqdm import tqdm + + +def pretrain_swin_param_trans(model, state_dicts): + # delete classifier's params + if 'head.fc' + '.weight' in state_dicts: + del state_dicts['head.fc' + '.weight'] + if 'head.fc' + '.bias' in state_dicts: + del state_dicts['head.fc' + '.bias'] + + state_dicts = { + k.replace('backbone.', ''): v + for k, v in state_dicts.items() + } + + if len(state_dicts) == len(model.state_dict()): + print("Load 3D weights") + return state_dicts + + print("Load 2D weights") + relative_position_index_keys = [ + k for k in state_dicts.keys() if "relative_position_index" in k + ] + for k in relative_position_index_keys: + del state_dicts[k] + + # delete attn_mask since we always re-init it + attn_mask_keys = [k for k in state_dicts.keys() if "attn_mask" in k] + for k in attn_mask_keys: + del state_dicts[k] + + state_dicts['patch_embed.proj.weight'] = state_dicts[ + 'patch_embed.proj.weight'].unsqueeze(2).tile( + [1, 1, model.patch_size[0], 1, 1]) / model.patch_size[0] + + # bicubic interpolate relative_position_bias_table if not match + relative_position_bias_table_keys = [ + k for k in state_dicts.keys() if "relative_position_bias_table" in k + ] + total_len = len(relative_position_bias_table_keys) + with tqdm(total=total_len, + position=1, + bar_format='{desc}', + desc="Loading weights") as desc: + for key in tqdm(relative_position_bias_table_keys, + total=total_len, + position=0): + relative_position_bias_table_pretrained = state_dicts[key] + relative_position_bias_table_current = model.state_dict()[key] + L1, nH1 = relative_position_bias_table_pretrained.shape + L2, nH2 = relative_position_bias_table_current.shape + L2 = (2 * model.window_size[1] - 1) * (2 * model.window_size[2] - 1) + wd = model.window_size[0] + if nH1 != nH2: + desc.set_description(f"Error in loading {key}, skip") + else: + if L1 != L2: + S1 = int(L1**0.5) + relative_position_bias_table_pretrained_resized = paddle.nn.functional.interpolate( + relative_position_bias_table_pretrained.transpose( + [1, 0]).reshape([1, nH1, S1, S1]), + size=(2 * model.window_size[1] - 1, + 2 * model.window_size[2] - 1), + mode='bicubic') + relative_position_bias_table_pretrained = relative_position_bias_table_pretrained_resized.reshape( + [nH2, L2]).transpose([1, 0]) + desc.set_description(f"Loading {key}") + state_dicts[key] = relative_position_bias_table_pretrained.tile( + [2 * wd - 1, 1]) + time.sleep(0.01) + ret_str = "loading {:<20d} weights completed.".format( + len(model.state_dict())) + desc.set_description(ret_str) + return state_dicts + + +def pretrain_vit_param_trans(model, state_dicts, num_patches, num_seg, + attention_type): + """ + Convert ViT's pre-trained model parameters to a parameter dictionary that matches the existing model + """ + if 'head' + '.weight' in state_dicts: + del state_dicts['head' + '.weight'] + if 'head' + '.bias' in state_dicts: + del state_dicts['head' + '.bias'] + + total_len = len(model.state_dict()) + if num_patches + 1 != state_dicts['pos_embed'].shape[1]: + pos_embed = state_dicts['pos_embed'] + cls_pos_embed = pos_embed[0, 0, :].unsqueeze(0).unsqueeze(1) + other_pos_embed = pos_embed[0, + 1:, :].unsqueeze(0).unsqueeze(1).transpose( + (0, 1, 3, 2)) + new_pos_embed = F.interpolate(other_pos_embed, + size=(other_pos_embed.shape[-2], + num_patches), + mode='nearest') + new_pos_embed = new_pos_embed.squeeze(0).transpose((0, 2, 1)) + new_pos_embed = paddle.concat((cls_pos_embed, new_pos_embed), axis=1) + state_dicts['pos_embed'] = new_pos_embed + time.sleep(0.01) + + if 'time_embed' in state_dicts and num_seg != state_dicts[ + 'time_embed'].shape[1]: + time_embed = state_dicts['time_embed'].transpose((0, 2, 1)).unsqueeze(0) + new_time_embed = F.interpolate(time_embed, + size=(time_embed.shape[-2], num_seg), + mode='nearest') + state_dicts['time_embed'] = new_time_embed.squeeze(0).transpose( + (0, 2, 1)) + time.sleep(0.01) + with tqdm(total=total_len, + position=1, + bar_format='{desc}', + desc="Loading weights") as desc: + if attention_type == 'divided_space_time': + new_state_dicts = state_dicts.copy() + for key in tqdm(state_dicts): + if 'blocks' in key and 'attn' in key: + desc.set_description("Loading %s" % key) + new_key = key.replace('attn', 'temporal_attn') + if not new_key in state_dicts: + new_state_dicts[new_key] = state_dicts[key] + else: + new_state_dicts[new_key] = state_dicts[new_key] + if 'blocks' in key and 'norm1' in key: + desc.set_description("Loading %s" % key) + new_key = key.replace('norm1', 'temporal_norm1') + if not new_key in state_dicts: + new_state_dicts[new_key] = state_dicts[key] + else: + new_state_dicts[new_key] = state_dicts[new_key] + time.sleep(0.01) + ret_str = "loading {:<20d} weights completed.".format( + len(model.state_dict())) + desc.set_description(ret_str) + return new_state_dicts + + +def pretrain_resnet18_param_trans(model, loaded_dict): + encoder_dict = model.encoder.state_dict() + pose_encoder_dict = model.pose_encoder.state_dict() + + names = ['encoder.', 'encoder_day.', 'encoder_night.'] + for name in names: + total_len = len(loaded_dict.items()) + with tqdm(total=total_len, + position=1, + bar_format='{desc}', + desc="Loading weights") as desc: + for key, value in tqdm(loaded_dict.items(), + total=total_len, + position=0): + key = str(name + key) + if key in encoder_dict: + encoder_dict[key] = value + desc.set_description('Loading %s' % key) + time.sleep(0.01) + + num_input_images = 2 + loaded_dict['conv1.weight'] = paddle.concat( + [loaded_dict['conv1.weight']] * num_input_images, 1) / num_input_images + total_len = len(loaded_dict.items()) + with tqdm(total=total_len, + position=1, + bar_format='{desc}', + desc="Loading weights") as desc: + for name, value in tqdm(loaded_dict.items(), + total=total_len, + position=0): + name = str('encoder.' + name) + if name in pose_encoder_dict: + pose_encoder_dict[name] = value + desc.set_description('Loading %s' % key) + time.sleep(0.01) + ret_str = "loading {:<20d} weights completed.".format( + len(model.state_dict())) + desc.set_description(ret_str) + return encoder_dict, pose_encoder_dict + + +#XXX(shipping): maybe need load N times because of different cards have different params. +@main_only +def load_ckpt(model, weight_path, **kargs): + """ + 1. Load pre-trained model parameters + 2. Extract and convert from the pre-trained model to the parameters + required by the existing model + 3. Load the converted parameters of the existing model + """ + #model.set_state_dict(state_dict) + + if not osp.isfile(weight_path): + raise IOError(f'{weight_path} is not a checkpoint file') + #state_dicts = load(weight_path) + + logger = get_logger("paddlevideo") + state_dicts = paddle.load(weight_path) + if 'ResnetEncoder' in str(model): + encoder_dict, pose_encoder_dict = pretrain_resnet18_param_trans( + model, state_dicts) + model.encoder.load_dict(encoder_dict) + model.pose_encoder.load_dict(pose_encoder_dict) + tmp = model.state_dict() + elif "VisionTransformer" in str(model): # For TimeSformer case + tmp = pretrain_vit_param_trans(model, state_dicts, kargs['num_patches'], + kargs['num_seg'], + kargs['attention_type']) + elif 'SwinTransformer3D' in str(model): + tmp = pretrain_swin_param_trans(model, state_dicts) + else: + tmp = {} + total_len = len(model.state_dict()) + with tqdm(total=total_len, + position=1, + bar_format='{desc}', + desc="Loading weights") as desc: + for item in tqdm(model.state_dict(), total=total_len, position=0): + name = item + desc.set_description('Loading %s' % name) + if name not in state_dicts: # Convert from non-parallel model + if str('backbone.' + name) in state_dicts: + tmp[name] = state_dicts['backbone.' + name] + else: # Convert from parallel model + tmp[name] = state_dicts[name] + time.sleep(0.01) + ret_str = "loading {:<20d} weights completed.".format( + len(model.state_dict())) + desc.set_description(ret_str) + model.set_state_dict(tmp) + + +def mkdir(dir): + if not os.path.exists(dir): + # avoid error when train with multiple gpus + try: + os.makedirs(dir) + except: + pass + + +""" +def save(state_dicts, file_name): + def convert(state_dict): + model_dict = {} + + for k, v in state_dict.items(): + if isinstance( + v, + (paddle.fluid.framework.Variable, paddle.fluid.core.VarBase)): + model_dict[k] = v.numpy() + else: + model_dict[k] = v + + return model_dict + + final_dict = {} + for k, v in state_dicts.items(): + if isinstance( + v, + (paddle.fluid.framework.Variable, paddle.fluid.core.VarBase)): + final_dict = convert(state_dicts) + break + elif isinstance(v, dict): + final_dict[k] = convert(v) + else: + final_dict[k] = v + + with open(file_name, 'wb') as f: + pickle.dump(final_dict, f, protocol=2) +""" + + +@main_only +def save(obj, path): + paddle.save(obj, path) + + +def load(file_name): + if not osp.isfile(file_name): + raise IOError(f'{file_name} not exist') + return paddle.load(file_name) diff --git a/paddlevideo/version.py b/paddlevideo/version.py new file mode 100644 index 0000000000000000000000000000000000000000..b5b7f481f4afac92604a6b9f036eb93510069556 --- /dev/null +++ b/paddlevideo/version.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ["paddlevideo_version"] +paddlevideo_version = "0.0.1" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..0dbc253ea2e03081455670a19e5e0e75bfab4aa1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +numpy +pandas +tqdm +PyYAML>=5.1 +opencv-python==4.2.0.32 +decord==0.4.2 +av==8.0.3 +scipy==1.6.3 +scikit-image diff --git a/run.sh b/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..0f4fd0ec56c98a10bdb16f912226ae5e736c8756 --- /dev/null +++ b/run.sh @@ -0,0 +1,86 @@ +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +#export FLAGS_conv_workspace_size_limit=800 #MB +#export FLAGS_cudnn_exhaustive_search=1 +#export FLAGS_cudnn_batchnorm_spatial_persistent=1 + + +start_time=$(date +%s) + +# run ava training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=logdir.ava_part main.py --validate -w paddle.init_param.pdparams -c configs/detection/ava/ava_part.yaml +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=logdir.ava_all.1203 main.py --validate -w paddle.init_param.pdparams -c configs/detection/ava/ava_all.yaml + +# run adds training +# python3.7 main.py --validate -c configs/estimation/adds/adds.yaml --seed 20 + +# run tsm training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --validate -c configs/recognition/tsm/tsm_k400_frames.yaml + +# run tsm amp training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_k400_frames.yaml + +# run tsm amp training, nhwc +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsm main.py --amp --validate -c configs/recognition/tsm/tsm_k400_frames_nhwc.yaml + +# run tsn training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_tsn main.py --validate -c configs/recognition/tsn/tsn_k400_frames.yaml + +# run video-swin-transformer training +# python3.7 -u -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_videoswin main.py --amp --validate -c configs/recognition/videoswin/videoswin_k400_videos.yaml + +# run slowfast training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_slowfast main.py --validate -c configs/recognition/slowfast/slowfast.yaml + +# run slowfast multi-grid training +# python3.7 -B -m paddle.distributed.launch --selected_gpus="0,1,2,3,4,5,6,7" --log_dir=log-slowfast main.py --validate --multigrid -c configs/recognition/slowfast/slowfast_multigrid.yaml + +# run bmn training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_bmn main.py --validate -c configs/localization/bmn.yaml + +# run attention_lstm training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_attetion_lstm main.py --validate -c configs/recognition/attention_lstm/attention_lstm_youtube-8m.yaml + +# run pp-tsm training +python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsm main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml + +# run pp-tsn training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptsn main.py --validate -c configs/recognition/pptsn/pptsn_k400_frames.yaml + +# run timesformer training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_timesformer main.py --validate -c configs/recognition/timesformer/timesformer_k400_videos.yaml + +# run pp-timesformer training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_pptimesformer main.py --validate -c configs/recognition/pptimesformer/pptimesformer_k400_videos.yaml + +# run st-gcn training +# python3.7 main.py -c configs/recognition/stgcn/stgcn_fsd.yaml + +# run agcn training +# python3.7 main.py -c configs/recognition/agcn/agcn_fsd.yaml + +# run actbert training +# python3.7 main.py --validate -c configs/multimodal/actbert/actbert.yaml + +# run tsn dali training +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3" --log_dir=log_tsn main.py --train_dali -c configs/recognition/tsn/tsn_dali.yaml + + +# test.sh +# just use `example` as example, please replace to real name. +# python3.7 -B -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --log_dir=log_test main.py --test -c configs/example.yaml -w "output/example/example_best.pdparams" + +# NOTE: run bmn test, only support single card, bs=1 +# python3.7 main.py --test -c configs/localization/bmn.yaml -w output/BMN/BMN_epoch_00010.pdparams -o DATASET.batch_size=1 + +# export_models script +# just use `example` as example, please replace to real name. +# python3.7 tools/export_model.py -c configs/example.yaml -p output/example/example_best.pdparams -o ./inference + +# predict script +# just use `example` as example, please replace to real name. +# python3.7 tools/predict.py -v example.avi --model_file "./inference/example.pdmodel" --params_file "./inference/example.pdiparams" --enable_benchmark=False --model="example" --num_seg=8 + +end_time=$(date +%s) +cost_time=$[ $end_time-$start_time ] +echo "Time to train is $(($cost_time/60))min $(($cost_time%60))s" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..b5f3e85ffcebdec9339ff32c26dfa2870c76926a --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from setuptools import setup +from io import open + +with open('requirements.txt', encoding="utf-8-sig") as f: + requirements = f.readlines() + +def readme(): + with open('docs/en/whl_en.md', encoding="utf-8-sig") as f: + README = f.read() + return README + + +setup( + name='paddlevideo', #name of .whl file + packages=['ppvideo'], #install package name + package_dir={'ppvideo': ''}, + include_package_data=True, #Accept all data files and directories matched by MANIFEST.in + install_requires=requirements, + entry_points={"console_scripts": ["ppvideo= ppvideo.tools.paddlevideo_clas:main"]}, + version='0.0.1', + license='Apache License 2.0', + description='Awesome Video toolkits based on PaddlePaddle ', + long_description=readme(), + long_description_content_type='text/markdown', + url='https://github.com/PaddlePaddle/PaddleVideo', + download_url='https://github.com/PaddlePaddle/PaddleVideo.git', + keywords=[ + 'A treasure chest for video understanding powered by PaddlePaddle.' + ], + classifiers=[ + 'Intended Audience :: Developers', 'Operating System :: OS Independent', + 'Natural Language :: Chinese (Simplified)', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Utilities' + ],) \ No newline at end of file diff --git a/test_tipc/README.md b/test_tipc/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e80c6e9c562035e1ad3ba7b8e76195868de84e32 --- /dev/null +++ b/test_tipc/README.md @@ -0,0 +1,122 @@ + +# 飞桨训推一体认证(TIPC) + +## 1. 简介 + +飞桨除了基本的模型训练和预测,还提供了支持多端多平台的高性能推理部署工具。本文档提供了PaddleVideo中所有模型的飞桨训推一体认证 (Training and Inference Pipeline Certification(TIPC)) 信息和测试工具,方便用户查阅每种模型的训练推理部署打通情况,并可以进行一键测试。 + +
    + +
    + +## 2. 汇总信息 + +打通情况汇总如下,已填写的部分表示可以使用本工具进行一键测试,未填写的表示正在支持中。 + +**字段说明:** +- 基础训练预测:包括模型训练、Paddle Inference Python预测。 +- 更多训练方式:包括多机多卡(TODO)、混合精度。 +- 模型压缩(TODO):包括裁剪、离线/在线量化、蒸馏。 +- 其他预测部署:包括Paddle Inference C++预测、Paddle Serving部署(TODO)、Paddle-Lite部署(TODO)等。 + +更详细的mkldnn、Tensorrt等预测加速相关功能的支持情况可以查看各测试工具的[更多教程](#more)。 + +| 算法名称 | 模型名称 | 模型类型 | 基础
    训练预测 | 更多
    训练方式 | 模型压缩 | 其他预测部署 | +| :--- | :--- | :----: | :--------: | :---- | :---- | :---- | +| PP-TSM |pptsm_k400_frames_uniform | 动作识别 | 支持 | 混合精度 | - | Paddle Inference: C++ | +| PP-TSN |pptsn_k400_videos | 动作识别 | 支持 | 混合精度 | - | Paddle Inference: C++ | +| AGCN |agcn_fsd | 动作识别 | 支持 | 混合精度 | - | - | +| STGCN |stgcn_fsd | 动作识别 | 支持 | 混合精度 | - | - | +| TimeSformer |timesformer_k400_videos | 动作识别 | 支持 | 混合精度 | - | - | +| SlowFast |slowfast | 动作识别 | 支持 | 混合精度 | - | - | +| TSM |tsm_k400_frames | 动作识别 | 支持 | 混合精度 | - | - | +| TSN |tsn_k400_frames | 动作识别 |支持|混合精度|-|-| +| AttentionLSTM |attention_lstm_youtube8m | 动作识别 | 支持 | 混合精度 | - | - | +| BMN |bmn | 动作时间定位 | 支持 | 混合精度 | - | - | + + + +## 3. 测试工具简介 +### 目录介绍 + +```shell +test_tipc/ +├── configs/ # 配置文件目录 +│ ├── PP-TSM/ +│ │ ├── train_infer_python.txt # PP-TSM在Linux上进行python训练预测(基础训练预测)的配置文件 +│ │ └── train_amp_infer_python.txt # PP-TSM在Linux上进行python训练预测(混合精度训练预测)的配置文件 +│ ├── PP-TSN/ +│ │ ├── train_infer_python.txt # PP-TSN在Linux上进行python训练预测(基础训练预测)的配置文件 +│ │ └── train_amp_infer_python.txt # PP-TSN在Linux上进行python训练预测(混合精度训练预测)的配置文件 +│ ├── ... +│ └── ... +├── results/ # 预先保存的预测结果,用于和实际预测结果进行精度比对 +│ ├── PP-TSM/ +│ │ ├── python_ppvideo_PP-TSM_results_fp16.txt # 预存的PP-TSM识别识别模型python预测fp16精度的结果 +│ │ └── python_ppvideo_PP-TSM_results_fp32.txt # 预存的PP-TSM识别识别模型python预测fp32精度的结果 +│ ├── PP-TSN/ +│ │ ├── python_ppvideo_PP-TSN_results_fp32.txt # 预存的PP-TSN识别识别模型python预测fp16精度的结果 +│ │ └── python_ppvideo_PP-TSN_results_fp32.txt # 预存的PP-TSN识别识别模型python预测fp32精度的结果 +│ ├── PP-TSN_CPP/ +│ │ ├── python_ppvideo_PP-TSN_results_fp32.txt # 预存的PP-TSN识别识别模型C++预测fp16精度的结果 +│ │ └── python_ppvideo_PP-TSN_results_fp32.txt # 预存的PP-TSN识别识别模型C++预测fp32精度的结果 +│ ├── ... +│ └── ... +├── prepare.sh # 完成test_*.sh运行所需要的数据和模型下载 +├── docs/ # 详细的TIPC各种功能文档 +├── test_train_inference_python.sh # 测试python训练预测的主程序 +├── test_inference_cpp.sh # 测试C++预测的主程序 +├── compare_results.py # 用于对比log中的预测结果与results中的预存结果精度误差是否在限定范围内 +└── README.md # 介绍文档 +``` + +### 测试流程概述 + +使用本工具,可以测试不同功能的支持情况,以及预测结果是否对齐,测试流程概括如下: +
    + +
    + + +1. 运行prepare.sh准备测试所需数据和模型; +2. 运行要测试的功能对应的测试脚本`test_*.sh`,产出log,由log可以看到不同配置是否运行成功; +3. 用`compare_results.py`对比log中的预测结果和预存在results目录下的结果,判断预测精度是否符合预期(在误差范围内)。 + +测试单项功能仅需两行命令,**如需测试不同模型/功能,替换配置文件即可**,命令格式如下: +```shell +# 功能:准备数据 +# 格式:bash + 运行脚本 + 参数1: 配置文件选择 + 参数2: 模式选择 +bash test_tipc/prepare.sh configs/[model_name]/[params_file_name] [Mode] + +# 功能:运行测试 +# 格式:bash + 运行脚本 + 参数1: 配置文件选择 + 参数2: 模式选择 +bash test_tipc/test_train_inference_python.sh configs/[model_name]/[params_file_name] [Mode] +``` + +例如,测试基本训练预测功能的`lite_train_lite_infer`模式,运行: +```shell +# 准备数据 +bash test_tipc/prepare.sh ./test_tipc/configs/PP-TSM/train_infer_python.txt 'lite_train_lite_infer' +# 运行测试 +bash test_tipc/test_train_inference_python.sh ./test_tipc/configs/PP-TSM/train_infer_python.txt 'lite_train_lite_infer' +``` +关于本示例命令的更多信息可查看[基础训练预测使用文档](./docs/test_train_inference_python.md)。 + +### 配置文件命名规范 +在`configs`目录下存放所有模型测试需要用到的配置文件,配置文件的命名遵循如下规范: + +1. 基础训练预测配置简单命名为:`train_infer_python.txt`,表示**Linux环境下单机、不使用混合精度训练+python预测**,其完整命名对应`train_linux_gpu_normal_normal_infer_python_linux_gpu_cpu.txt`,由于本配置文件使用频率较高,这里进行了名称简化。 + +2. 其他带训练配置命名格式为:`train_训练硬件环境(linux_gpu/linux_dcu/…)_是否多机(fleet/normal)_是否混合精度(amp/normal)_预测模式(infer/lite/serving/js)_语言(cpp/python/java)_预测硬件环境(linux_gpu/mac/jetson/opencl_arm_gpu/...).txt`。如,linux gpu下多机多卡+混合精度链条测试对应配置 `train_linux_gpu_fleet_amp_infer_python_linux_gpu_cpu.txt`,linux dcu下基础训练预测对应配置 `train_linux_dcu_normal_normal_infer_python_linux_dcu.txt`。 + +3. 仅预测的配置(如serving、lite等)命名格式:`model_训练硬件环境(linux_gpu/linux_dcu/…)_是否多机(fleet/normal)_是否混合精度(amp/normal)_(infer/lite/serving/js)_语言(cpp/python/java)_预测硬件环境(linux_gpu/mac/jetson/opencl_arm_gpu/...).txt`,即,与2相比,仅第一个字段从train换为model,测试时模型直接下载获取,这里的“训练硬件环境”表示所测试的模型是在哪种环境下训练得到的。 + +**根据上述命名规范,可以直接从子目录名称和配置文件名找到需要测试的场景和功能对应的配置文件。** + + + +## 4. 开始测试 +各功能测试中涉及混合精度、裁剪、量化等训练相关,及mkldnn、Tensorrt等多种预测相关参数配置,请点击下方相应链接了解更多细节和使用教程: +- [test_train_inference_python 使用](docs/test_train_inference_python.md) :测试基于Python的模型训练、评估、推理等基本功能。 +- [test_amp_train_inference_python 使用](docs/test_train_amp_inference_python.md) :测试基于Python的**混合精度**模型训练、评估、推理等基本功能。 +- [test_inference_cpp 使用](docs/test_inference_cpp.md) :测试基于C++的模型推理功能。 diff --git a/test_tipc/benchmark_train.sh b/test_tipc/benchmark_train.sh new file mode 100644 index 0000000000000000000000000000000000000000..74d77405deab0a92961ada491c9dd92dd7996120 --- /dev/null +++ b/test_tipc/benchmark_train.sh @@ -0,0 +1,286 @@ +#!/bin/bash +source test_tipc/common_func.sh + +# set env +python=python +export model_branch=`git symbolic-ref HEAD 2>/dev/null | cut -d"/" -f 3` +export model_commit=$(git log|head -n1|awk '{print $2}') +export str_tmp=$(echo `pip list|grep paddlepaddle-gpu|awk -F ' ' '{print $2}'`) +export frame_version=${str_tmp%%.post*} +export frame_commit=$(echo `${python} -c "import paddle;print(paddle.version.commit)"`) + +# BENCHMARK_ROOT='.' # only for self-test + +# run benchmark sh +# Usage: +# bash run_benchmark_train.sh config.txt params +# or +# bash run_benchmark_train.sh config.txt + +function func_parser_params(){ + strs=$1 + IFS="=" + array=(${strs}) + tmp=${array[1]} + echo ${tmp} +} + +function func_sed_params(){ + filename=$1 + line=$2 + param_value=$3 + params=`sed -n "${line}p" $filename` + IFS=":" + array=(${params}) + key=${array[0]} + value=${array[1]} + if [[ $value =~ 'benchmark_train' ]];then + IFS='=' + _val=(${value}) + param_value="${param_value}" + fi + new_params="${key}:${param_value}" + IFS=";" + cmd="sed -i '${line}s/.*/${new_params}/' '${filename}'" + eval $cmd +} + +function set_gpu_id(){ + string=$1 + _str=${string:1:6} + IFS="C" + arr=(${_str}) + M=${arr[0]} + P=${arr[1]} + gn=`expr $P - 1` + gpu_num=`expr $gn / $M` + seq=`seq -s "," 0 $gpu_num` + echo $seq +} + +function get_repo_name(){ + IFS=";" + cur_dir=$(pwd) + IFS="/" + arr=(${cur_dir}) + echo ${arr[-1]} +} + +FILENAME=$1 +# copy FILENAME as new +new_filename="./test_tipc/benchmark_train.txt" +cmd=`yes|cp $FILENAME $new_filename` +FILENAME=$new_filename +# MODE must be one of ['benchmark_train'] +MODE=$2 +PARAMS=$3 +# bash test_tipc/benchmark_train.sh test_tipc/configs/det_mv3_db_v2_0/train_benchmark.txt benchmark_train dynamic_bs8_null_DP_N1C1 +IFS=$'\n' +# parser params from train_benchmark.txt +dataline=`cat $FILENAME` +# parser params +IFS=$'\n' +lines=(${dataline}) +model_name=$(func_parser_value "${lines[1]}") + +# 获取'train_benchmark_params'所在的行数 +line_num=`grep -n "train_benchmark_params" $FILENAME | cut -d ":" -f 1` +# for train log parser +batch_size=$(func_parser_value "${lines[line_num]}") +line_num=`expr $line_num + 1` +fp_items=$(func_parser_value "${lines[line_num]}") +line_num=`expr $line_num + 1` +epoch=$(func_parser_value "${lines[line_num]}") + +line_num=`expr $line_num + 1` +profile_option_key=$(func_parser_key "${lines[line_num]}") +profile_option_params=$(func_parser_value "${lines[line_num]}") +profile_option="${profile_option_key}:${profile_option_params}" + +line_num=`expr $line_num + 1` +flags_value=$(func_parser_value "${lines[line_num]}") +# set flags +IFS=";" +flags_list=(${flags_value}) +for _flag in ${flags_list[*]}; do + cmd="export ${_flag}" + eval $cmd +done + +# set log_name +repo_name=$(get_repo_name ) +SAVE_LOG=${BENCHMARK_LOG_DIR:-$(pwd)} # */benchmark_log +mkdir -p "${SAVE_LOG}/benchmark_log/" +status_log="${SAVE_LOG}/benchmark_log/results.log" + +# The number of lines in which train params can be replaced. +line_python=3 +line_gpuid=4 +line_precision=6 +line_epoch=7 +line_batchsize=9 +line_profile=12 +line_eval_py=24 +line_eval_py_2=25 +line_export_py=38 +line_export_py_2=28 +line_export_py_3=30 +line_norm_train=16 + +func_sed_params "$FILENAME" "${line_eval_py}" "null" +func_sed_params "$FILENAME" "${line_eval_py_2}" "null" +func_sed_params "$FILENAME" "${line_export_py}" "null" +func_sed_params "$FILENAME" "${line_export_py_2}" "null" +func_sed_params "$FILENAME" "${line_export_py_3}" "null" +func_sed_params "$FILENAME" "${line_python}" "$python" + +# 末尾加上--max_iters=30和--log_interval=1,以便运行并输出足量数据 +set_log_interval_cmd="sed -i '${line_norm_train}s/.*/& --max_iters=30 -o log_interval=1/' '${filename}'" +eval $set_log_interval_cmd + +# 去掉--validate,benchmark不需要validate +remove_validate_cmd="sed -i '${line_norm_train}s/--validate//' '${filename}'" +eval $remove_validate_cmd + +# if params +if [ ! -n "$PARAMS" ] ;then + # PARAMS input is not a word. + IFS="|" + batch_size_list=(${batch_size}) + fp_items_list=(${fp_items}) + device_num_list=(N1C4) + run_mode="DP" +else + # parser params from input: modeltype_bs${bs_item}_${fp_item}_${run_mode}_${device_num} + IFS="_" + params_list=(${PARAMS}) + model_type=${params_list[0]} + batch_size=${params_list[1]} + batch_size=`echo ${batch_size} | tr -cd "[0-9]" ` + precision=${params_list[2]} + run_mode=${params_list[3]} + device_num=${params_list[4]} + IFS=";" + + if [ ${precision} = "null" ];then + precision="fp32" + fi + + fp_items_list=($precision) + batch_size_list=($batch_size) + device_num_list=($device_num) +fi + +log_interval='--log_interval 1' +IFS="|" +for batch_size in ${batch_size_list[*]}; do + for precision in ${fp_items_list[*]}; do + for device_num in ${device_num_list[*]}; do + # sed batchsize and precision + func_sed_params "$FILENAME" "${line_precision}" "$precision" + func_sed_params "$FILENAME" "${line_batchsize}" "$batch_size" + func_sed_params "$FILENAME" "${line_epoch}" "$epoch" + gpu_id=$(set_gpu_id $device_num) + + if [ ${#gpu_id} -le 1 ];then + log_path="$SAVE_LOG/profiling_log" + mkdir -p $log_path + log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_profiling" + func_sed_params "$FILENAME" "${line_gpuid}" "0" # sed used gpu_id + # set profile_option params + tmp=`sed -i "${line_profile}s/.*/${profile_option}/" "${FILENAME}"` + + # for models which need to accumulate gradient. + if [[ ${model_name} =~ "TimeSformer" ]]; then + global_bs=`expr ${batch_size} \* ${device_num:3:4} \* 8` + modify_global_bs_cmd="sed -i '${line_norm_train}s/.*/& -o GRADIENT_ACCUMULATION.global_batch_size=${global_bs}/' '${filename}'" + eval $modify_global_bs_cmd + fi + + # run test_train_inference_python.sh + cmd="bash test_tipc/test_train_inference_python.sh ${FILENAME} benchmark_train > ${log_path}/${log_name} 2>&1 " + echo $cmd + eval $cmd + eval "cat ${log_path}/${log_name}" + + # without profile + log_path="$SAVE_LOG/train_log" + speed_log_path="$SAVE_LOG/index" + mkdir -p $log_path + mkdir -p $speed_log_path + log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_log" + speed_log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_speed" + func_sed_params "$FILENAME" "${line_profile}" "null" # sed profile_id as null + + cmd="bash test_tipc/test_train_inference_python.sh ${FILENAME} benchmark_train > ${log_path}/${log_name} 2>&1 " + echo $cmd + job_bt=`date '+%Y%m%d%H%M%S'` + eval $cmd + job_et=`date '+%Y%m%d%H%M%S'` + export model_run_time=$((${job_et}-${job_bt})) + eval "cat ${log_path}/${log_name}" + + # parser log + _model_name="${model_name}_bs${batch_size}_${precision}_${run_mode}" + cmd="${python} ${BENCHMARK_ROOT}/scripts/analysis.py --filename ${log_path}/${log_name} \ + --speed_log_file '${speed_log_path}/${speed_log_name}' \ + --model_name ${_model_name} \ + --base_batch_size ${batch_size} \ + --run_mode ${run_mode} \ + --fp_item ${precision} \ + --keyword ips: \ + --skip_steps 5 \ + --device_num ${device_num} \ + --speed_unit instance/sec \ + --convergence_key loss: " + echo $cmd + eval $cmd + last_status=${PIPESTATUS[0]} + status_check $last_status "${cmd}" "${status_log}" + else + IFS=";" + unset_env=`unset CUDA_VISIBLE_DEVICES` + log_path="$SAVE_LOG/train_log" + speed_log_path="$SAVE_LOG/index" + mkdir -p $log_path + mkdir -p $speed_log_path + log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_log" + speed_log_name="${repo_name}_${model_name}_bs${batch_size}_${precision}_${run_mode}_${device_num}_speed" + func_sed_params "$FILENAME" "${line_gpuid}" "$gpu_id" # sed used gpu_id + func_sed_params "$FILENAME" "${line_profile}" "null" # sed --profile_option as null + + # for models which need to accumulate gradient. + if [[ ${model_name} =~ "TimeSformer" ]]; then + global_bs=`expr ${batch_size} \* ${device_num:3:4} \* 8` + modify_global_bs_cmd="sed -i '${line_norm_train}s/.*/& -o GRADIENT_ACCUMULATION.global_batch_size=${global_bs}/' '${filename}'" + eval $modify_global_bs_cmd + fi + + cmd="bash test_tipc/test_train_inference_python.sh ${FILENAME} benchmark_train > ${log_path}/${log_name} 2>&1 " + echo $cmd + job_bt=`date '+%Y%m%d%H%M%S'` + eval $cmd + job_et=`date '+%Y%m%d%H%M%S'` + export model_run_time=$((${job_et}-${job_bt})) + eval "cat ${log_path}/${log_name}" + # parser log + _model_name="${model_name}_bs${batch_size}_${precision}_${run_mode}" + cmd="${python} ${BENCHMARK_ROOT}/scripts/analysis.py --filename ${log_path}/${log_name} \ + --speed_log_file '${speed_log_path}/${speed_log_name}' \ + --model_name ${_model_name} \ + --base_batch_size ${batch_size} \ + --run_mode ${run_mode} \ + --fp_item ${precision} \ + --keyword ips: \ + --skip_steps 5 \ + --device_num ${device_num} \ + --speed_unit instance/sec \ + --convergence_key loss: " + echo $cmd + eval $cmd + last_status=${PIPESTATUS[0]} + status_check $last_status "${cmd}" "${status_log}" + fi + done + done +done diff --git a/test_tipc/common_func.sh b/test_tipc/common_func.sh new file mode 100644 index 0000000000000000000000000000000000000000..2c039e9642e8c125d1e73981e1d97e723b15a9ea --- /dev/null +++ b/test_tipc/common_func.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +function func_parser_key(){ + strs=$1 + IFS=":" + array=(${strs}) + tmp=${array[0]} + echo ${tmp} +} + +function func_parser_value(){ + strs=$1 + IFS=":" + array=(${strs}) + tmp=${array[1]} + echo ${tmp} +} + +function func_set_params(){ + key=$1 + value=$2 + if [ ${key}x = "null"x ];then + echo " " + elif [[ ${value} = "null" ]] || [[ ${value} = " " ]] || [ ${#value} -le 0 ];then + echo " " + else + echo "${key}=${value}" + fi +} + +function func_parser_params(){ + strs=$1 + IFS=":" + array=(${strs}) + key=${array[0]} + tmp=${array[1]} + IFS="|" + res="" + for _params in ${tmp[*]}; do + IFS="=" + array=(${_params}) + mode=${array[0]} + value=${array[1]} + if [[ ${mode} = ${MODE} ]]; then + IFS="|" + #echo $(func_set_params "${mode}" "${value}") + echo $value + break + fi + IFS="|" + done + echo ${res} +} + +function status_check(){ + last_status=$1 # the exit code + run_command=$2 + run_log=$3 + if [ $last_status -eq 0 ]; then + echo -e "\033[33m Run successfully with command - ${run_command}! \033[0m" | tee -a ${run_log} + else + echo -e "\033[33m Run failed with command - ${run_command}! \033[0m" | tee -a ${run_log} + fi +} diff --git a/test_tipc/compare_results.py b/test_tipc/compare_results.py new file mode 100644 index 0000000000000000000000000000000000000000..dd8308dc9a00634c26abc0324cec12721f2da69c --- /dev/null +++ b/test_tipc/compare_results.py @@ -0,0 +1,171 @@ +import numpy as np +import os +import subprocess +import json +import argparse +import glob + + +def init_args(): + parser = argparse.ArgumentParser() + # params for testing assert allclose + parser.add_argument("--atol", type=float, default=1e-3) + parser.add_argument("--rtol", type=float, default=1e-3) + parser.add_argument("--gt_file", type=str, default="") + parser.add_argument("--log_file", type=str, default="") + parser.add_argument("--precision", type=str, default="fp32") + return parser + + +def parse_args(): + parser = init_args() + return parser.parse_args() + + +def run_shell_command(cmd): + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True) + out, err = p.communicate() + + if p.returncode == 0: + return out.decode('utf-8') + else: + return None + + +def parser_results_from_log_by_name(log_path, names_list): + if not os.path.exists(log_path): + raise ValueError("The log file {} does not exists!".format(log_path)) + + if names_list is None or len(names_list) < 1: + return [] + + parser_results = {} + lines = open(log_path, 'r').read().splitlines() + if 'python_infer' in log_path: # parse python inference + for line in lines: + split_items = line.replace('\t', ' ') + split_items = split_items.split(' ') + split_items = [item for item in split_items if len(item) > 0] + for name in names_list: + if name in line: + if '.' in split_items[-1]: + parser_results[name] = float(split_items[-1]) + else: + parser_results[name] = int(split_items[-1]) + else: # parse cpp inference + for line in lines: + split_items = line.replace('\t', ' ') + split_items = split_items.split(' ') + split_items = [item for item in split_items if len(item) > 0] + if all([(name + ':') in split_items for name in names_list]): + # print(split_items) + parser_results['class'] = int(split_items[2]) + parser_results['score'] = float(split_items[-1]) + return parser_results + + +def load_gt_from_file(gt_file): + if not os.path.exists(gt_file): + raise ValueError("The log file {} does not exists!".format(gt_file)) + with open(gt_file, 'r') as f: + data = f.readlines() + f.close() + parser_gt = {} + for line in data: + if 'top-1 class' in line: + split_items = line.replace('\t', ' ') + split_items = split_items.split(' ') + split_items = [item for item in split_items if len(item) > 0] + parser_gt['top-1 class'] = int(split_items[-1]) + elif 'top-1 score' in line: + split_items = line.replace('\t', ' ') + split_items = split_items.split(' ') + split_items = [item for item in split_items if len(item) > 0] + parser_gt['top-1 score'] = float(split_items[-1]) + elif "score" in line and 'segment' in line: + location_dict = eval(line) + parser_gt[f"score_{len(parser_gt)}"] = location_dict['score'] + parser_gt[f"segment_{len(parser_gt)}"] = location_dict['segment'] + elif "class:" in line and "score:" in line: + split_items = line.replace('\t', ' ') + split_items = split_items.split(' ') + split_items = [item for item in split_items if len(item) > 0] + parser_gt['class'] = int(split_items[2]) + parser_gt['score'] = float(split_items[-1]) + return parser_gt + + +def load_gt_from_txts(gt_file): + gt_list = glob.glob(gt_file) + gt_collection = {} + for gt_f in gt_list: + gt_dict = load_gt_from_file(gt_f) + basename = os.path.basename(gt_f) + if "fp32" in basename: + gt_collection["fp32"] = [gt_dict, gt_f] + elif "fp16" in basename: + gt_collection["fp16"] = [gt_dict, gt_f] + elif "int8" in basename: + gt_collection["int8"] = [gt_dict, gt_f] + else: + continue + return gt_collection + + +def collect_predict_from_logs(log_path, key_list): + log_list = glob.glob(log_path) + pred_collection = {} + for log_f in log_list: + pred_dict = parser_results_from_log_by_name(log_f, key_list) + key = os.path.basename(log_f) + pred_collection[key] = pred_dict + + return pred_collection + + +def testing_assert_allclose(dict_x, dict_y, atol=1e-7, rtol=1e-7): + for k in dict_x: + np.testing.assert_allclose(np.array(dict_x[k]), + np.array(dict_y[k]), + atol=atol, + rtol=rtol) + + +if __name__ == "__main__": + # Usage example: + # test python infer: + ## python3.7 test_tipc/compare_results.py --gt_file=./test_tipc/results/PP-TSM/*.txt --log_file=./test_tipc/output/PP-TSM/python_infer_*.log + # test cpp infer: + ## python3.7 test_tipc/compare_results.py --gt_file=./test_tipc/results/PP-TSM_CPP/*.txt --log_file=./test_tipc/output/PP-TSM_CPP/cpp_infer_*.log + + args = parse_args() + + gt_collection = load_gt_from_txts(args.gt_file) + key_list = gt_collection["fp32"][0].keys() + pred_collection = collect_predict_from_logs(args.log_file, key_list) + for filename in pred_collection.keys(): + if "fp32" in filename: + gt_dict, gt_filename = gt_collection["fp32"] + elif "fp16" in filename: + gt_dict, gt_filename = gt_collection["fp16"] + elif "int8" in filename: + gt_dict, gt_filename = gt_collection["int8"] + else: + continue + pred_dict = pred_collection[filename] + try: + testing_assert_allclose(gt_dict, + pred_dict, + atol=args.atol, + rtol=args.rtol) + print( + "Assert allclose passed! The results of {} and {} are consistent!" + .format(filename, gt_filename)) + except Exception as E: + print(E) + raise ValueError( + "The results of {} and the results of {} are inconsistent!". + format(filename, gt_filename)) diff --git a/test_tipc/configs/AGCN/train_amp_infer_python.txt b/test_tipc/configs/AGCN/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..0dc3a48c53e01fb861f5e292b1e586763d4925c3 --- /dev/null +++ b/test_tipc/configs/AGCN/train_amp_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:AGCN +python:python3.7 +gpu_list:0 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:null +null:null +train_model_name:null +train_infer_video_dir:null +null:null +## +trainer:amp_train +norm_train:main.py -c configs/recognition/agcn/agcn_fsd.yaml --seed 1234 -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' -c configs/recognition/agcn/agcn_fsd.yaml --seed 1234 -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/agcn/agcn_fsd.yaml -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +-w:./test_tipc/output/AGCN/AGCN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/AGCN +-p:null +norm_export:tools/export_model.py -c configs/recognition/agcn/agcn_fsd.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/AGCN_fsd.pdparams +infer_export:tools/export_model.py -c configs/recognition/agcn/agcn_fsd.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/agcn/agcn_fsd.yaml +--use_gpu:True|False +--enable_mkldnn:False|True +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/fsd10/example_skeleton.npy +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[2, 350, 25, 1]}] diff --git a/test_tipc/configs/AGCN/train_infer_python.txt b/test_tipc/configs/AGCN/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..53497191b2fc9a9ed6c755ea0518fb26254ba6ae --- /dev/null +++ b/test_tipc/configs/AGCN/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:AGCN +python:python3.7 +gpu_list:0 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:null +null:null +train_model_name:null +train_infer_video_dir:null +null:null +## +trainer:norm_train +norm_train:main.py -c configs/recognition/agcn/agcn_fsd.yaml --seed 1234 -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/agcn/agcn_fsd.yaml -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +-w:./test_tipc/output/AGCN/AGCN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/AGCN +-p:null +norm_export:tools/export_model.py -c configs/recognition/agcn/agcn_fsd.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/AGCN_fsd.pdparams +infer_export:tools/export_model.py -c configs/recognition/agcn/agcn_fsd.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/agcn/agcn_fsd.yaml +--use_gpu:True|False +--enable_mkldnn:False|True +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/fsd10/example_skeleton.npy +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[2, 350, 25, 1]}] diff --git a/test_tipc/configs/AttentionLSTM/train_amp_infer_python.txt b/test_tipc/configs/AttentionLSTM/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..01ff8a23dc9a14d6c5fce92054be3c609fe736bb --- /dev/null +++ b/test_tipc/configs/AttentionLSTM/train_amp_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:AttentionLSTM +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:64 +null:null +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/yt8m/train_small.list' -o DATASET.valid.file_path='data/yt8m/train_small.list' -o DATASET.test.file_path='data/yt8m/train_small.list' +## +trainer:amp_train +norm_train:main.py --validate -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' --validate -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml --seed 1234 +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml +-w:./test_tipc/output/AttentionLSTM/AttentionLSTM_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/AttentionLSTM +-p:null +norm_export:tools/export_model.py -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/AttentionLSTM_yt8.pdparams +infer_export:tools/export_model.py -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.pkl +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[512, 1024]},{int32,[1]},{float32,[512, 1024]},{float32,[512, 128]},{int32,[1]},{float32,[512, 128]}] diff --git a/test_tipc/configs/AttentionLSTM/train_infer_python.txt b/test_tipc/configs/AttentionLSTM/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..0c43e5e61e168a6ce61903f8dcb55ff9cbdf0672 --- /dev/null +++ b/test_tipc/configs/AttentionLSTM/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:AttentionLSTM +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:64 +null:null +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/yt8m/train_small.list' -o DATASET.valid.file_path='data/yt8m/train_small.list' -o DATASET.test.file_path='data/yt8m/train_small.list' +## +trainer:norm_train +norm_train:main.py --validate -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml +-w:./test_tipc/output/AttentionLSTM/AttentionLSTM_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/AttentionLSTM +-p:null +norm_export:tools/export_model.py -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/AttentionLSTM_yt8.pdparams +infer_export:tools/export_model.py -c configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/attention_lstm/attention_lstm_youtube8m.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.pkl +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[512, 1024]},{int32,[1]},{float32,[512, 1024]},{float32,[512, 128]},{int32,[1]},{float32,[512, 128]}] diff --git a/test_tipc/configs/BMN/train_amp_infer_python.txt b/test_tipc/configs/BMN/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..a9c02b86e32c0caa603b33fd2ed5a05f4235961a --- /dev/null +++ b/test_tipc/configs/BMN/train_amp_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:BMN +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:16 +-o MODEL.backbone.pretrained:null +train_model_name:null +--profiler_options:null +-o DATASET.train.file_path:null +## +trainer:amp_train +norm_train:main.py --validate -c configs/localization/bmn.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' --validate -c configs/localization/bmn.yaml --seed 1234 +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/localization/bmn.yaml +-w:./test_tipc/output/BMN/BMN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/BMN +-p:null +norm_export:tools/export_model.py -c configs/localization/bmn.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/BMN.pdparams +infer_export:tools/export_model.py -c configs/localization/bmn.yaml +infer_quant:False +inference:tools/predict.py --config configs/localization/bmn.yaml +--use_gpu:True|False +--enable_mkldnn:False +--cpu_threads:1|6 +--batch_size:1 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example_feat.list +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:8 +fp_items:fp32 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[400, 100]}] diff --git a/test_tipc/configs/BMN/train_infer_python.txt b/test_tipc/configs/BMN/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..28b49a716050dc61ae5de4048150a804cb272087 --- /dev/null +++ b/test_tipc/configs/BMN/train_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:BMN +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:16 +-o MODEL.backbone.pretrained:null +train_model_name:null +--profiler_options:null +-o DATASET.train.file_path:null +## +trainer:norm_train +norm_train:main.py --validate -c configs/localization/bmn.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/localization/bmn.yaml +-w:./test_tipc/output/BMN/BMN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/BMN +-p:null +norm_export:tools/export_model.py -c configs/localization/bmn.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/BMN.pdparams +infer_export:tools/export_model.py -c configs/localization/bmn.yaml +infer_quant:False +inference:tools/predict.py --config configs/localization/bmn.yaml +--use_gpu:True|False +--enable_mkldnn:False +--cpu_threads:1|6 +--batch_size:1 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example_feat.list +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:8 +fp_items:fp32 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[400, 100]}] diff --git a/test_tipc/configs/PP-TSM/infer_cpp.txt b/test_tipc/configs/PP-TSM/infer_cpp.txt new file mode 100644 index 0000000000000000000000000000000000000000..ef65b19e5ecfafe0fa0a8d9a7908262c3cdc1421 --- /dev/null +++ b/test_tipc/configs/PP-TSM/infer_cpp.txt @@ -0,0 +1,18 @@ +===========================cpp_infer_params=========================== +model_name:PP-TSM +use_opencv:True +infer_model:./inference/ppTSM +infer_quant:False +inference:./deploy/cpp_infer/build/ppvideo rec +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--rec_batch_num:1 +--use_tensorrt:False|True +--precision:fp32|fp16 +--rec_model_dir: +--video_dir:./deploy/cpp_infer/example_video_dir +--inference_model_name:ppTSM +--benchmark:True +--char_list_file:data/k400/Kinetics-400_label_list.txt +--num_seg:8 diff --git a/test_tipc/configs/PP-TSM/train_amp_infer_python.txt b/test_tipc/configs/PP-TSM/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..d8a8c2f4944b4b143d43a39d8f7fe42095493b56 --- /dev/null +++ b/test_tipc/configs/PP-TSM/train_amp_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:PP-TSM +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:2 +-o MODEL.backbone.pretrained:'data/ResNet50_vd_ssld_v2_pretrained.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_frames.list' -o DATASET.valid.file_path='data/k400/val_small_frames.list' -o DATASET.test.file_path='data/k400/val_small_frames.list' +## +trainer:amp_train +norm_train:main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml --seed 1234 +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +-w:./test_tipc/output/ppTSM/ppTSM_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/ppTSM +-p:null +norm_export:tools/export_model.py -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/ppTSM_k400_uniform.pdparams +infer_export:tools/export_model.py -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[8, 3, 224, 224]}] diff --git a/test_tipc/configs/PP-TSM/train_infer_python.txt b/test_tipc/configs/PP-TSM/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..45691f69abf41a1d39e71d24fe0c3989836eece0 --- /dev/null +++ b/test_tipc/configs/PP-TSM/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:PP-TSM +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:2 +-o MODEL.backbone.pretrained:'data/ResNet50_vd_ssld_v2_pretrained.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_frames.list' -o DATASET.valid.file_path='data/k400/val_small_frames.list' -o DATASET.test.file_path='data/k400/val_small_frames.list' +## +trainer:norm_train +norm_train:main.py --validate -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +-w:./test_tipc/output/ppTSM/ppTSM_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/ppTSM +-p:null +norm_export:tools/export_model.py -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/ppTSM_k400_uniform.pdparams +infer_export:tools/export_model.py -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[8, 3, 224, 224]}] diff --git a/test_tipc/configs/PP-TSN/infer_cpp.txt b/test_tipc/configs/PP-TSN/infer_cpp.txt new file mode 100644 index 0000000000000000000000000000000000000000..32da79cde4cdc49ab9094d339e2840e61fcc76e1 --- /dev/null +++ b/test_tipc/configs/PP-TSN/infer_cpp.txt @@ -0,0 +1,18 @@ +===========================cpp_infer_params=========================== +model_name:PP-TSN +use_opencv:True +infer_model:./inference/ppTSN +infer_quant:False +inference:./deploy/cpp_infer/build/ppvideo rec +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--rec_batch_num:1 +--use_tensorrt:False|True +--precision:fp32|fp16 +--rec_model_dir: +--video_dir:./deploy/cpp_infer/example_video_dir +--inference_model_name:ppTSN +--benchmark:True +--char_list_file:data/k400/Kinetics-400_label_list.txt +--num_seg:25 diff --git a/test_tipc/configs/PP-TSN/train_amp_infer_python.txt b/test_tipc/configs/PP-TSN/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..46a9f4ac49ac34797d76ffd6d415f0cf7f4febce --- /dev/null +++ b/test_tipc/configs/PP-TSN/train_amp_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:PP-TSN +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:2 +-o MODEL.backbone.pretrained:'data/ResNet50_vd_ssld_v2_pretrained.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_videos.list' -o DATASET.valid.file_path='data/k400/val_small_videos.list' -o DATASET.test.file_path='data/k400/val_small_videos.list' +## +trainer:amp_train +norm_train:main.py --validate -c configs/recognition/pptsn/pptsn_k400_videos.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' --validate -c configs/recognition/pptsn/pptsn_k400_videos.yaml --seed 1234 +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/pptsn/pptsn_k400_videos.yaml +-w:./test_tipc/output/ppTSN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/ppTSN +-p:null +norm_export:tools/export_model.py -c configs/recognition/pptsn/pptsn_k400_videos.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/ppTSN_k400.pdparams +infer_export:tools/export_model.py -c configs/recognition/pptsn/pptsn_k400_videos.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/pptsn/pptsn_k400_videos.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[250, 3, 224, 224]}] diff --git a/test_tipc/configs/PP-TSN/train_infer_python.txt b/test_tipc/configs/PP-TSN/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..711e8d3e0dbd74503535ca1b0a8530709f29902d --- /dev/null +++ b/test_tipc/configs/PP-TSN/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:PP-TSN +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:2 +-o MODEL.backbone.pretrained:'data/ResNet50_vd_ssld_v2_pretrained.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_videos.list' -o DATASET.valid.file_path='data/k400/val_small_videos.list' -o DATASET.test.file_path='data/k400/val_small_videos.list' +## +trainer:norm_train +norm_train:main.py --validate -c configs/recognition/pptsn/pptsn_k400_videos.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/pptsn/pptsn_k400_videos.yaml +-w:./test_tipc/output/ppTSN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/ppTSN +-p:null +norm_export:tools/export_model.py -c configs/recognition/pptsn/pptsn_k400_videos.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/ppTSN_k400.pdparams +infer_export:tools/export_model.py -c configs/recognition/pptsn/pptsn_k400_videos.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/pptsn/pptsn_k400_videos.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[250, 3, 224, 224]}] diff --git a/test_tipc/configs/STGCN/train_amp_infer_python.txt b/test_tipc/configs/STGCN/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..1af7312b6dad0d1d11d81060046b54b93842aeee --- /dev/null +++ b/test_tipc/configs/STGCN/train_amp_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:STGCN +python:python3.7 +gpu_list:0 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:null +null:null +train_model_name:null +train_infer_video_dir:null +null:null +## +trainer:amp_train +norm_train:main.py -c configs/recognition/stgcn/stgcn_fsd.yaml --seed 1234 -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' -c configs/recognition/stgcn/stgcn_fsd.yaml --seed 1234 -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/stgcn/stgcn_fsd.yaml -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +-w:./test_tipc/output/STGCN/STGCN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/STGCN +-p:null +norm_export:tools/export_model.py -c configs/recognition/stgcn/stgcn_fsd.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/STGCN_fsd.pdparams +infer_export:tools/export_model.py -c configs/recognition/stgcn/stgcn_fsd.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/stgcn/stgcn_fsd.yaml +--use_gpu:True|False +--enable_mkldnn:False|True +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/fsd10/example_skeleton.npy +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[2, 350, 25, 1]}] diff --git a/test_tipc/configs/STGCN/train_infer_python.txt b/test_tipc/configs/STGCN/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..f63a591bea492443199e3db37d0b7e1fd409975f --- /dev/null +++ b/test_tipc/configs/STGCN/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:STGCN +python:python3.7 +gpu_list:0 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:null +null:null +train_model_name:null +train_infer_video_dir:null +null:null +## +trainer:norm_train +norm_train:main.py -c configs/recognition/stgcn/stgcn_fsd.yaml --seed 1234 -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/stgcn/stgcn_fsd.yaml -o DATASET.train.file_path="data/fsd10/FSD_train_data.npy" -o DATASET.train.label_path="data/fsd10/FSD_train_label.npy" -o DATASET.test.file_path="data/fsd10/FSD_train_data.npy" +-w:./test_tipc/output/STGCN/STGCN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/STGCN +-p:null +norm_export:tools/export_model.py -c configs/recognition/stgcn/stgcn_fsd.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/STGCN_fsd.pdparams +infer_export:tools/export_model.py -c configs/recognition/stgcn/stgcn_fsd.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/stgcn/stgcn_fsd.yaml +--use_gpu:True|False +--enable_mkldnn:False|True +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/fsd10/example_skeleton.npy +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[2, 350, 25, 1]}] diff --git a/test_tipc/configs/SlowFast/train_amp_infer_python.txt b/test_tipc/configs/SlowFast/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..3134357960f1fc706fb3d68176a54105a6e9eaff --- /dev/null +++ b/test_tipc/configs/SlowFast/train_amp_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:SlowFast +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:null +-o MODEL.backbone.pretrained:null +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_videos.list' -o DATASET.valid.file_path='data/k400/val_small_videos.list' -o DATASET.test.file_path='data/k400/val_small_videos.list' +## +trainer:amp_train +norm_train:main.py --validate -c configs/recognition/slowfast/slowfast.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' --validate -c configs/recognition/slowfast/slowfast.yaml --seed 1234 +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/slowfast/slowfast.yaml +-w:./test_tipc/output/SlowFast/SlowFast_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/SlowFast +-p:null +norm_export:tools/export_model.py -c configs/recognition/slowfast/slowfast.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/slowfast_4x16.pdparams +infer_export:tools/export_model.py -c configs/recognition/slowfast/slowfast.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/slowfast/slowfast.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:8 +fp_items:fp32 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[3, 4, 256, 256]},{float32,[3, 32, 256, 256]}] diff --git a/test_tipc/configs/SlowFast/train_infer_python.txt b/test_tipc/configs/SlowFast/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..6c060a00b8dfe14765df5c873f0e70bedd3ff781 --- /dev/null +++ b/test_tipc/configs/SlowFast/train_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:SlowFast +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:null +-o MODEL.backbone.pretrained:null +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_videos.list' -o DATASET.valid.file_path='data/k400/val_small_videos.list' -o DATASET.test.file_path='data/k400/val_small_videos.list' +## +trainer:norm_train +norm_train:main.py --validate -c configs/recognition/slowfast/slowfast.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/slowfast/slowfast.yaml +-w:./test_tipc/output/SlowFast/SlowFast_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/SlowFast +-p:null +norm_export:tools/export_model.py -c configs/recognition/slowfast/slowfast.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/slowfast_4x16.pdparams +infer_export:tools/export_model.py -c configs/recognition/slowfast/slowfast.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/slowfast/slowfast.yaml +--use_gpu:True|False +--enable_mkldnn:False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:8 +fp_items:fp32 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[3, 4, 256, 256]},{float32,[3, 32, 256, 256]}] diff --git a/test_tipc/configs/TSM/train_amp_infer_python.txt b/test_tipc/configs/TSM/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..2f8d1c8f204f9b4390677e121e4f84e4ed9786d9 --- /dev/null +++ b/test_tipc/configs/TSM/train_amp_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:TSM +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:2 +-o MODEL.backbone.pretrained:'data/ResNet50_pretrain.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_frames.list' -o DATASET.valid.file_path='data/k400/val_small_frames.list' -o DATASET.test.file_path='data/k400/val_small_frames.list' +## +trainer:amp_train +norm_train:main.py --validate -c configs/recognition/tsm/tsm_k400_frames.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' --validate -c configs/recognition/tsm/tsm_k400_frames.yaml --seed 1234 +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/tsm/tsm_k400_frames.yaml +-w:./test_tipc/output/TSM/TSM_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/TSM +-p:null +norm_export:tools/export_model.py -c configs/recognition/tsm/tsm_k400_frames.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/TSM_k400.pdparams +infer_export:tools/export_model.py -c configs/recognition/tsm/tsm_k400_frames.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/tsm/tsm_k400_frames.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:30 +fp_items:fp32|fp16 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[8, 3, 224, 224]}] diff --git a/test_tipc/configs/TSM/train_infer_python.txt b/test_tipc/configs/TSM/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..d33b10ae4b289a7ba2c0c228d45ba041531cf435 --- /dev/null +++ b/test_tipc/configs/TSM/train_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:TSM +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:2 +-o MODEL.backbone.pretrained:'data/ResNet50_pretrain.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_frames.list' -o DATASET.valid.file_path='data/k400/val_small_frames.list' -o DATASET.test.file_path='data/k400/val_small_frames.list' +## +trainer:norm_train +norm_train:main.py --validate -c configs/recognition/tsm/tsm_k400_frames.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/tsm/tsm_k400_frames.yaml +-w:./test_tipc/output/TSM/TSM_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/TSM +-p:null +norm_export:tools/export_model.py -c configs/recognition/tsm/tsm_k400_frames.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/TSM_k400.pdparams +infer_export:tools/export_model.py -c configs/recognition/tsm/tsm_k400_frames.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/tsm/tsm_k400_frames.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:30 +fp_items:fp32|fp16 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[8, 3, 224, 224]}] diff --git a/test_tipc/configs/TSN/train_amp_infer_python.txt b/test_tipc/configs/TSN/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..b1b96b997d3b4c4584efbb320df275985e391a82 --- /dev/null +++ b/test_tipc/configs/TSN/train_amp_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:TSN +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:2 +-o MODEL.backbone.pretrained:'data/ResNet50_pretrain.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_frames.list' -o DATASET.valid.file_path='data/k400/val_small_frames.list' -o DATASET.test.file_path='data/k400/val_small_frames.list' +## +trainer:amp_train +norm_train:main.py --validate -c configs/recognition/tsn/tsn_k400_frames.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' --validate -c configs/recognition/tsn/tsn_k400_frames.yaml --seed 1234 +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/tsn/tsn_k400_frames.yaml +-w:./test_tipc/output/TSN/TSN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/TSN +-p:null +norm_export:tools/export_model.py -c configs/recognition/tsn/tsn_k400_frames.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/TSN_k400.pdparams +infer_export:tools/export_model.py -c configs/recognition/tsn/tsn_k400_frames.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/tsn/tsn_k400_frames.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:32 +fp_items:fp32 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[250, 3, 224, 224]}] diff --git a/test_tipc/configs/TSN/train_infer_python.txt b/test_tipc/configs/TSN/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..5b7ca90450ac615ca2460c358076abeb7c5823ec --- /dev/null +++ b/test_tipc/configs/TSN/train_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:TSN +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:2 +-o MODEL.backbone.pretrained:'data/ResNet50_pretrain.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_frames.list' -o DATASET.valid.file_path='data/k400/val_small_frames.list' -o DATASET.test.file_path='data/k400/val_small_frames.list' +## +trainer:norm_train +norm_train:main.py --validate -c configs/recognition/tsn/tsn_k400_frames.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/tsn/tsn_k400_frames.yaml +-w:./test_tipc/output/TSN/TSN_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/TSN +-p:null +norm_export:tools/export_model.py -c configs/recognition/tsn/tsn_k400_frames.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/TSN_k400.pdparams +infer_export:tools/export_model.py -c configs/recognition/tsn/tsn_k400_frames.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/tsn/tsn_k400_frames.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False|True +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:32 +fp_items:fp32 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[250, 3, 224, 224]}] diff --git a/test_tipc/configs/TimeSformer/train_amp_infer_python.txt b/test_tipc/configs/TimeSformer/train_amp_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..84d5e45028f477a89afd02d0275665e53537a959 --- /dev/null +++ b/test_tipc/configs/TimeSformer/train_amp_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:TimeSformer +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:null +-o MODEL.backbone.pretrained:'data/ViT_base_patch16_224_pretrained.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_videos.list' -o DATASET.valid.file_path='data/k400/val_small_videos.list' -o DATASET.test.file_path='data/k400/val_small_videos.list' +## +trainer:amp_train +norm_train:main.py --validate -c configs/recognition/timesformer/timesformer_k400_videos.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +amp_train:main.py --amp --amp_level='O2' --validate -c configs/recognition/timesformer/timesformer_k400_videos.yaml --seed 1234 +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/timesformer/timesformer_k400_videos.yaml +-w:./test_tipc/output/TimeSformer/TimeSformer_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/TimeSformer +-p:null +norm_export:tools/export_model.py -c configs/recognition/timesformer/timesformer_k400_videos.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/TimeSformer_k400.pdparams +infer_export:tools/export_model.py -c configs/recognition/timesformer/timesformer_k400_videos.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/timesformer/timesformer_k400_videos.yaml +--use_gpu:True|False +--enable_mkldnn:True|False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:1|14 +fp_items:fp32 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[3, 24, 224, 224]}] diff --git a/test_tipc/configs/TimeSformer/train_infer_python.txt b/test_tipc/configs/TimeSformer/train_infer_python.txt new file mode 100644 index 0000000000000000000000000000000000000000..c9dffd925649dc5b5e3ed5081faff58a1baeac53 --- /dev/null +++ b/test_tipc/configs/TimeSformer/train_infer_python.txt @@ -0,0 +1,59 @@ +===========================train_params=========================== +model_name:TimeSformer +python:python3.7 +gpu_list:0|0,1 +Global.use_gpu:null|null +Global.auto_cast:null +-o epochs:2 +-o output_dir:null +-o DATASET.batch_size:null +-o MODEL.backbone.pretrained:'data/ViT_base_patch16_224_pretrained.pdparams' +train_model_name:null +train_infer_video_dir:null +-o DATASET.train.file_path:'data/k400/train_small_videos.list' -o DATASET.valid.file_path='data/k400/val_small_videos.list' -o DATASET.test.file_path='data/k400/val_small_videos.list' +## +trainer:norm_train +norm_train:main.py --validate -c configs/recognition/timesformer/timesformer_k400_videos.yaml --seed 1234 +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:main.py --test -c configs/recognition/timesformer/timesformer_k400_videos.yaml +-w:./test_tipc/output/TimeSformer/TimeSformer_epoch_00001.pdparams +## +===========================infer_params=========================== +-o:inference/TimeSformer +-p:null +norm_export:tools/export_model.py -c configs/recognition/timesformer/timesformer_k400_videos.yaml --save_name inference +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +inference_dir:null +infer_model:./data/TimeSformer_k400.pdparams +infer_export:tools/export_model.py -c configs/recognition/timesformer/timesformer_k400_videos.yaml +infer_quant:False +inference:tools/predict.py --config configs/recognition/timesformer/timesformer_k400_videos.yaml +--use_gpu:True|False +--enable_mkldnn:False +--cpu_threads:1|6 +--batch_size:1|2 +--use_tensorrt:False +--precision:fp32|fp16 +--model_file:inference.pdmodel +--input_file:./data/example.avi +null:null +--enable_benchmark:True +--params_file:inference.pdiparams +===========================train_benchmark_params========================== +batch_size:1|14 +fp_items:fp32 +epoch:1 +--profiler_options:batch_range=[10,20];state=GPU;tracer_option=Default;profile_path=model.profile +flags:FLAGS_conv_workspace_size_limit=800 +===========================infer_benchmark_params========================== +random_infer_input:[{float32,[3, 24, 224, 224]}] diff --git a/test_tipc/docs/Video_TIPC.png b/test_tipc/docs/Video_TIPC.png new file mode 100644 index 0000000000000000000000000000000000000000..5031baee8b77de418a3433dbfa07c4b9e021e3e9 Binary files /dev/null and b/test_tipc/docs/Video_TIPC.png differ diff --git a/test_tipc/docs/benchmark_train.md b/test_tipc/docs/benchmark_train.md new file mode 100644 index 0000000000000000000000000000000000000000..debbcdb2de018815d350f0ffdfb682c0ca16946f --- /dev/null +++ b/test_tipc/docs/benchmark_train.md @@ -0,0 +1,82 @@ + +# TIPC Linux端Benchmark测试文档 + +该文档为Benchmark测试说明,Benchmark预测功能测试的主程序为`benchmark_train.sh`,用于验证监控模型训练的性能。 + +# 1. 测试流程 +## 1.1 准备数据和环境安装 +运行`test_tipc/prepare.sh`,完成训练数据准备和安装环境流程(以TSM模型为例)。 + +```shell +# 运行格式:bash test_tipc/prepare.sh train_benchmark.txt mode +bash test_tipc/prepare.sh test_tipc/configs/TSM/train_infer_python.txt benchmark_train +``` + +## 1.2 功能测试 +执行`test_tipc/benchmark_train.sh`,完成模型训练和日志解析(以TSM模型为例)。 + +```shell +# 运行格式:bash test_tipc/benchmark_train.sh train_benchmark.txt mode +bash test_tipc/benchmark_train.sh test_tipc/configs/TSM/train_infer_python.txt benchmark_train + +``` + +`test_tipc/benchmark_train.sh`支持根据传入的第三个配置参数实现只运行某一个训练配置,如下(以TSM模型为例): +```shell +# 运行格式:bash test_tipc/benchmark_train.sh train_benchmark.txt mode config_pram +## 动态图, batchsize=30, fp32, 数据并行模式, 单机单卡训练配置 +bash test_tipc/benchmark_train.sh test_tipc/configs/TSM/train_infer_python.txt benchmark_train dynamic_bs30_fp32_DP_N1C1 +## 动态图, batchsize=30, fp16, 数据并行模式, 单机4卡训练配置 +bash test_tipc/benchmark_train.sh test_tipc/configs/TSM/train_infer_python.txt benchmark_train dynamic_bs30_fp16_DP_N1C4 +``` +dynamic_bs30_fp16_DP_N1C4/benchmark_train.sh传入的参数,格式如下: + +`${modeltype}_${batch_size}_${fp_item}_${run_mode}_${device_num}` + +包含的信息有:模型类型、batchsize大小、训练精度如fp32,fp16等、分布式运行模式以及分布式训练使用的机器信息如单机单卡(N1C1)。 + + +## 2. 日志输出 + +运行后将会把修改的配置文件临时保存到`test_tipc/benchmark_train.txt`,然后使用该临时文件进行训练与分析,并保存模型的训练日志和解析日志。 + +如TSM模型某一参数文件的训练日志解析结果是: + +```json +{ + "model_branch": "tipc_benchmark", + "model_commit": "c8f93c7fd9908391371bcccf36a4db4398c49777", + "model_name": "TSM_bs1_fp16_MultiP_DP", + "batch_size": 1, + "fp_item": "fp16", + "run_process_type": "MultiP", + "run_mode": "DP", + "convergence_value": 0, + "convergence_key": "loss:", + "ips": 40.237, + "speed_unit": "instance/sec", + "device_num": "N1C4", + "model_run_time": "28", + "frame_commit": "828f87aecd8a47d19f19f0a83155f8dd340eeaa9", + "frame_version": "0.0.0" +} +``` + +训练日志和日志解析结果保存在4个目录下,文件组织格式如下(以TSM模型为例): +``` +PaddleVideo +├── train_log +│ ├── PaddleVideo_TSM_bs1_fp16_MultiP_DP_N1C4_log +│   ├── PaddleVideo_TSM_bs1_fp32_MultiP_DP_N1C4_log +│ +├── index +│   ├── PaddleVideo_TSM_bs1_fp16_MultiP_DP_N1C4_speed +│   ├── PaddleVideo_TSM_bs1_fp32_MultiP_DP_N1C4_speed +│ +├── profiling_log +│   ├── PaddleVideo_TSM_bs1_fp32_SingleP_DP_N1C1_profiling +│   ├── PaddleVideo_TSM_bs1_fp32_SingleP_DP_N1C1_profiling +│ +├── benchmark_log + └── results.log +``` diff --git a/test_tipc/docs/guide.png b/test_tipc/docs/guide.png new file mode 100644 index 0000000000000000000000000000000000000000..319ac819daff38ed77e84cdff2b122e8bc4a8e5f Binary files /dev/null and b/test_tipc/docs/guide.png differ diff --git a/test_tipc/docs/install.md b/test_tipc/docs/install.md new file mode 100644 index 0000000000000000000000000000000000000000..e9c32d7f5e12a5bb901a5f630bf7945b374eca89 --- /dev/null +++ b/test_tipc/docs/install.md @@ -0,0 +1,127 @@ +## 1. 环境准备 + +本教程适用于test_tipc目录下基础功能测试的运行环境搭建。 + +推荐环境: +- CUDA 10.2 +- CUDNN 7.6.5 +- TensorRT 7.0.0.11 + +环境配置可以选择docker镜像安装,或者在本地环境Python搭建环境。推荐使用docker镜像安装,避免不必要的环境配置。 + +## 2. Docker 镜像安装 + +推荐docker镜像安装,按照如下命令创建镜像,当前目录映射到镜像中的`/paddle`目录下 +```bash +nvidia-docker run --name paddle -it -v $PWD:/paddle paddlepaddle/paddle:2.0.0rc0-gpu-cuda10.2-cudnn7 /bin/bash +cd /paddle + +# 安装带TRT的paddle +python3.7 -m pip install https://paddle-wheel.bj.bcebos.com/with-trt/2.1.1-gpu-cuda10.1-cudnn7-mkl-gcc8.2/paddlepaddle_gpu-2.1.1.post101-cp37-cp37m-linux_x86_64.whl +``` + +## 3. Python 环境构建 + +推荐环境配置: +- CUDA10.2 + CUDNN7.6 + TensorRT 7 + +下面以CUDA10.2 + CUDNN7.6 + TensorRT 7配置为例,介绍环境配置的流程。 + +### 3.1 安装CUDNN + +如果当前环境满足CUDNN版本的要求,可以跳过此步骤。 + +以CUDNN8.1 安装安装为例,安装步骤如下,首先下载CUDNN,从[Nvidia官网](https://developer.nvidia.com/rdp/cudnn-archive)下载CUDNN 7.6.5版本,下载符合当前系统版本的三个deb文件,分别是: +- cuDNN Runtime Library ,如:[libcudnn7_7.6.5.32-1+cuda10.2_amd64.deb](https://developer.nvidia.com/compute/machine-learning/cudnn/secure/7.6.5.32/Production/10.2_20191118/Ubuntu16_04-x64/libcudnn7_7.6.5.32-1%2Bcuda10.2_amd64.deb) +- cuDNN Developer Library ,如:[libcudnn7-dev_7.6.5.32-1+cuda10.2_amd64.deb](https://developer.nvidia.com/compute/machine-learning/cudnn/secure/7.6.5.32/Production/10.2_20191118/Ubuntu16_04-x64/libcudnn7-dev_7.6.5.32-1%2Bcuda10.2_amd64.deb) +- cuDNN Code Samples,如:[libcudnn7-doc_7.6.5.32-1+cuda10.2_amd64.deb](https://developer.nvidia.com/compute/machine-learning/cudnn/secure/7.6.5.32/Production/10.2_20191118/Ubuntu16_04-x64/libcudnn7-doc_7.6.5.32-1%2Bcuda10.2_amd64.deb) + +deb安装可以参考[官方文档](https://docs.nvidia.com/deeplearning/cudnn/install-guide/index.html#installlinux-deb),安装方式如下 +```bash +# x.x.x表示下载的版本号 +# $HOME为工作目录 +sudo dpkg -i libcudnn7_x.x.x-1+cudax.x_arm64.deb +sudo dpkg -i libcudnn7-dev_7.x.x.x-1+cudax.x_arm64.deb +sudo dpkg -i libcudnn7-doc_7.x.x.x-1+cudax.x_arm64.deb + +# 验证是否正确安装 +cp -r /usr/src/cudnn_samples_v7/ $HOME +cd $HOME/cudnn_samples_v7/mnistCUDNN + +# 编译 +make clean && make +./mnistCUDNN +``` +如果运行mnistCUDNN完后提示运行成功,则表示安装成功。如果运行后出现freeimage相关的报错,需要按照提示安装freeimage库: +```bash +sudo apt-get install libfreeimage-dev +sudo apt-get install libfreeimage +``` + +### 3.2 安装TensorRT + +首先,从[Nvidia官网TensorRT板块](https://developer.nvidia.com/tensorrt-getting-started)下载TensorRT,这里选择7.0.0.11版本的TensorRT,注意选择适合自己系统版本和CUDA版本的TensorRT,另外建议**下载TAR package的安装包**。 + +以Ubuntu16.04+CUDA10.2为例,下载并解压后可以参考[官方文档](https://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-713/install-guide/index.html#installing-tar)的安装步骤,按照如下步骤安装: +```bash +# 以下安装命令中 '${version}' 为下载的TensorRT版本,如7.0.0.11 +# 设置环境变量, 为解压后的TensorRT的lib目录 +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH: + +# 安装TensorRT +cd TensorRT-${version}/python +python3.7 -m pip install tensorrt-*-cp37-none-linux_x86_64.whl + +# 安装graphsurgeon +cd TensorRT-${version}/graphsurgeon +python3.7 -m pip install graphsurgeon-0.4.1-py2.py3-none-any.whl +``` + + +### 3.3 安装PaddlePaddle + +下载支持TensorRT版本的Paddle安装包,下载[链接](https://paddleinference.paddlepaddle.org.cn/user_guides/download_lib.html#python) +选择下载 linux-cuda10.1-trt6-gcc8.2 Python3.7版本的Paddle: + +```bash +# 从下载链接中可以看到是paddle2.1.1-cuda10.2-cudnn8.1版本 +wget https://paddle-wheel.bj.bcebos.com/with-trt/2.1.1-gpu-cuda10.1-cudnn7-mkl-gcc8.2/paddlepaddle_gpu-2.1.1.post101-cp37-cp37m-linux_x86_64.whl +python3.7 -m pip install -U paddlepaddle_gpu-2.1.1.post101-cp37-cp37m-linux_x86_64.whl +``` + +## 4. 安装PaddleVideo依赖 +```bash +# 安装AutoLog +git clone https://github.com/LDOUBLEV/AutoLog +cd AutoLog +python3.7 -m pip install -r requirements.txt +python3.7 setup.py bdist_wheel +python3.7 -m pip install ./dist/auto_log-1.0.0-py3-none-any.whl + +# 克隆PaddleVideo代码 +cd ../ +git clone https://github.com/PaddlePaddle/PaddleVideo.git + +``` + +安装PaddleVideo依赖: +```bash +cd PaddleVideo +python3.7 -m pip install -r requirements.txt +``` + +## FAQ : +Q. You are using Paddle compiled with TensorRT, but TensorRT dynamic library is not found. Ignore this if TensorRT is not needed. + +A. 问题一般是当前安装paddle版本带TRT,但是本地环境找不到TensorRT的预测库,需要下载TensorRT库,解压后设置环境变量LD_LIBRARY_PATH; +如: + +```bash +export PATH=$PATH:/usr/local/cuda-10.2/bin +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda-10.2/lib64 +export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/cuda-10.2/lib64 +source /etc/profile +export LD_LIBRARY_PATH=/xx/xx/TensorRT-7.0.0.11/lib:$LD_LIBRARY_PATH +``` +或者问题是下载的TensorRT版本和当前paddle中编译的TRT版本不匹配,需要下载版本相符的TensorRT重新安装。 + diff --git a/test_tipc/docs/test_inference_cpp.md b/test_tipc/docs/test_inference_cpp.md new file mode 100644 index 0000000000000000000000000000000000000000..0a060e6ecbc93381b50b4f13253d62504bef120d --- /dev/null +++ b/test_tipc/docs/test_inference_cpp.md @@ -0,0 +1,95 @@ +# C++预测功能测试 + +C++预测功能测试的主程序为`test_inference_cpp.sh`,可以测试基于C++预测库的模型推理功能。 + +## 1. 测试结论汇总 + +基于训练是否使用量化,进行本测试的模型可以分为`正常模型`和`量化模型`(TODO),这两类模型对应的C++预测功能汇总如下: + +| 模型类型 |device | batchsize | tensorrt | mkldnn | cpu多线程 | +| ---- | ---- | ---- | :----: | :----: | :----: | +| 正常模型 | GPU | 1/6 | fp32/fp16 | - | - | +| 正常模型 | CPU | 1/6 | - | fp32 | 支持 | + +## 2. 测试流程 +运行环境配置请参考[文档](./install.md)的内容配置TIPC的运行环境。 + +### 2.1 功能测试 +先运行`prepare.sh`准备数据和模型,然后运行`test_inference_cpp.sh`进行测试,最终在```test_tipc/output```目录下生成`cpp_infer_*.log`后缀的日志文件。 + +```bash +bash test_tipc/prepare.sh test_tipc/configs/PP-TSM/PP-TSM_infer_cpp.txt 'cpp_infer' +``` +```bash +# 用法1: +bash test_tipc/test_inference_cpp.sh test_tipc/configs/PP-TSM/PP-TSM_infer_cpp.txt +# 用法2: 指定GPU卡预测,第三个传入参数为GPU卡号 +bash test_tipc/test_inference_cpp.sh test_tipc/configs/PP-TSM/PP-TSM_infer_cpp.txt 1 +``` + +运行预测指令后,在`test_tipc/output`文件夹下自动会保存运行日志,包括以下文件: + +```shell +test_tipc/PP-TSM/output/ + ├── results_cpp.log # 运行指令状态的日志 + ├── cpp_infer_cpu_usemkldnn_False_threads_1_precision_fp32_batchsize_1.log # CPU上不开启Mkldnn,线程数设置为1,测试batch_size=1条件下的预测运行日志 + ├── cpp_infer_cpu_usemkldnn_False_threads_6_precision_fp32_batchsize_1.log # CPU上不开启Mkldnn,线程数设置为6,测试batch_size=1条件下的预测运行日志 + ├── cpp_infer_gpu_usetrt_False_precision_fp32_batchsize_1.log # GPU上不开启TensorRT,测试batch_size=1的fp32精度预测日志 + ├── cpp_infer_gpu_usetrt_True_precision_fp16_batchsize_1.log # GPU上开启TensorRT,测试batch_size=1的fp16精度预测日志 +...... +``` +其中results_cpp.log中包含了每条指令的运行状态,如果运行成功会输出: + +``` +Run successfully with command - ./deploy/cpp_infer/build/ppvideo rec --use_gpu=True --use_tensorrt=False --precision=fp32 --rec_model_dir=./inference/ppTSM --rec_batch_num=1 --video_dir=./deploy/cpp_infer/example_video_dir --benchmark=True --inference_model_name=ppTSM --char_list_file=data/k400/Kinetics-400_label_list.txt --num_seg=8 > ./test_tipc/output/PP-TSM/cpp_infer_gpu_usetrt_False_precision_fp32_batchsize_1.log 2>&1 +...... +``` +如果运行失败,会输出: +``` +Run failed with command - ./deploy/cpp_infer/build/ppvideo rec --use_gpu=False --enable_mkldnn=False --cpu_threads=1 --rec_model_dir=./inference/ppTSM --rec_batch_num=1 --video_dir=./deploy/cpp_infer/example_video_dir --benchmark=True --inference_model_name=ppTSM --char_list_file=data/k400/Kinetics-400_label_list.txt --num_seg=8 > ./test_tipc/output/PP-TSM/cpp_infer_cpu_usemkldnn_False_threads_1_precision_fp32_batchsize_1.log 2>&1 +...... +``` +可以很方便的根据results_cpp.log中的内容判定哪一个指令运行错误。 + + +### 2.2 精度测试 + +使用compare_results.py脚本比较模型预测的结果是否符合预期,主要步骤包括: +- 提取预测输出文本的结果 +- 提取本地参考输出文本结果 +- 比较上述两个结果是否符合精度预期,误差大于设置阈值时会报错。 + +#### 使用方式 +运行命令: +```shell +python3.7 test_tipc/compare_results.py --gt_file "test_tipc/results/PP-TSM_CPP/cpp_ppvideo_PP-TSM_results_fp*.txt" --log_file "test_tipc/output/PP-TSM/cpp_infer_*.log" --atol=1e-3 --rtol=1e-3 +``` + +参数介绍: +- gt_file: 指向事先保存好的预测结果路径,支持*.txt 结尾,会自动索引*.txt格式的文件,文件默认保存在test_tipc/result/ 文件夹下 +- log_file: 指向运行test_tipc/test_inference_cpp.sh 脚本的infer模式保存的预测日志,预测日志中打印的有预测结果,比如:文本框,预测文本,类别等等,同样支持cpp_infer_*.log格式传入 +- atol: 设置的绝对误差 +- rtol: 设置的相对误差 + +#### 运行结果 + +正常运行输出示例: +```bash +Assert allclose passed! The results of cpp_infer_cpu_usemkldnn_True_threads_1_precision_fp32_batchsize_1.log and test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt are consistent! +Assert allclose passed! The results of cpp_infer_cpu_usemkldnn_False_threads_1_precision_fp32_batchsize_1.log and test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt are consistent! +Assert allclose passed! The results of cpp_infer_gpu_usetrt_True_precision_fp16_batchsize_1.log and test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp16.txt are consistent! +Assert allclose passed! The results of cpp_infer_gpu_usetrt_False_precision_fp32_batchsize_1.log and test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt are consistent! +Assert allclose passed! The results of cpp_infer_cpu_usemkldnn_True_threads_6_precision_fp32_batchsize_1.log and test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt are consistent! +Assert allclose passed! The results of cpp_infer_cpu_usemkldnn_False_threads_6_precision_fp32_batchsize_1.log and test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt are consistent! +Assert allclose passed! The results of cpp_infer_gpu_usetrt_True_precision_fp32_batchsize_1.log and test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt are consistent! +``` + +出现不一致结果时的运行输出示例: +```bash +ValueError: The results of cpp_infer_cpu_usemkldnn_True_threads_1_precision_fp32_batchsize_1.log and the results of test_tipc/results/PP-TSM_CPP/cpp_ppvideo_PP-TSM_results_fp32.txt are inconsistent! +``` + + +## 3. 更多教程 + +本文档为功能测试用,更详细的C++预测使用教程请参考:[服务器端C++预测](../../deploy/cpp_infer/readme.md) diff --git a/test_tipc/docs/test_train_amp_inference_python.md b/test_tipc/docs/test_train_amp_inference_python.md new file mode 100644 index 0000000000000000000000000000000000000000..efc167e1c5128f533cb2c98c91e4b7a9b1844f44 --- /dev/null +++ b/test_tipc/docs/test_train_amp_inference_python.md @@ -0,0 +1,124 @@ +# Linux GPU/CPU 混合精度训练推理测试 + +Linux GPU/CPU 混合精度训练推理测试的主程序为`test_train_inference_python.sh`,可以测试基于Python的模型训练、评估、推理等基本功能。 + +## 1. 测试结论汇总 + +- 训练相关: + + | 算法名称 | 模型名称 | 单机单卡 | 单机多卡 | + | :---- | :---- | :---- | :---- | + | PP-TSM | pptsm_k400_frames_uniform | 混合精度训练 | 混合精度训练 | + | PP-TSN | pptsn_k400_videos | 混合精度训练 | 混合精度训练 | + | AGCN | agcn_fsd | 混合精度训练 | - | + | STGCN | stgcn_fsd | 混合精度训练 | - | + | TimeSformer | timesformer_k400_videos | 混合精度训练 | 混合精度训练 | + | SlowFast | slowfast | 混合精度训练 | 混合精度训练 | + | TSM | tsm_k400_frames | 混合精度训练 | 混合精度训练 | + | TSN | tsn_k400_frames | 混合精度训练 | 混合精度训练 | + | AttentionLSTM| attention_lstm_youtube8m | 混合精度训练 | 混合精度训练 | + | BMN | bmn | 混合精度训练 | 混合精度训练 | + + +- 推理相关: + + | 算法名称 | 模型名称 | device_CPU | device_GPU | batchsize | + | :---- | :---- | :---- | :---- | :---- | + | PP-TSM | pptsm_k400_frames_uniform | 支持 | 支持 | 1/2 | + | PP-TSN | pptsn_k400_videos | 支持 | 支持 | 1/2 | + | AGCN | agcn_fsd | 支持 | 支持 | 1/2 | + | STGCN | stgcn_fsd | 支持 | 支持 | 1/2 | + | TimeSformer | timesformer_k400_videos | 支持 | 支持 | 1/2 | + | SlowFast | slowfast | 支持 | 支持 | 1/2 | + | TSM | tsm_k400_frames | 支持 | 支持 | 1/2 | + | TSN | tsn_k400_frames | 支持 | 支持 | 1/2 | + | AttentionLSTM| attention_lstm_youtube8m | 支持 | 支持 | 1/2 | + | BMN | bmn | 支持 | 支持 | 1 | +## 2. 测试流程 + +### 2.1 准备环境 + + +- 安装PaddlePaddle:如果您已经安装了2.2或者以上版本的paddlepaddle,那么无需运行下面的命令安装paddlepaddle。 + ``` + # 需要安装2.2及以上版本的Paddle + # 安装GPU版本的Paddle + pip install paddlepaddle-gpu==2.2.0 + # 安装CPU版本的Paddle + pip install paddlepaddle==2.2.0 + ``` + +- 安装依赖 + ``` + pip install -r requirements.txt + ``` +- 安装AutoLog(规范化日志输出工具) + ``` + pip install https://paddleocr.bj.bcebos.com/libs/auto_log-1.2.0-py3-none-any.whl + ``` + +### 2.2 功能测试 + + +测试方法如下所示,希望测试不同的模型文件,只需更换为自己的参数配置文件,即可完成对应模型的测试。 + +```bash +bash test_tipc/test_train_inference_python.sh ${your_params_file_path} lite_train_lite_infer +``` + +以`PP-TSM`的`Linux GPU/CPU 混合精度(默认优化等级为O2)训练推理测试`为例,命令如下所示。 + +```bash +bash test_tipc/prepare.sh test_tipc/configs/PP-TSM/train_amp_infer_python.txt lite_train_lite_infer +``` + +```bash +bash test_tipc/test_train_inference_python.sh test_tipc/configs/PP-TSM/train_amp_infer_python.txt lite_train_lite_infer +``` + +输出结果如下,表示命令运行成功。 + +```bash +Run successfully with command - python3.7 main.py --amp --amp_level='O1' --validate -c configs/recognition/tsm/tsm_k400_frames.yaml --seed 1234 --max_iters=30 -o output_dir=./test_tipc/output/TSM/amp_train_gpus_0_autocast_null -o epochs=2 -o MODEL.backbone.pretrained='data/ResNet50_pretrain.pdparams' -o DATASET.batch_size=2 -o DATASET.train.file_path='data/k400/train_small_frames.list' -o DATASET.valid.file_path='data/k400/val_small_frames.list' -o DATASET.test.file_path='data/k400/val_small_frames.list' ! + +........ + +Run successfully with command - python3.7 tools/predict.py --config configs/recognition/tsm/tsm_k400_frames.yaml --use_gpu=False --enable_mkldnn=False --cpu_threads=6 --model_file=./test_tipc/output/TSM/amp_train_gpus_0,1_autocast_null/inference.pdmodel --batch_size=2 --input_file=./data/example.avi --enable_benchmark=False --precision=fp32 --params_file=./test_tipc/output/TSM/amp_train_gpus_0,1_autocast_null/inference.pdiparams > ./test_tipc/output/TSM/python_infer_cpu_usemkldnn_False_threads_6_precision_fp32_batchsize_2.log 2>&1 ! + +``` + +在开启benchmark选项时,可以得到测试的详细数据,包含运行环境信息(系统版本、CUDA版本、CUDNN版本、驱动版本),Paddle版本信息,参数设置信息(运行设备、线程数、是否开启内存优化等),模型信息(模型名称、精度),数据信息(batchsize、是否为动态shape等),性能信息(CPU/GPU的占用、运行耗时、预处理耗时、推理耗时、后处理耗时),内容如下所示: + +```log +[2022/03/18 12:01:21] root INFO: ---------------------- Env info ---------------------- +[2022/03/18 12:01:21] root INFO: OS_version: Ubuntu 16.04 +[2022/03/18 12:01:21] root INFO: CUDA_version: 10.2.89 +[2022/03/18 12:01:21] root INFO: CUDNN_version: 7.6.5 +[2022/03/18 12:01:21] root INFO: drivier_version: 440.64.00 +[2022/03/18 12:01:21] root INFO: ---------------------- Paddle info ---------------------- +[2022/03/18 12:01:21] root INFO: paddle_version: 0.0.0 +[2022/03/18 12:01:21] root INFO: paddle_commit: 6849d33b62cacccb27797375a212e37a47ca9484 +[2022/03/18 12:01:21] root INFO: log_api_version: 1.0 +[2022/03/18 12:01:21] root INFO: ----------------------- Conf info ----------------------- +[2022/03/18 12:01:21] root INFO: runtime_device: gpu +[2022/03/18 12:01:21] root INFO: ir_optim: True +[2022/03/18 12:01:21] root INFO: enable_memory_optim: True +[2022/03/18 12:01:21] root INFO: enable_tensorrt: False +[2022/03/18 12:01:21] root INFO: enable_mkldnn: False +[2022/03/18 12:01:21] root INFO: cpu_math_library_num_threads: 1 +[2022/03/18 12:01:21] root INFO: ----------------------- Model info ---------------------- +[2022/03/18 12:01:21] root INFO: model_name: ppTSM +[2022/03/18 12:01:21] root INFO: precision: fp32 +[2022/03/18 12:01:21] root INFO: ----------------------- Data info ----------------------- +[2022/03/18 12:01:21] root INFO: batch_size: 2 +[2022/03/18 12:01:21] root INFO: input_shape: dynamic +[2022/03/18 12:01:21] root INFO: data_num: 30 +[2022/03/18 12:01:21] root INFO: ----------------------- Perf info ----------------------- +[2022/03/18 12:01:21] root INFO: cpu_rss(MB): 2062.625, gpu_rss(MB): 2111.0, gpu_util: 100.0% +[2022/03/18 12:01:21] root INFO: total time spent(s): 5.5024 +[2022/03/18 12:01:21] root INFO: preprocess_time(ms): 247.8535, inference_time(ms): 26.6164, postprocess_time(ms): 0.6504 +``` + +该信息可以在运行log中查看,以`PP-TSM`为例,上述的log完整信息文件位置在`./test_tipc/output/PP-TSM/python_infer_gpu_usetrt_False_precision_fp32_batchsize_2.log`。 + +如果运行失败,也会在终端中输出运行失败的日志信息以及对应的运行命令。可以基于该命令,分析运行失败的原因。 diff --git a/test_tipc/docs/test_train_inference_python.md b/test_tipc/docs/test_train_inference_python.md new file mode 100644 index 0000000000000000000000000000000000000000..f1d220961b50d62b74cb532770dc8813ea8a02b9 --- /dev/null +++ b/test_tipc/docs/test_train_inference_python.md @@ -0,0 +1,131 @@ +# Linux端基础训练预测功能测试 + +Linux端基础训练预测功能测试的主程序为`test_train_inference_python.sh`,可以测试基于Python的模型训练、评估、推理等基本功能,包括裁剪(TODO)、量化(TODO)、蒸馏。 + +- Mac端基础训练预测功能测试参考[TODO]() +- Windows端基础训练预测功能测试参考[TODO]() + +## 1. 测试结论汇总 + +- 训练相关: + + | 算法名称 | 模型名称 | 单机单卡 | 单机多卡 | 多机多卡 | 模型压缩(单机多卡) | + | :---- | :---- | :---- | :---- | :---- | :---- | + | PP-TSM | pptsm_k400_frames_uniform | 正常训练 | 正常训练 | - | - | + | PP-TSN | pptsn_k400_videos | 正常训练 | 正常训练 | - | - | + | AGCN | agcn_fsd | 正常训练 | - | - | - | + | STGCN | stgcn_fsd | 正常训练 | - | - | - | + | TimeSformer | timesformer_k400_videos | 正常训练 | 正常训练 | - | - | + | SlowFast | slowfast | 正常训练 | 正常训练 | - | - | + | TSM | tsm_k400_frames | 正常训练 | 正常训练 | - | - | + | TSN | tsn_k400_frames | 正常训练 | 正常训练 | - | - | + | AttentionLSTM | attention_lstm_youtube8m | 正常训练 | 正常训练 | - | - | + | BMN | bmn | 正常训练 | 正常训练 | - | - | + + +- 预测相关:基于训练是否使用量化,可以将训练产出的模型可以分为`正常模型`和`量化模型(TODO)`,这两类模型对应的预测功能汇总如下, + + | 模型类型 |device | batchsize | tensorrt | mkldnn | cpu多线程 | + | ---- | ---- | ---- | :----: | :----: | :----: | + | 正常模型 | GPU | 1/2 | fp32/fp16 | - | 1/6 | + | 正常模型 | CPU | 1/2 | - | fp32/fp16 | 1/6 | + + +## 2. 测试流程 + +运行环境配置请参考[文档](./install.md)的内容配置TIPC的运行环境。 + +### 2.1 安装依赖 +- 安装对应软硬件环境下的PaddlePaddle(>=2.0) + +- 安装PaddleVideo依赖 + ``` + # 需在PaddleVideo目录下执行 + python3.7 -m pip install -r requirements.txt + ``` +- 安装autolog(规范化日志输出工具) + ``` + git clone https://github.com/LDOUBLEV/AutoLog + cd AutoLog + python3.7 -m pip install -r requirements.txt + python3 setup.py bdist_wheel + python3.7 -m pip install ./dist/auto_log-1.0.0-py3-none-any.whl + cd ../ + ``` +- 安装PaddleSlim (可选) + ``` + # 如果要测试量化、裁剪等功能,则需用以下命令安装PaddleSlim + python3.7 -m pip install paddleslim + ``` + + +### 2.2 基本功能测试 +1. 先运行`prepare.sh`,根据传入模型名字,准备对应数据和预训练模型参数 +2. 再运行`test_train_inference_python.sh`,根据传入模型名字,进行对应测试 +3. 在`test_tipc/output`目录下生成 `python_infer_*.log` 格式的日志文件 + +具体地,以PP-TSM的测试链条为例,运行细节如下: + +`test_train_inference_python.sh` 包含5种运行模式,每种模式的运行数据不同,分别用于测试速度和精度,分别是: + +- 模式1:**lite_train_lite_infer**,使用少量数据训练,用于快速验证训练到预测的走通流程,不验证精度和速度; + ```shell + bash test_tipc/prepare.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'lite_train_lite_infer' + bash test_tipc/test_train_inference_python.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'lite_train_lite_infer' + ``` + +- 模式2:**lite_train_whole_infer**,使用少量数据训练,一定量数据预测,用于验证训练后的模型执行预测,预测速度是否合理; + ```shell + bash test_tipc/prepare.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'lite_train_whole_infer' + bash test_tipc/test_train_inference_python.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'lite_train_whole_infer' + ``` + +- 模式3:**whole_infer**,不训练,全量数据预测,走通开源模型评估、动转静,检查inference model预测时间和精度; + ```shell + bash test_tipc/prepare.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'whole_infer' + # 用法1: + bash test_tipc/test_train_inference_python.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'whole_infer' + # 用法2: 指定GPU卡预测,第三个传入参数为GPU卡号 + bash test_tipc/test_train_inference_python.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'whole_infer' '1' + ``` + +- 模式4:**whole_train_whole_infer**: 全量数据训练,全量数据预测,验证模型训练精度,预测精度,预测速度; + ```shell + bash test_tipc/prepare.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'whole_train_whole_infer' + bash test_tipc/test_train_inference_python.sh test_tipc/configs/PP-TSM/train_infer_python.txt 'whole_train_whole_infer' + ``` + + +最终在`tests/output/model_name`目录下生成.log后缀的日志文件 + + +### 2.3 精度测试 + +使用compare_results.py脚本比较模型预测的结果是否符合预期,主要步骤包括: +- 提取`*.log`日志中的预测结果,包括类别和概率 +- 从本地文件中提取保存好的真值结果; +- 比较上述两个结果是否符合精度预期,误差大于设置阈值时会报错。 + +#### 使用方式 +运行命令: +```shell +python3.7 test_tipc/compare_results.py --gt_file="test_tipc/results/python_*.txt" --log_file="test_tipc/output/python_*.log" --atol=1e-3 --rtol=1e-3 +``` + +参数介绍: +- gt_file: 指向事先保存好的预测结果路径,支持*.txt 结尾,会自动索引*.txt格式的文件,文件默认保存在test_tipc/result/ 文件夹下 +- log_file: 指向运行test_tipc/test_train_inference_python.sh 脚本的infer模式保存的预测日志,预测日志中打印的有预测结果,比如:预测文本,类别等等,同样支持python_infer_*.log格式传入 +- atol: 设置的绝对误差 +- rtol: 设置的相对误差 + +#### 运行结果 + +正常运行效果如下: +```bash +Assert allclose passed! The results of python_infer_cpu_usemkldnn_False_threads_6_precision_fp32_batchsize_16.log and ./test_tipc/results/PP-TSM/python_ppvideo_PP-TSM_results_fp32.txt are consistent! +``` + +出现不一致结果时的样例输出如下: +```bash +ValueError: The results of python_infer_gpu_usetrt_False_precision_fp32_batchsize_8.log and the results of ./test_tipc/results/PP-TSM/python_ppvideo_PP-TSM_results_fp32.txt are inconsistent! +``` diff --git a/test_tipc/prepare.sh b/test_tipc/prepare.sh new file mode 100644 index 0000000000000000000000000000000000000000..d29e6ee32259f2089ca2d083f2fccf9612b3ceb4 --- /dev/null +++ b/test_tipc/prepare.sh @@ -0,0 +1,469 @@ +#!/bin/bash +source test_tipc/common_func.sh + +FILENAME=$1 + +# set -xe + +:< train_small.list # 将train*.pkl的路径写入train_small.list + ls pkl_frame/validate*.pkl > val_small.list # 将validate*.pkl的路径写入val_small.list + + ${python} split_yt8m.py train_small.list # 拆分每个train*.pkl变成多个train*_split*.pkl + ${python} split_yt8m.py val_small.list # 拆分每个validate*.pkl变成多个validate*_split*.pkl + + ls pkl_frame/train*_split*.pkl > train_small.list # 将train*_split*.pkl的路径重新写入train_small.list + ls pkl_frame/validate*_split*.pkl > val_small.list # 将validate*_split*.pkl的路径重新写入val_small.list + popd + elif [ ${model_name} == "SlowFast" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_videos_small.tar + tar -xf k400_videos_small.tar + popd + elif [ ${model_name} == "BMN" ]; then + # pretrain lite train data + pushd ./data + mkdir bmn_data + cd bmn_data + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/bmn_feat.tar.gz + tar -xf bmn_feat.tar.gz + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/activitynet_1.3_annotations.json + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/activity_net_1_3_new.json + popd + else + echo "Not added into TIPC yet." + fi + +elif [ ${MODE} = "whole_train_whole_infer" ];then + if [ ${model_name} == "PP-TSM" ]; then + # pretrain whole train data + pushd ./data/k400 + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/train_link.list + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/val_link.list + bash download_k400_data.sh train_link.list + bash download_k400_data.sh val_link.list + ${python} extract_rawframes.py ./videos/ ./rawframes/ --level 2 --ext mp4 # extract frames from video file + # download annotations + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train_frames.list + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val_frames.list + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams --no-check-certificate + elif [ ${model_name} == "PP-TSN" ]; then + # pretrain whole train data + pushd ./data/k400 + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/train_link.list + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/val_link.list + bash download_k400_data.sh train_link.list + bash download_k400_data.sh val_link.list + # download annotations + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train.list + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val.list + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams --no-check-certificate + elif [ ${model_name} == "AGCN" ]; then + # pretrain whole train data + pushd data/fsd10 + wget -nc https://videotag.bj.bcebos.com/Data/FSD_train_data.npy + wget -nc https://videotag.bj.bcebos.com/Data/FSD_train_label.npy + popd + elif [ ${model_name} == "STGCN" ]; then + # pretrain whole train data + pushd data/fsd10 + wget -nc https://videotag.bj.bcebos.com/Data/FSD_train_data.npy + wget -nc https://videotag.bj.bcebos.com/Data/FSD_train_label.npy + popd + elif [ ${model_name} == "TSM" ]; then + # pretrain whole train data + pushd ./data/k400 + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/train_link.list + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/val_link.list + bash download_k400_data.sh train_link.list + bash download_k400_data.sh val_link.list + ${python} extract_rawframes.py ./videos/ ./rawframes/ --level 2 --ext mp4 # extract frames from video file + # download annotations + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train_frames.list + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val_frames.list + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams --no-check-certificate + elif [ ${model_name} == "TSN" ]; then + # pretrain whole train data + pushd ./data/k400 + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/train_link.list + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/val_link.list + bash download_k400_data.sh train_link.list + bash download_k400_data.sh val_link.list + ${python} extract_rawframes.py ./videos/ ./rawframes/ --level 2 --ext mp4 # extract frames from video file + # download annotations + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train_frames.list + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val_frames.list + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams --no-check-certificate + elif [ ${model_name} == "TimeSformer" ]; then + # pretrain whole train data + pushd ./data/k400 + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/train_link.list + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/val_link.list + bash download_k400_data.sh train_link.list + bash download_k400_data.sh val_link.list + # download annotations + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train.list + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val.list + popd + # download pretrained weights + wget -nc -P ./data https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams --no-check-certificate + elif [ ${model_name} == "AttentionLSTM" ]; then + # pretrain whole train data + pushd data/yt8m + mkdir frame + cd frame + ## download & decompression training data + curl data.yt8m.org/download.py | partition=2/frame/train mirror=asia python + curl data.yt8m.org/download.py | partition=2/frame/validate mirror=asia python + ${python} -m pip install tensorflow-gpu==1.14.0 -i https://pypi.tuna.tsinghua.edu.cn/simple + cd .. + ${python} tf2pkl.py ./frame ./pkl_frame/ + ls pkl_frame/train*.pkl > train.list # 将train*.pkl的路径写入train.list + ls pkl_frame/validate*.pkl > val.list # 将validate*.pkl的路径写入val.list + + ${python} split_yt8m.py train.list # 拆分每个train*.pkl变成多个train*_split*.pkl + ${python} split_yt8m.py val.list # 拆分每个validate*.pkl变成多个validate*_split*.pkl + + ls pkl_frame/train*_split*.pkl > train.list # 将train*_split*.pkl的路径重新写入train.list + ls pkl_frame/validate*_split*.pkl > val.list # 将validate*_split*.pkl的路径重新写入val.list + popd + elif [ ${model_name} == "SlowFast" ]; then + # pretrain whole train data + pushd ./data/k400 + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/train_link.list + wget -nc https://ai-rank.bj.bcebos.com/Kinetics400/val_link.list + bash download_k400_data.sh train_link.list + bash download_k400_data.sh val_link.list + # download annotations + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/train.list + wget -nc https://videotag.bj.bcebos.com/PaddleVideo/Data/Kinetic400/val.list + popd + elif [ ${model_name} == "BMN" ]; then + # pretrain whole train data + pushd ./data + mkdir bmn_data + cd bmn_data + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/bmn_feat.tar.gz + tar -xf bmn_feat.tar.gz + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/activitynet_1.3_annotations.json + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/activity_net_1_3_new.json + popd + else + echo "Not added into TIPC yet." + fi +elif [ ${MODE} = "lite_train_whole_infer" ];then + if [ ${model_name} == "PP-TSM" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_rawframes_small.tar + tar -xf k400_rawframes_small.tar + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams --no-check-certificate + elif [ ${model_name} == "PP-TSN" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_videos_small.tar + tar -xf k400_videos_small.tar + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_vd_ssld_v2_pretrained.pdparams --no-check-certificate + elif [ ${model_name} == "AGCN" ]; then + # pretrain lite train data + pushd data/fsd10 + wget -nc https://videotag.bj.bcebos.com/Data/FSD_train_data.npy + wget -nc https://videotag.bj.bcebos.com/Data/FSD_train_label.npy + popd + elif [ ${model_name} == "STGCN" ]; then + # pretrain lite train data + pushd data/fsd10 + wget -nc https://videotag.bj.bcebos.com/Data/FSD_train_data.npy + wget -nc https://videotag.bj.bcebos.com/Data/FSD_train_label.npy + popd + elif [ ${model_name} == "TSM" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_rawframes_small.tar + tar -xf k400_rawframes_small.tar + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams --no-check-certificate + elif [ ${model_name} == "TSN" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_rawframes_small.tar + tar -xf k400_rawframes_small.tar + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams --no-check-certificate + elif [ ${model_name} == "TimeSformer" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_videos_small.tar + tar -xf k400_videos_small.tar + popd + # download pretrained weights + wget -nc -P ./data https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams --no-check-certificate + elif [ ${model_name} == "AttentionLSTM" ]; then + # pretrain lite train data + pushd data/yt8m + ## download & decompression training data + wget -nc https://videotag.bj.bcebos.com/Data/yt8m_rawframe_small.tar + tar -xf yt8m_rawframe_small.tar + ${python} -m pip install tensorflow-gpu==1.14.0 -i https://pypi.tuna.tsinghua.edu.cn/simple + ${python} tf2pkl.py ./frame ./pkl_frame/ + ls pkl_frame/train*.pkl > train_small.list # 将train*.pkl的路径写入train_small.list + ls pkl_frame/validate*.pkl > val_small.list # 将validate*.pkl的路径写入val_small.list + + ${python} split_yt8m.py train_small.list # 拆分每个train*.pkl变成多个train*_split*.pkl + ${python} split_yt8m.py val_small.list # 拆分每个validate*.pkl变成多个validate*_split*.pkl + + ls pkl_frame/train*_split*.pkl > train_small.list # 将train*_split*.pkl的路径重新写入train_small.list + ls pkl_frame/validate*_split*.pkl > val_small.list # 将validate*_split*.pkl的路径重新写入val_small.list + popd + elif [ ${model_name} == "SlowFast" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_videos_small.tar + tar -xf k400_videos_small.tar + popd + elif [ ${model_name} == "BMN" ]; then + # pretrain lite train data + pushd ./data + mkdir bmn_data + cd bmn_data + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/bmn_feat.tar.gz + tar -xf bmn_feat.tar.gz + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/activitynet_1.3_annotations.json + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/activity_net_1_3_new.json + popd + else + echo "Not added into TIPC yet." + fi +elif [ ${MODE} = "whole_infer" ];then + if [ ${model_name} = "PP-TSM" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_uniform.pdparams --no-check-certificate + elif [ ${model_name} = "PP-TSN" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSN_k400.pdparams --no-check-certificate + elif [ ${model_name} == "AGCN" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AGCN_fsd.pdparams --no-check-certificate + elif [ ${model_name} == "STGCN" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.2/STGCN_fsd.pdparams --no-check-certificate + elif [ ${model_name} == "TSM" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.1/TSM/TSM_k400.pdparams --no-check-certificate + elif [ ${model_name} == "TSN" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TSN_k400.pdparams --no-check-certificate + elif [ ${model_name} == "TimeSformer" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.2/TimeSformer_k400.pdparams --no-check-certificate + elif [ ${model_name} == "AttentionLSTM" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo-release2.2/AttentionLSTM_yt8.pdparams --no-check-certificate + elif [ ${model_name} == "SlowFast" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/SlowFast/SlowFast.pdparams --no-check-certificate + elif [ ${model_name} == "BMN" ]; then + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/BMN/BMN.pdparams --no-check-certificate + else + echo "Not added into TIPC yet." + fi +fi + +if [ ${MODE} = "benchmark_train" ];then + ${python} -m pip install -r requirements.txt + if [ ${model_name} == "PP-TSM" ]; then + echo "Not added into TIPC yet." + elif [ ${model_name} == "PP-TSN" ]; then + echo "Not added into TIPC yet." + elif [ ${model_name} == "AGCN" ]; then + echo "Not added into TIPC yet." + elif [ ${model_name} == "STGCN" ]; then + echo "Not added into TIPC yet." + elif [ ${model_name} == "TSM" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_rawframes_small.tar + tar -xf k400_rawframes_small.tar + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams --no-check-certificate + elif [ ${model_name} == "TSN" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_rawframes_small.tar + tar -xf k400_rawframes_small.tar + popd + # download pretrained weights + wget -nc -P ./data https://videotag.bj.bcebos.com/PaddleVideo/PretrainModel/ResNet50_pretrain.pdparams --no-check-certificate + elif [ ${model_name} == "TimeSformer" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_videos_small.tar + tar -xf k400_videos_small.tar + popd + # download pretrained weights + wget -nc -P ./data https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/ViT_base_patch16_224_pretrained.pdparams --no-check-certificate + elif [ ${model_name} == "AttentionLSTM" ]; then + echo "Not added into TIPC yet." + elif [ ${model_name} == "SlowFast" ]; then + # pretrain lite train data + pushd ./data/k400 + wget -nc https://videotag.bj.bcebos.com/Data/k400_videos_small.tar + tar -xf k400_videos_small.tar + popd + elif [ ${model_name} == "BMN" ]; then + # pretrain lite train data + pushd ./data + mkdir bmn_data + cd bmn_data + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/bmn_feat.tar.gz + tar -xf bmn_feat.tar.gz + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/activitynet_1.3_annotations.json + wget -nc https://paddlemodels.bj.bcebos.com/video_detection/activity_net_1_3_new.json + popd + else + echo "Not added into TIPC yet." + fi +fi + +if [ ${MODE} = "klquant_whole_infer" ]; then + echo "Not added into TIPC now." +fi + +if [ ${MODE} = "cpp_infer" ];then + # install required packages + apt-get update + apt install libavformat-dev + apt install libavcodec-dev + apt install libswresample-dev + apt install libswscale-dev + apt install libavutil-dev + apt install libsdl1.2-dev + apt-get install ffmpeg + + if [ ${model_name} = "PP-TSM" ]; then + # download pretrained weights + wget -nc -P data/ https://videotag.bj.bcebos.com/PaddleVideo-release2.1/PPTSM/ppTSM_k400_uniform.pdparams --no-check-certificate + # export inference model + ${python} tools/export_model.py -c configs/recognition/pptsm/pptsm_k400_frames_uniform.yaml -p data/ppTSM_k400_uniform.pdparams -o ./inference/ppTSM + elif [ ${model_name} = "PP-TSN" ]; then + # download pretrained weights + wget -nc -P data/ https://videotag.bj.bcebos.com/PaddleVideo-release2.2/ppTSN_k400.pdparams --no-check-certificate + # export inference model + ${python} tools/export_model.py -c configs/recognition/pptsn/pptsn_k400_videos.yaml -p data/ppTSN_k400.pdparams -o ./inference/ppTSN + else + echo "Not added into TIPC now." + fi +fi + +if [ ${MODE} = "serving_infer" ];then + echo "Not added into TIPC now." +fi + +if [ ${MODE} = "paddle2onnx_infer" ];then + echo "Not added into TIPC now." +fi diff --git a/test_tipc/results/AGCN/python_ppvideo_AGCN_results_fp16.txt b/test_tipc/results/AGCN/python_ppvideo_AGCN_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..aaae6a042d442678c2ce844e93ef5c76da653fa5 --- /dev/null +++ b/test_tipc/results/AGCN/python_ppvideo_AGCN_results_fp16.txt @@ -0,0 +1,3 @@ +Current video file: data/fsd10/example_skeleton.npy + top-1 class: 27 + top-1 score: 0.8965644240379333 diff --git a/test_tipc/results/AGCN/python_ppvideo_AGCN_results_fp32.txt b/test_tipc/results/AGCN/python_ppvideo_AGCN_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..aaae6a042d442678c2ce844e93ef5c76da653fa5 --- /dev/null +++ b/test_tipc/results/AGCN/python_ppvideo_AGCN_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: data/fsd10/example_skeleton.npy + top-1 class: 27 + top-1 score: 0.8965644240379333 diff --git a/test_tipc/results/AttentionLSTM/python_ppvideo_AttentionLSTM_results_fp16.txt b/test_tipc/results/AttentionLSTM/python_ppvideo_AttentionLSTM_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..5ecc6a423cac1029be093598a3b1ce0f35f1ede1 --- /dev/null +++ b/test_tipc/results/AttentionLSTM/python_ppvideo_AttentionLSTM_results_fp16.txt @@ -0,0 +1,3 @@ +Current video file: data/example.pkl + top-1 class: 11 + top-1 score: 0.9840923547744751 diff --git a/test_tipc/results/AttentionLSTM/python_ppvideo_AttentionLSTM_results_fp32.txt b/test_tipc/results/AttentionLSTM/python_ppvideo_AttentionLSTM_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..95457c0c960e8c908cf2999c1b8efc083774730c --- /dev/null +++ b/test_tipc/results/AttentionLSTM/python_ppvideo_AttentionLSTM_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: data/example.pkl + top-1 class: 11 + top-1 score: 0.9841002225875854 diff --git a/test_tipc/results/BMN/python_ppvideo_BMN_results_fp16.txt b/test_tipc/results/BMN/python_ppvideo_BMN_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..66c756eea572ded6e7001df9f6046240399b7875 --- /dev/null +++ b/test_tipc/results/BMN/python_ppvideo_BMN_results_fp16.txt @@ -0,0 +1,6 @@ +Current video file: data/example_feat.npy : +{'score': 0.7967117428779602, 'segment': [0.0, 122.9877]} +{'score': 0.49079903960227966, 'segment': [12.423000000000002, 124.23]} +{'score': 0.21400144696235657, 'segment': [39.7536, 122.9877]} +{'score': 0.210616335272789, 'segment': [0.0, 109.3224]} +{'score': 0.06873712688684464, 'segment': [23.6037, 114.2916]} diff --git a/test_tipc/results/BMN/python_ppvideo_BMN_results_fp32.txt b/test_tipc/results/BMN/python_ppvideo_BMN_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae9a11121ce14258de63e7b96c8468a492c2b00b --- /dev/null +++ b/test_tipc/results/BMN/python_ppvideo_BMN_results_fp32.txt @@ -0,0 +1,6 @@ +Current video file: data/example_feat.npy : +{'score': 0.7968077063560486, 'segment': [0.0, 122.9877]} +{'score': 0.49097612500190735, 'segment': [12.423000000000002, 124.23]} +{'score': 0.21395836770534515, 'segment': [39.7536, 122.9877]} +{'score': 0.2106524258852005, 'segment': [0.0, 109.3224]} +{'score': 0.06876271963119507, 'segment': [23.6037, 114.2916]} diff --git a/test_tipc/results/PP-TSM/python_ppvideo_PP-TSM_results_fp16.txt b/test_tipc/results/PP-TSM/python_ppvideo_PP-TSM_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..941f5225a901e3fe5ee4e626f500bfaf12bbc0fa --- /dev/null +++ b/test_tipc/results/PP-TSM/python_ppvideo_PP-TSM_results_fp16.txt @@ -0,0 +1,3 @@ +Current video file: ./data/example.avi + top-1 class: 5 + top-1 score: 0.990794837474823 diff --git a/test_tipc/results/PP-TSM/python_ppvideo_PP-TSM_results_fp32.txt b/test_tipc/results/PP-TSM/python_ppvideo_PP-TSM_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..6035dc3c511c3e19bd8e29ea09bbf5ba2460a221 --- /dev/null +++ b/test_tipc/results/PP-TSM/python_ppvideo_PP-TSM_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: ./data/example.avi + top-1 class: 5 + top-1 score: 0.990738570690155 diff --git a/test_tipc/results/PP-TSM_CPP/cpp_ppvideo_PP-TSM_results_fp16.txt b/test_tipc/results/PP-TSM_CPP/cpp_ppvideo_PP-TSM_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..14a02fc3f7b7fe9ea41360f67107244c16527e90 --- /dev/null +++ b/test_tipc/results/PP-TSM_CPP/cpp_ppvideo_PP-TSM_results_fp16.txt @@ -0,0 +1 @@ +./deploy/cpp_infer/example_video_dir/example01.avi class: 5 archery score: 0.988793 diff --git a/test_tipc/results/PP-TSM_CPP/cpp_ppvideo_PP-TSM_results_fp32.txt b/test_tipc/results/PP-TSM_CPP/cpp_ppvideo_PP-TSM_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..a43e1527a8e5afcd5b72326f02c0129bff93c756 --- /dev/null +++ b/test_tipc/results/PP-TSM_CPP/cpp_ppvideo_PP-TSM_results_fp32.txt @@ -0,0 +1 @@ +./deploy/cpp_infer/example_video_dir/example01.avi class: 5 archery score: 0.988699 diff --git a/test_tipc/results/PP-TSN/python_ppvideo_PP-TSN_results_fp16.txt b/test_tipc/results/PP-TSN/python_ppvideo_PP-TSN_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..57d820f8771e320303d36eb0a574cfa842791444 --- /dev/null +++ b/test_tipc/results/PP-TSN/python_ppvideo_PP-TSN_results_fp16.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9998562335968018 diff --git a/test_tipc/results/PP-TSN/python_ppvideo_PP-TSN_results_fp32.txt b/test_tipc/results/PP-TSN/python_ppvideo_PP-TSN_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..f3751bd514c184092f7cd8a7cb7127bca237ec9f --- /dev/null +++ b/test_tipc/results/PP-TSN/python_ppvideo_PP-TSN_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9998553991317749 diff --git a/test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp16.txt b/test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..a72619cf1778bfbb8206b7469cbf26d670eccfd9 --- /dev/null +++ b/test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp16.txt @@ -0,0 +1 @@ +./deploy/cpp_infer/example_video_dir/example01.avi class: 5 archery score: 0.999323 diff --git a/test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt b/test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..1e22968f1193c15ff769f05c85e297b7f5caa6e3 --- /dev/null +++ b/test_tipc/results/PP-TSN_CPP/cpp_ppvideo_PP-TSN_results_fp32.txt @@ -0,0 +1 @@ +./deploy/cpp_infer/example_video_dir/example01.avi class: 5 archery score: 0.999315 diff --git a/test_tipc/results/STGCN.txt/python_ppvideo_STGCN_results_fp16.txt b/test_tipc/results/STGCN.txt/python_ppvideo_STGCN_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..aa05e5d889714dfeacf2cc5a4e068f99ad182185 --- /dev/null +++ b/test_tipc/results/STGCN.txt/python_ppvideo_STGCN_results_fp16.txt @@ -0,0 +1,3 @@ +Current video file: data/fsd10/example_skeleton.npy + top-1 class: 27 + top-1 score: 0.9912770986557007 diff --git a/test_tipc/results/STGCN.txt/python_ppvideo_STGCN_results_fp32.txt b/test_tipc/results/STGCN.txt/python_ppvideo_STGCN_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..aa05e5d889714dfeacf2cc5a4e068f99ad182185 --- /dev/null +++ b/test_tipc/results/STGCN.txt/python_ppvideo_STGCN_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: data/fsd10/example_skeleton.npy + top-1 class: 27 + top-1 score: 0.9912770986557007 diff --git a/test_tipc/results/SlowFast/python_ppvideo_SlowFast_results_fp16.txt b/test_tipc/results/SlowFast/python_ppvideo_SlowFast_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..9baf852478fc53eb665055d2766531e1132ba1ee --- /dev/null +++ b/test_tipc/results/SlowFast/python_ppvideo_SlowFast_results_fp16.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 1.0 diff --git a/test_tipc/results/SlowFast/python_ppvideo_SlowFast_results_fp32.txt b/test_tipc/results/SlowFast/python_ppvideo_SlowFast_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..e419ed6e9b6837883155350dd12a56e2323bab95 --- /dev/null +++ b/test_tipc/results/SlowFast/python_ppvideo_SlowFast_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9999998807907104 diff --git a/test_tipc/results/TSM/python_ppvideo_TSM_results_fp16.txt b/test_tipc/results/TSM/python_ppvideo_TSM_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..df56a1554a95942ae5e43fe536565b2a4502cf4e --- /dev/null +++ b/test_tipc/results/TSM/python_ppvideo_TSM_results_fp16.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9999222755432129 diff --git a/test_tipc/results/TSM/python_ppvideo_TSM_results_fp32.txt b/test_tipc/results/TSM/python_ppvideo_TSM_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..8e2a425a549bb551f50ce32bf5ccfdad770e97e7 --- /dev/null +++ b/test_tipc/results/TSM/python_ppvideo_TSM_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9999209642410278 diff --git a/test_tipc/results/TSN/python_ppvideo_TSN_results_fp16.txt b/test_tipc/results/TSN/python_ppvideo_TSN_results_fp16.txt new file mode 100644 index 0000000000000000000000000000000000000000..1f1a8187f6a8bc02f1ea67c605a3f31c120388a5 --- /dev/null +++ b/test_tipc/results/TSN/python_ppvideo_TSN_results_fp16.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.99908447265625 diff --git a/test_tipc/results/TSN/python_ppvideo_TSN_results_fp32.txt b/test_tipc/results/TSN/python_ppvideo_TSN_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..f964d3c35d02e88b8f41b9fa8e3b0663e3766479 --- /dev/null +++ b/test_tipc/results/TSN/python_ppvideo_TSN_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9990712404251099 diff --git a/test_tipc/results/TimeSformer/python_ppvideo_TimeSformer_results_fp32.txt b/test_tipc/results/TimeSformer/python_ppvideo_TimeSformer_results_fp32.txt new file mode 100644 index 0000000000000000000000000000000000000000..281e789c9007a729bb0d08b93c8424e1086a5757 --- /dev/null +++ b/test_tipc/results/TimeSformer/python_ppvideo_TimeSformer_results_fp32.txt @@ -0,0 +1,3 @@ +Current video file: data/example.avi + top-1 class: 5 + top-1 score: 0.9997474551200867 diff --git a/test_tipc/test_inference_cpp.sh b/test_tipc/test_inference_cpp.sh new file mode 100644 index 0000000000000000000000000000000000000000..5a83fd3452fb43958aa27fa94ebb5deb36a3e180 --- /dev/null +++ b/test_tipc/test_inference_cpp.sh @@ -0,0 +1,227 @@ +#!/bin/bash +source test_tipc/common_func.sh + +FILENAME=$1 +dataline=$(awk 'NR==1, NR==18{print}' $FILENAME) + +# parser params +IFS=$'\n' +lines=(${dataline}) + +# parser cpp inference model +model_name=$(func_parser_value "${lines[1]}") +use_opencv=$(func_parser_value "${lines[2]}") +cpp_infer_model_dir_list=$(func_parser_value "${lines[3]}") +cpp_infer_is_quant=$(func_parser_value "${lines[4]}") +# parser cpp inference +inference_cmd=$(func_parser_value "${lines[5]}") +cpp_use_gpu_key=$(func_parser_key "${lines[6]}") +cpp_use_gpu_list=$(func_parser_value "${lines[6]}") +cpp_use_mkldnn_key=$(func_parser_key "${lines[7]}") +cpp_use_mkldnn_list=$(func_parser_value "${lines[7]}") +cpp_cpu_threads_key=$(func_parser_key "${lines[8]}") +cpp_cpu_threads_list=$(func_parser_value "${lines[8]}") +cpp_batch_size_key=$(func_parser_key "${lines[9]}") +cpp_batch_size_list=$(func_parser_value "${lines[9]}") +cpp_use_trt_key=$(func_parser_key "${lines[10]}") +cpp_use_trt_list=$(func_parser_value "${lines[10]}") +cpp_precision_key=$(func_parser_key "${lines[11]}") +cpp_precision_list=$(func_parser_value "${lines[11]}") +cpp_infer_model_key=$(func_parser_key "${lines[12]}") +cpp_image_dir_key=$(func_parser_key "${lines[13]}") +cpp_infer_img_dir=$(func_parser_value "${lines[13]}") +cpp_infer_key1=$(func_parser_key "${lines[14]}") +cpp_infer_value1=$(func_parser_value "${lines[14]}") +cpp_benchmark_key=$(func_parser_key "${lines[15]}") +cpp_benchmark_value=$(func_parser_value "${lines[15]}") +cpp_infer_key2=$(func_parser_key "${lines[16]}") +cpp_infer_value2=$(func_parser_value "${lines[16]}") +cpp_infer_key3=$(func_parser_key "${lines[17]}") +cpp_infer_value3=$(func_parser_value "${lines[17]}") + +LOG_PATH="./test_tipc/output/${model_name}" +mkdir -p ${LOG_PATH} +status_log="${LOG_PATH}/results_cpp.log" + + +function func_cpp_inference(){ + IFS='|' + _script=$1 + _model_dir=$2 + _log_path=$3 + _img_dir=$4 + _flag_quant=$5 + # inference + for use_gpu in ${cpp_use_gpu_list[*]}; do + if [ ${use_gpu} = "False" ] || [ ${use_gpu} = "cpu" ]; then + for use_mkldnn in ${cpp_use_mkldnn_list[*]}; do + if [ ${use_mkldnn} = "False" ] && [ ${_flag_quant} = "True" ]; then + continue + fi + for threads in ${cpp_cpu_threads_list[*]}; do + for batch_size in ${cpp_batch_size_list[*]}; do + precision="fp32" + if [ ${use_mkldnn} = "False" ] && [ ${_flag_quant} = "True" ]; then + precison="int8" + fi + _save_log_path="${_log_path}/cpp_infer_cpu_usemkldnn_${use_mkldnn}_threads_${threads}_precision_${precision}_batchsize_${batch_size}.log" + set_infer_data=$(func_set_params "${cpp_image_dir_key}" "${_img_dir}") + set_benchmark=$(func_set_params "${cpp_benchmark_key}" "${cpp_benchmark_value}") + set_batchsize=$(func_set_params "${cpp_batch_size_key}" "${batch_size}") + set_cpu_threads=$(func_set_params "${cpp_cpu_threads_key}" "${threads}") + set_model_dir=$(func_set_params "${cpp_infer_model_key}" "${_model_dir}") + set_infer_params1=$(func_set_params "${cpp_infer_key1}" "${cpp_infer_value1}") + set_infer_params2=$(func_set_params "${cpp_infer_key2}" "${cpp_infer_value2}") + set_infer_params3=$(func_set_params "${cpp_infer_key3}" "${cpp_infer_value3}") + command="${_script} ${cpp_use_gpu_key}=${use_gpu} ${cpp_use_mkldnn_key}=${use_mkldnn} ${set_cpu_threads} ${set_model_dir} ${set_batchsize} ${set_infer_data} ${set_benchmark} ${set_infer_params1} ${set_infer_params2} ${set_infer_params3} > ${_save_log_path} 2>&1 " + eval $command + last_status=${PIPESTATUS[0]} + eval "cat ${_save_log_path}" + status_check $last_status "${command}" "${status_log}" + done + done + done + elif [ ${use_gpu} = "True" ] || [ ${use_gpu} = "gpu" ]; then + for use_trt in ${cpp_use_trt_list[*]}; do + for precision in ${cpp_precision_list[*]}; do + if [[ ${_flag_quant} = "False" ]] && [[ ${precision} =~ "int8" ]]; then + continue + fi + if [[ ${precision} =~ "fp16" || ${precision} =~ "int8" ]] && [ ${use_trt} = "False" ]; then + continue + fi + if [[ ${use_trt} = "False" || ${precision} =~ "int8" ]] && [ ${_flag_quant} = "True" ]; then + continue + fi + for batch_size in ${cpp_batch_size_list[*]}; do + _save_log_path="${_log_path}/cpp_infer_gpu_usetrt_${use_trt}_precision_${precision}_batchsize_${batch_size}.log" + set_infer_data=$(func_set_params "${cpp_image_dir_key}" "${_img_dir}") + set_benchmark=$(func_set_params "${cpp_benchmark_key}" "${cpp_benchmark_value}") + set_batchsize=$(func_set_params "${cpp_batch_size_key}" "${batch_size}") + set_tensorrt=$(func_set_params "${cpp_use_trt_key}" "${use_trt}") + set_precision=$(func_set_params "${cpp_precision_key}" "${precision}") + set_model_dir=$(func_set_params "${cpp_infer_model_key}" "${_model_dir}") + set_infer_params1=$(func_set_params "${cpp_infer_key1}" "${cpp_infer_value1}") + set_infer_params2=$(func_set_params "${cpp_infer_key2}" "${cpp_infer_value2}") + set_infer_params3=$(func_set_params "${cpp_infer_key3}" "${cpp_infer_value3}") + command="${_script} ${cpp_use_gpu_key}=${use_gpu} ${set_tensorrt} ${set_precision} ${set_model_dir} ${set_batchsize} ${set_infer_data} ${set_benchmark} ${set_infer_params1} ${set_infer_params2} ${set_infer_params3} > ${_save_log_path} 2>&1 " + eval $command + last_status=${PIPESTATUS[0]} + eval "cat ${_save_log_path}" + status_check $last_status "${command}" "${status_log}" + + done + done + done + else + echo "Does not support hardware other than CPU and GPU Currently!" + fi + done +} + + +cd deploy/cpp_infer +if [ ${use_opencv} = "True" ]; then + if [ -d "opencv-3.4.7/opencv3/" ] && [ $(md5sum opencv-3.4.7.tar.gz | awk -F ' ' '{print $1}') = "faa2b5950f8bee3f03118e600c74746a" ];then + echo "################### build opencv skipped ###################" + else + echo "################### building opencv ###################" + rm -rf opencv-3.4.7.tar.gz opencv-3.4.7/ + wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/test/opencv-3.4.7.tar.gz + tar -xf opencv-3.4.7.tar.gz + + cd opencv-3.4.7/ + install_path=$(pwd)/opencv3 + + rm -rf build + mkdir build + cd build + + cmake .. \ + -DCMAKE_INSTALL_PREFIX=${install_path} \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DWITH_IPP=OFF \ + -DBUILD_IPP_IW=OFF \ + -DWITH_LAPACK=OFF \ + -DWITH_EIGEN=OFF \ + -DCMAKE_INSTALL_LIBDIR=lib64 \ + -DWITH_ZLIB=ON \ + -DBUILD_ZLIB=ON \ + -DWITH_JPEG=ON \ + -DBUILD_JPEG=ON \ + -DWITH_PNG=ON \ + -DBUILD_PNG=ON \ + -DWITH_TIFF=ON \ + -DBUILD_TIFF=ON \ + -DWITH_FFMPEG=ON + + make -j + make install + cd ../ + echo "################### building opencv finished ###################" + fi +fi + + +if [ !-d "paddle_inference" ]; then + echo "################### download inference lib skipped ###################" +else + echo "################### downloading inference lib ###################" + wget -nc https://paddle-inference-lib.bj.bcebos.com/2.1.1-gpu-cuda10.1-cudnn7-mkl-gcc8.2/paddle_inference.tgz + tar -xf paddle_inference.tgz + echo "################### downloading inference lib finished ###################" +fi + +echo "################### building PaddleVideo demo ####################" +if [ ${use_opencv} = "True" ]; then + OPENCV_DIR=$(pwd)/opencv-3.4.7/opencv3 +else + OPENCV_DIR='' +fi + +LIB_DIR=$(pwd)/paddle_inference +CUDA_LIB_DIR=$(dirname `find /usr -name libcudart.so`) +CUDNN_LIB_DIR=$(dirname `find /usr -name libcudnn.so`) + +BUILD_DIR=build +rm -rf ${BUILD_DIR} +mkdir ${BUILD_DIR} +cd ${BUILD_DIR} +cmake .. \ + -DPADDLE_LIB=${LIB_DIR} \ + -DWITH_MKL=ON \ + -DWITH_GPU=OFF \ + -DWITH_STATIC_LIB=OFF \ + -DWITH_TENSORRT=OFF \ + -DOPENCV_DIR=${OPENCV_DIR} \ + -DCUDNN_LIB=${CUDNN_LIB_DIR} \ + -DCUDA_LIB=${CUDA_LIB_DIR} \ + -DTENSORRT_DIR=${TENSORRT_DIR} \ + +make -j +cd ../../../ +echo "################### building PaddleVideo demo finished ###################" + + +# set cuda device +GPUID=$2 +if [ ${#GPUID} -le 0 ];then + env=" " +else + env="export CUDA_VISIBLE_DEVICES=${GPUID}" +fi +set CUDA_VISIBLE_DEVICES +eval $env + + +echo "################### running test ###################" +export Count=0 +IFS="|" +infer_quant_flag=(${cpp_infer_is_quant}) +for infer_model in ${cpp_infer_model_dir_list[*]}; do + #run inference + is_quant=${infer_quant_flag[Count]} + func_cpp_inference "${inference_cmd}" "${infer_model}" "${LOG_PATH}" "${cpp_infer_img_dir}" ${is_quant} + Count=$(($Count + 1)) +done diff --git a/test_tipc/test_train_inference_python.sh b/test_tipc/test_train_inference_python.sh new file mode 100644 index 0000000000000000000000000000000000000000..beb8c0666a018dc90294731b69e316d043ce3f9b --- /dev/null +++ b/test_tipc/test_train_inference_python.sh @@ -0,0 +1,413 @@ +#!/bin/bash +source test_tipc/common_func.sh + +FILENAME=$1 +# MODE be one of ['lite_train_lite_infer' 'lite_train_whole_infer' 'whole_train_whole_infer', 'whole_infer', 'klquant_whole_infer'] +MODE=$2 + +dataline=$(awk 'NR==1, NR==51{print}' $FILENAME) + +# parser params +IFS=$'\n' +lines=(${dataline}) + +# The training params +model_name=$(func_parser_value "${lines[1]}") +python=$(func_parser_value "${lines[2]}") +gpu_list=$(func_parser_value "${lines[3]}") +train_use_gpu_key=$(func_parser_key "${lines[4]}") +train_use_gpu_value=$(func_parser_value "${lines[4]}") +autocast_list=$(func_parser_value "${lines[5]}") +autocast_key=$(func_parser_key "${lines[5]}") +epoch_key=$(func_parser_key "${lines[6]}") +epoch_num=$(func_parser_value "${lines[6]}") +save_model_key=$(func_parser_key "${lines[7]}") +train_batch_key=$(func_parser_key "${lines[8]}") +train_batch_value=$(func_parser_value "${lines[8]}") +pretrain_model_key=$(func_parser_key "${lines[9]}") +pretrain_model_value=$(func_parser_value "${lines[9]}") +train_model_name=$(func_parser_value "${lines[10]}") +train_param_key1=$(func_parser_key "${lines[12]}") +train_param_value1=$(func_parser_value "${lines[12]}") +train_param_key2=$(func_parser_key "${lines[11]}") +train_param_value2=$(func_parser_value "${lines[11]}") + +trainer_list=$(func_parser_value "${lines[14]}") +trainer_norm=$(func_parser_key "${lines[15]}") +norm_trainer=$(func_parser_value "${lines[15]}") +pact_key=$(func_parser_key "${lines[16]}") +pact_trainer=$(func_parser_value "${lines[16]}") +fpgm_key=$(func_parser_key "${lines[17]}") +fpgm_trainer=$(func_parser_value "${lines[17]}") +distill_key=$(func_parser_key "${lines[18]}") +distill_trainer=$(func_parser_value "${lines[18]}") +amp_key=$(func_parser_key "${lines[19]}") +amp_trainer=$(func_parser_value "${lines[19]}") +trainer_key2=$(func_parser_key "${lines[20]}") +trainer_value2=$(func_parser_value "${lines[20]}") + +eval_py=$(func_parser_value "${lines[23]}") +eval_key1=$(func_parser_key "${lines[24]}") +eval_value1=$(func_parser_value "${lines[24]}") + +save_infer_key=$(func_parser_key "${lines[27]}") +save_infer_value=$(func_parser_value "${lines[27]}") + +export_weight=$(func_parser_key "${lines[28]}") +norm_export=$(func_parser_value "${lines[29]}") +pact_export=$(func_parser_value "${lines[30]}") +fpgm_export=$(func_parser_value "${lines[31]}") +distill_export=$(func_parser_value "${lines[32]}") +export_key1=$(func_parser_key "${lines[33]}") +export_value1=$(func_parser_value "${lines[33]}") +export_key2=$(func_parser_key "${lines[34]}") +export_value2=$(func_parser_value "${lines[34]}") +inference_dir=$(func_parser_value "${lines[35]}") + +# parser inference model +infer_model_dir_list=$(func_parser_value "${lines[36]}") +infer_export_list=$(func_parser_value "${lines[37]}") +infer_is_quant=$(func_parser_value "${lines[38]}") +# parser inference +inference_py=$(func_parser_value "${lines[39]}") +use_gpu_key=$(func_parser_key "${lines[40]}") +use_gpu_list=$(func_parser_value "${lines[40]}") +use_mkldnn_key=$(func_parser_key "${lines[41]}") +use_mkldnn_list=$(func_parser_value "${lines[41]}") +cpu_threads_key=$(func_parser_key "${lines[42]}") +cpu_threads_list=$(func_parser_value "${lines[42]}") +batch_size_key=$(func_parser_key "${lines[43]}") +batch_size_list=$(func_parser_value "${lines[43]}") +use_trt_key=$(func_parser_key "${lines[44]}") +use_trt_list=$(func_parser_value "${lines[44]}") +precision_key=$(func_parser_key "${lines[45]}") +precision_list=$(func_parser_value "${lines[45]}") +infer_model_key=$(func_parser_key "${lines[46]}") +infer_model_value=$(func_parser_value "${lines[46]}") + +video_dir_key=$(func_parser_key "${lines[47]}") +infer_video_dir=$(func_parser_value "${lines[47]}") +save_log_key=$(func_parser_key "${lines[48]}") +benchmark_key=$(func_parser_key "${lines[49]}") +benchmark_value=$(func_parser_value "${lines[49]}") + +infer_key1=$(func_parser_key "${lines[50]}") +infer_value1=$(func_parser_value "${lines[50]}") + +# parser klquant_infer +if [ ${MODE} = "klquant_whole_infer" ]; then + dataline=$(awk 'NR==1 NR==17{print}' $FILENAME) + lines=(${dataline}) + model_name=$(func_parser_value "${lines[1]}") + python=$(func_parser_value "${lines[2]}") + # parser inference model + infer_model_dir_list=$(func_parser_value "${lines[3]}") + infer_export_list=$(func_parser_value "${lines[4]}") + infer_is_quant=$(func_parser_value "${lines[5]}") + # parser inference + inference_py=$(func_parser_value "${lines[6]}") + use_gpu_key=$(func_parser_key "${lines[7]}") + use_gpu_list=$(func_parser_value "${lines[7]}") + use_mkldnn_key=$(func_parser_key "${lines[8]}") + use_mkldnn_list=$(func_parser_value "${lines[8]}") + cpu_threads_key=$(func_parser_key "${lines[9]}") + cpu_threads_list=$(func_parser_value "${lines[9]}") + batch_size_key=$(func_parser_key "${lines[10]}") + batch_size_list=$(func_parser_value "${lines[10]}") + use_trt_key=$(func_parser_key "${lines[11]}") + use_trt_list=$(func_parser_value "${lines[11]}") + precision_key=$(func_parser_key "${lines[12]}") + precision_list=$(func_parser_value "${lines[12]}") + infer_model_key=$(func_parser_key "${lines[13]}") + video_dir_key=$(func_parser_key "${lines[14]}") + infer_video_dir=$(func_parser_value "${lines[14]}") + save_log_key=$(func_parser_key "${lines[15]}") + benchmark_key=$(func_parser_key "${lines[16]}") + benchmark_value=$(func_parser_value "${lines[16]}") + infer_key1=$(func_parser_key "${lines[17]}") + infer_value1=$(func_parser_value "${lines[17]}") +fi + +LOG_PATH="./test_tipc/output/${model_name}" +mkdir -p ${LOG_PATH} +status_log="${LOG_PATH}/results_python.log" + + +function func_inference(){ + IFS='|' + _python=$1 + _script=$2 + _model_dir=$3 + _log_path=$4 + _video_dir=$5 + _flag_quant=$6 + # inference + for use_gpu in ${use_gpu_list[*]}; do + if [ ${use_gpu} = "False" ] || [ ${use_gpu} = "cpu" ]; then + for use_mkldnn in ${use_mkldnn_list[*]}; do + if [[ ${use_mkldnn} = "False" ]] && [[ ${_flag_quant} = "True" ]]; then + continue + fi + for threads in ${cpu_threads_list[*]}; do + for batch_size in ${batch_size_list[*]}; do + for precision in ${precision_list[*]}; do + if [[ ${use_mkldnn} = "False" ]] && [[ ${precision} = "fp16" ]]; then + continue + fi # skip when enable fp16 but disable mkldnn + if [[ ${_flag_quant} = "True" ]] && [[ ${precision} != "int8" ]]; then + continue + fi # skip when quant model inference but precision is not int8 + set_precision=$(func_set_params "${precision_key}" "${precision}") + + _save_log_path="${_log_path}/python_infer_cpu_usemkldnn_${use_mkldnn}_threads_${threads}_precision_${precision}_batchsize_${batch_size}.log" + mkdir -p ${_log_path} + set_infer_data=$(func_set_params "${video_dir_key}" "${infer_video_dir}") + set_benchmark=$(func_set_params "${benchmark_key}" "${benchmark_value}") + set_batchsize=$(func_set_params "${batch_size_key}" "${batch_size}") + set_cpu_threads=$(func_set_params "${cpu_threads_key}" "${threads}") + set_model_dir=$(func_set_params "${infer_model_key}" "${_model_dir}/${infer_model_value}") + set_infer_params1=$(func_set_params "${infer_key1}" "${_model_dir}/${infer_value1}") + command="${_python} ${_script} ${use_gpu_key}=${use_gpu} ${use_mkldnn_key}=${use_mkldnn} ${set_cpu_threads} ${set_model_dir} ${set_batchsize} ${set_infer_data} ${set_benchmark} ${set_precision} ${set_infer_params1} > ${_save_log_path} 2>&1 " + eval $command + last_status=${PIPESTATUS[0]} + eval "cat ${_save_log_path}" + status_check $last_status "${command}" "${status_log}" + done + done + done + done + elif [ ${use_gpu} = "True" ] || [ ${use_gpu} = "gpu" ]; then + for use_trt in ${use_trt_list[*]}; do + for precision in ${precision_list[*]}; do + if [[ ${_flag_quant} = "False" ]] && [[ ${precision} =~ "int8" ]]; then + continue + fi + if [[ ${precision} =~ "fp16" || ${precision} =~ "int8" ]] && [[ ${use_trt} = "False" ]]; then + continue + fi + if [[ ${use_trt} = "False" || ${precision} =~ "int8" ]] && [[ ${_flag_quant} = "True" ]]; then + continue + fi + for batch_size in ${batch_size_list[*]}; do + _save_log_path="${_log_path}/python_infer_gpu_usetrt_${use_trt}_precision_${precision}_batchsize_${batch_size}.log" + set_infer_data=$(func_set_params "${video_dir_key}" "${infer_video_dir}") + + set_benchmark=$(func_set_params "${benchmark_key}" "${benchmark_value}") + set_batchsize=$(func_set_params "${batch_size_key}" "${batch_size}") + set_tensorrt=$(func_set_params "${use_trt_key}" "${use_trt}") + set_precision=$(func_set_params "${precision_key}" "${precision}") + set_model_dir=$(func_set_params "${infer_model_key}" "${_model_dir}/${infer_model_value}") + set_infer_params1=$(func_set_params "${infer_key1}" "${_model_dir}/${infer_value1}") + command="${_python} ${_script} ${use_gpu_key}=${use_gpu} ${set_tensorrt} ${set_precision} ${set_model_dir} ${set_batchsize} ${set_infer_data} ${set_benchmark} ${set_infer_params1} > ${_save_log_path} 2>&1 " + + eval $command + + last_status=${PIPESTATUS[0]} + eval "cat ${_save_log_path}" + status_check $last_status "${command}" "${status_log}" + + done + done + done + else + echo "Does not support hardware other than CPU and GPU Currently!" + fi + done +} + +if [ ${MODE} = "whole_infer" ] || [ ${MODE} = "klquant_whole_infer" ]; then + GPUID=$3 + if [ ${#GPUID} -le 0 ];then + env=" " + else + env="export CUDA_VISIBLE_DEVICES=${GPUID}" + fi + set CUDA_VISIBLE_DEVICES + eval $env + export Count=0 + IFS="|" + infer_run_exports=(${infer_export_list}) + infer_quant_flag=(${infer_is_quant}) + for infer_model in ${infer_model_dir_list[*]}; do + # run export + if [ ${infer_run_exports[Count]} != "null" ];then + save_infer_dir=$(dirname $infer_model) + set_export_weight=$(func_set_params "${export_weight}" "${infer_model}") + set_save_infer_key=$(func_set_params "${save_infer_key}" "${save_infer_dir}") + export_cmd="${python} ${infer_run_exports[Count]} ${set_export_weight} ${set_save_infer_key}" + echo ${infer_run_exports[Count]} + eval $export_cmd + echo $export_cmd + status_export=$? + status_check $status_export "${export_cmd}" "${status_log}" + else + save_infer_dir=${infer_model} + fi + #run inference + is_quant=${infer_quant_flag[Count]} + if [ ${MODE} = "klquant_infer" ]; then + is_quant="True" + fi + func_inference "${python}" "${inference_py}" "${save_infer_dir}" "${LOG_PATH}" "${infer_video_dir}" ${is_quant} + Count=$(($Count + 1)) + done +else + IFS="|" + export Count=0 + USE_GPU_KEY=(${train_use_gpu_value}) + for gpu in ${gpu_list[*]}; do + train_use_gpu=${USE_GPU_KEY[Count]} + Count=$(($Count + 1)) + ips="" + if [ ${gpu} = "-1" ];then + env="" + elif [ ${#gpu} -le 1 ];then + env="export CUDA_VISIBLE_DEVICES=${gpu}" + eval ${env} + elif [ ${#gpu} -le 15 ];then + IFS="," + array=(${gpu}) + env="export CUDA_VISIBLE_DEVICES=${array[0]}" + IFS="|" + else + IFS=";" + array=(${gpu}) + ips=${array[0]} + gpu=${array[1]} + IFS="|" + env=" " + fi + for autocast in ${autocast_list[*]}; do + if [ ${autocast} = "fp16" ]; then + set_amp_config="--amp" + else + set_amp_config=" " + fi + for trainer in ${trainer_list[*]}; do + flag_quant=False + if [ ${trainer} = ${pact_key} ]; then + run_train=${pact_trainer} + run_export=${pact_export} + flag_quant=True + elif [ ${trainer} = "${fpgm_key}" ]; then + run_train=${fpgm_trainer} + run_export=${fpgm_export} + elif [ ${trainer} = "${distill_key}" ]; then + run_train=${distill_trainer} + run_export=${distill_export} + elif [ ${trainer} = ${amp_key} ]; then + run_train=${amp_trainer} + run_export=${norm_export} + elif [[ ${trainer} = ${trainer_key2} ]]; then + run_train=${trainer_value2} + run_export=${export_value2} + else + run_train=${norm_trainer} + run_export=${norm_export} + fi + + if [ ${run_train} = "null" ]; then + continue + fi + if [[ ${MODE} != "benchmark_train" ]] && [[ ! ${MODE} =~ "whole_train" ]]; then + # 训练参数末尾加上--max_iters=30和--log_interval=1,以便运行并输出足量数据 + run_train=${run_train}" --max_iters=30" + fi + set_autocast=$(func_set_params "${autocast_key}" "${autocast}") + set_epoch=$(func_set_params "${epoch_key}" "${epoch_num}") + + if [[ $MODE =~ "whole_train" ]]; then + set_epoch="" + fi + + set_pretrain=$(func_set_params "${pretrain_model_key}" "${pretrain_model_value}") + if [[ $MODE =~ "whole_train" ]]; then + train_batch_key="" + train_batch_value="" + fi + set_batchsize=$(func_set_params "${train_batch_key}" "${train_batch_value}") + if [[ $MODE =~ "whole_train" ]]; then + train_param_key1="" + train_param_value1="" + fi + set_train_params1=$(func_set_params "${train_param_key1}" "${train_param_value1}") + if [[ $MODE =~ "whole_train" ]]; then + train_param_key2="" + train_param_value2="" + fi + set_train_params2=$(func_set_params "${train_param_key2}" "${train_param_value2}") + set_use_gpu=$(func_set_params "${train_use_gpu_key}" "${train_use_gpu}") + if [ ${#ips} -le 26 ];then + save_log="${LOG_PATH}/${trainer}_gpus_${gpu}_autocast_${autocast}" + nodes=1 + else + IFS="," + ips_array=(${ips}) + IFS="|" + nodes=${#ips_array[@]} + save_log="${LOG_PATH}/${trainer}_gpus_${gpu}_autocast_${autocast}_nodes_${nodes}" + fi + + # load pretrain from norm training if current trainer is pact or fpgm trainer + if ([ ${trainer} = ${pact_key} ] || [ ${trainer} = ${fpgm_key} ]) && [ ${nodes} -le 1 ]; then + set_pretrain="${load_norm_train_model}" + fi + + set_save_model=$(func_set_params "${save_model_key}" "${save_log}") + if [ ${#gpu} -le 2 ];then # train with cpu or single gpu + cmd="${python} ${run_train} ${set_amp_config} ${set_use_gpu} ${set_save_model} ${set_epoch} ${set_pretrain} ${set_batchsize} ${set_train_params1} ${set_train_params2} " + elif [ ${#ips} -le 26 ];then # train with multi-gpu + cmd="${python} -B -m paddle.distributed.launch --gpus=\"${gpu}\" ${run_train} ${set_amp_config} ${set_use_gpu} ${set_save_model} ${set_epoch} ${set_pretrain} ${set_batchsize} ${set_train_params1} ${set_train_params2} " + else # train with multi-machine + cmd="${python} -B -m paddle.distributed.launch --ips=${ips} --gpus=\"${gpu}\" ${run_train} ${set_amp_config} ${set_use_gpu} ${set_save_model} ${set_pretrain} ${set_epoch} ${set_batchsize} ${set_train_params1} ${set_train_params2} " + fi + + # run train + eval "unset CUDA_VISIBLE_DEVICES" + eval $cmd + status_check $? "${cmd}" "${status_log}" + + # set_eval_pretrain=$(func_set_params "${pretrain_model_key}" "${save_log}/${train_model_name}") + # save norm trained models to set pretrain for pact training and fpgm training + if [ [${trainer} = ${trainer_norm}] ] && [ [${nodes} -le 1] ]; then + load_norm_train_model=${set_eval_pretrain} + fi + # run test + if [ ${eval_py} != "null" ]; then + real_model_name=${model_name/PP-/pp} + set_eval_params1=$(func_set_params "${eval_key1}" "${save_log}/${real_model_name}_epoch_00001.pdparams") + if [[ $MODE =~ "lite_infer" ]] && [[ ${train_param_key1} != "null" ]]; then + eval_cmd="${python} ${eval_py} ${set_use_gpu} ${set_eval_params1} ${train_param_key1}=${train_param_value1}" + else + eval_cmd="${python} ${eval_py} ${set_use_gpu} ${set_eval_params1}" + fi + eval $eval_cmd + status_check $? "${eval_cmd}" "${status_log}" + fi + # run export model + if [ ${run_export} != "null" ]; then + save_infer_path="${save_log}" + real_model_name=${model_name/PP-/pp} + set_export_weight=$(func_set_params "${export_weight}" "${save_log}/${real_model_name}_epoch_00001.pdparams") + + set_save_infer_key=$(func_set_params "${save_infer_key}" "${save_log}") + export_cmd="${python} ${run_export} ${set_export_weight} ${set_save_infer_key}" + eval $export_cmd + status_check $? "${export_cmd}" "${status_log}" + + #run inference + eval $env + save_infer_path="${save_log}" + if [ ${inference_dir} != "null" ] && [ ${inference_dir} != '##' ]; then + infer_model_dir=${save_infer_path} + else + infer_model_dir=${save_infer_path} + fi + func_inference "${python}" "${inference_py}" "${infer_model_dir}" "${LOG_PATH}" "${flag_quant}" + + eval "unset CUDA_VISIBLE_DEVICES" + fi + done # done with: for trainer in ${trainer_list[*]}; do + done # done with: for autocast in ${autocast_list[*]}; do + done # done with: for gpu in ${gpu_list[*]}; do +fi # end if [ ${MODE} = "infer" ]; then diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b6bd235ccafd4bd452a0f4ef7d92289a0461af0f --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import utils +from .paddlevideo_clas import PaddleVideo +from . import ava_predict diff --git a/tools/ava_predict.py b/tools/ava_predict.py new file mode 100644 index 0000000000000000000000000000000000000000..04e5e377395f2b75683e686a860de4aa0d711646 --- /dev/null +++ b/tools/ava_predict.py @@ -0,0 +1,508 @@ +# copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import paddle +import os, sys +import copy as cp +import cv2 +import math +try: + import ppdet +except ImportError as e: + print( + f"{e}, [paddledet] package and it's dependencies is required for AVA.") + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../'))) + +from paddlevideo.modeling.builder import build_model +from paddlevideo.utils import get_config +from paddlevideo.loader.builder import build_dataloader, build_dataset, build_pipeline +from paddlevideo.metrics.ava_utils import read_labelmap + +import time +from os import path as osp +import numpy as np +from paddlevideo.utils import get_config +import pickle + +from paddlevideo.utils import (get_logger, load, mkdir, save) +import shutil + +FONTFACE = cv2.FONT_HERSHEY_DUPLEX +FONTSCALE = 0.5 +FONTCOLOR = (255, 255, 255) # BGR, white +MSGCOLOR = (128, 128, 128) # BGR, gray +THICKNESS = 1 +LINETYPE = 1 + + +def hex2color(h): + """Convert the 6-digit hex string to tuple of 3 int value (RGB)""" + return (int(h[:2], 16), int(h[2:4], 16), int(h[4:], 16)) + + +plate_blue = '03045e-023e8a-0077b6-0096c7-00b4d8-48cae4' +plate_blue = plate_blue.split('-') +plate_blue = [hex2color(h) for h in plate_blue] +plate_green = '004b23-006400-007200-008000-38b000-70e000' +plate_green = plate_green.split('-') +plate_green = [hex2color(h) for h in plate_green] + + +def abbrev(name): + """Get the abbreviation of label name: + 'take (an object) from (a person)' -> 'take ... from ...' + """ + while name.find('(') != -1: + st, ed = name.find('('), name.find(')') + name = name[:st] + '...' + name[ed + 1:] + return name + + +# annotations is pred results +def visualize(frames, annotations, plate=plate_blue, max_num=5): + """Visualize frames with predicted annotations. + Args: + frames (list[np.ndarray]): Frames for visualization, note that + len(frames) % len(annotations) should be 0. + annotations (list[list[tuple]]): The predicted results. + plate (str): The plate used for visualization. Default: plate_blue. + max_num (int): Max number of labels to visualize for a person box. + Default: 5,目前不能大于5. + Returns: + list[np.ndarray]: Visualized frames. + """ + + assert max_num + 1 <= len(plate) + plate = [x[::-1] for x in plate] + frames_ = cp.deepcopy(frames) + nf, na = len(frames), len(annotations) + assert nf % na == 0 + nfpa = len(frames) // len(annotations) + anno = None + h, w, _ = frames[0].shape + # proposals被归一化需要还原真实坐标值 + scale_ratio = np.array([w, h, w, h]) + + for i in range(na): + anno = annotations[i] + if anno is None: + continue + for j in range(nfpa): + ind = i * nfpa + j + frame = frames_[ind] + for ann in anno: + box = ann[0] + label = ann[1] + if not len(label): + continue + score = ann[2] + box = (box * scale_ratio).astype(np.int64) + st, ed = tuple(box[:2]), tuple(box[2:]) + cv2.rectangle(frame, st, ed, plate[0], 2) + for k, lb in enumerate(label): + if k >= max_num: + break + text = abbrev(lb) + text = ': '.join([text, str(score[k])]) + location = (0 + st[0], 18 + k * 18 + st[1]) + textsize = cv2.getTextSize(text, FONTFACE, FONTSCALE, + THICKNESS)[0] + textwidth = textsize[0] + diag0 = (location[0] + textwidth, location[1] - 14) + diag1 = (location[0], location[1] + 2) + cv2.rectangle(frame, diag0, diag1, plate[k + 1], -1) + cv2.putText(frame, text, location, FONTFACE, FONTSCALE, + FONTCOLOR, THICKNESS, LINETYPE) + + return frames_ + + +def frame_extraction(video_path, target_dir): + """Extract frames given video_path. + Args: + video_path (str): The video_path. + """ + + if not os.path.exists(target_dir): + os.makedirs(target_dir, exist_ok=True) + + # Should be able to handle videos up to several hours + frame_tmpl = osp.join(target_dir, '{:05d}.jpg') + vid = cv2.VideoCapture(video_path) + + FPS = int(vid.get(5)) + + frames = [] + frame_paths = [] + + flag, frame = vid.read() + index = 1 + while flag: + frames.append(frame) + frame_path = frame_tmpl.format(index) + frame_paths.append(frame_path) + cv2.imwrite(frame_path, frame) + index += 1 + flag, frame = vid.read() + return frame_paths, frames, FPS + + +def parse_args(): + def str2bool(v): + return v.lower() in ("true", "t", "1") + + # general params + parser = argparse.ArgumentParser("PaddleVideo Inference model script") + parser.add_argument('-c', + '--config', + type=str, + default='configs/example.yaml', + help='config file path') + + parser.add_argument('--video_path', help='video file/url') + + parser.add_argument('-o', + '--override', + action='append', + default=[], + help='config options to be overridden') + parser.add_argument('-w', + '--weights', + type=str, + help='weights for finetuning or testing') + + #detection_model_name + parser.add_argument('--detection_model_name', + help='the name of detection model ') + # detection_model_weights + parser.add_argument('--detection_model_weights', + help='the weights path of detection model ') + + # params for predict + parser.add_argument('--out-filename', + default='ava_det_demo.mp4', + help='output filename') + parser.add_argument('--predict-stepsize', + default=8, + type=int, + help='give out a prediction per n frames') + parser.add_argument( + '--output-stepsize', + default=4, + type=int, + help=('show one frame per n frames in the demo, we should have: ' + 'predict_stepsize % output_stepsize == 0')) + parser.add_argument('--output-fps', + default=6, + type=int, + help='the fps of demo video output') + + return parser.parse_args() + + +# 一帧的结果。根据概率大小进行排序 +def pack_result(human_detection, result): + """Short summary. + Args: + human_detection (np.ndarray): Human detection result. + result (type): The predicted label of each human proposal. + Returns: + tuple: Tuple of human proposal, label name and label score. + """ + results = [] + if result is None: + return None + + for prop, res in zip(human_detection, result): + res.sort(key=lambda x: -x[1]) + + results.append((prop, [x[0] for x in res], [x[1] for x in res])) + + return results + + +# 构造数据处理需要的results +def get_timestep_result(frame_dir, timestamp, clip_len, frame_interval, FPS): + result = {} + + result["frame_dir"] = frame_dir + + frame_num = len(os.listdir(frame_dir)) + + dir_name = frame_dir.split("/")[-1] + result["video_id"] = dir_name + + result['timestamp'] = timestamp + + timestamp_str = '{:04d}'.format(timestamp) + img_key = dir_name + "," + timestamp_str + result['img_key'] = img_key + + result['shot_info'] = (1, frame_num) + result['fps'] = FPS + + result['suffix'] = '{:05}.jpg' + + result['timestamp_start'] = 1 + result['timestamp_end'] = int(frame_num / result['fps']) + + return result + + +def detection_inference(frame_paths, output_dir, model_name, weights_path): + """Detect human boxes given frame paths. + Args: + frame_paths (list[str]): The paths of frames to do detection inference. + Returns: + list[np.ndarray]: The human detection results. + """ + + detection_cfg = ppdet.model_zoo.get_config_file(model_name) + detection_cfg = ppdet.core.workspace.load_config(detection_cfg) + detection_trainer = ppdet.engine.Trainer(detection_cfg, mode='test') + detection_trainer.load_weights(weights_path) + + print('Performing Human Detection for each frame') + + detection_trainer.predict(frame_paths, output_dir=output_dir, save_txt=True) + + print("finish object detection") + + results = [] + + for frame_path in frame_paths: + (file_dir, file_name) = os.path.split(frame_path) + (file_path, ext) = os.path.splitext(frame_path) + + txt_file_name = file_name.replace(ext, ".txt") + txt_path = os.path.join(output_dir, txt_file_name) + results.append(txt_path) + + return results + + +def get_detection_result(txt_file_path, img_h, img_w, person_det_score_thr): + """ + 根据检测结果文件得到图像中人的检测框(proposals)和置信度(scores) + txt_file_path:检测结果存放路径 + img_h:图像高度 + img_w:图像宽度 + """ + + proposals = [] + scores = [] + + with open(txt_file_path, 'r') as detection_file: + lines = detection_file.readlines() + for line in lines: # person 0.9842637181282043 0.0 469.1407470703125 944.7770385742188 831.806396484375 + items = line.split(" ") + if items[0] != 'person': #只要人 + continue + + score = items[1] + + if (float)(score) < person_det_score_thr: + continue + + x1 = (float(items[2])) / img_w + y1 = ((float)(items[3])) / img_h + box_w = ((float)(items[4])) + box_h = ((float)(items[5])) + + x2 = (float(items[2]) + box_w) / img_w + y2 = (float(items[3]) + box_h) / img_h + + scores.append(score) + + proposals.append([x1, y1, x2, y2]) + + return np.array(proposals), np.array(scores) + + +@paddle.no_grad() +def main(args): + config = get_config(args.config, show=False) #parse config file + + # extract frames from video + video_path = args.video_path + frame_dir = 'tmp_frames' + frame_paths, frames, FPS = frame_extraction(video_path, frame_dir) + + num_frame = len(frame_paths) #视频秒数*FPS + assert num_frame != 0 + print("Frame Number:", num_frame) + + # 帧图像高度和宽度 + h, w, _ = frames[0].shape + + # Get clip_len, frame_interval and calculate center index of each clip + data_process_pipeline = build_pipeline(config.PIPELINE.test) #测试时输出处理流水配置 + + clip_len = config.PIPELINE.test.sample['clip_len'] + assert clip_len % 2 == 0, 'We would like to have an even clip_len' + frame_interval = config.PIPELINE.test.sample['frame_interval'] + + # 此处关键帧每秒取一个 + clip_len = config.PIPELINE.test.sample['clip_len'] + assert clip_len % 2 == 0, 'We would like to have an even clip_len' + frame_interval = config.PIPELINE.test.sample['frame_interval'] + window_size = clip_len * frame_interval + timestamps = np.arange(window_size // 2, (num_frame + 1 - window_size // 2), + args.predict_stepsize) + print("timetamps number:", len(timestamps)) + + # get selected frame list according to timestamps + selected_frame_list = [] + for timestamp in timestamps: + selected_frame_list.append(frame_paths[timestamp - 1]) + + # Load label_map + label_map_path = config.DATASET.test['label_file'] + categories, class_whitelist = read_labelmap(open(label_map_path)) + label_map = {} + for item in categories: + id = item['id'] + name = item['name'] + label_map[id] = name + + # Construct model. + if config.MODEL.backbone.get('pretrained'): + config.MODEL.backbone.pretrained = '' # disable pretrain model init + model = build_model(config.MODEL) + + model.eval() + state_dicts = load(args.weights) + model.set_state_dict(state_dicts) + + detection_result_dir = 'tmp_detection' + detection_model_name = args.detection_model_name + detection_model_weights = args.detection_model_weights + detection_txt_list = detection_inference(selected_frame_list, + detection_result_dir, + detection_model_name, + detection_model_weights) + assert len(detection_txt_list) == len(timestamps) + + print('Performing SpatioTemporal Action Detection for each clip') + human_detections = [] + predictions = [] + + index = 0 + for timestamp, detection_txt_path in zip(timestamps, detection_txt_list): + proposals, scores = get_detection_result( + detection_txt_path, h, w, + (float)(config.DATASET.test['person_det_score_thr'])) + if proposals.shape[0] == 0: + predictions.append(None) + human_detections.append(None) + continue + + human_detections.append(proposals) + + result = get_timestep_result(frame_dir, + timestamp, + clip_len, + frame_interval, + FPS=FPS) + result["proposals"] = proposals + result["scores"] = scores + + new_result = data_process_pipeline(result) + proposals = new_result['proposals'] + + img_slow = new_result['imgs'][0] + img_slow = img_slow[np.newaxis, :] + img_fast = new_result['imgs'][1] + img_fast = img_fast[np.newaxis, :] + + proposals = proposals[np.newaxis, :] + + scores = scores[np.newaxis, :] + + img_shape = np.asarray(new_result['img_shape']) + img_shape = img_shape[np.newaxis, :] + + data = [ + paddle.to_tensor(img_slow, dtype='float32'), + paddle.to_tensor(img_fast, dtype='float32'), + paddle.to_tensor(proposals, dtype='float32'), scores, + paddle.to_tensor(img_shape, dtype='int32') + ] + + with paddle.no_grad(): + result = model(data, mode='infer') + + result = result[0] + prediction = [] + + person_num = proposals.shape[1] + # N proposals + for i in range(person_num): + prediction.append([]) + + # Perform action score thr + for i in range(len(result)): + if i + 1 not in class_whitelist: + continue + for j in range(person_num): + if result[i][j, 4] > config.MODEL.head['action_thr']: + prediction[j].append((label_map[i + 1], result[i][j, + 4])) + predictions.append(prediction) + + index = index + 1 + if index % 10 == 0: + print(index, "/", len(timestamps)) + + results = [] + for human_detection, prediction in zip(human_detections, predictions): + results.append(pack_result(human_detection, prediction)) + + def dense_timestamps(timestamps, n): + """Make it nx frames.""" + old_frame_interval = (timestamps[1] - timestamps[0]) + start = timestamps[0] - old_frame_interval / n * (n - 1) / 2 + new_frame_inds = np.arange( + len(timestamps) * n) * old_frame_interval / n + start + return new_frame_inds.astype(np.int) + + dense_n = int(args.predict_stepsize / args.output_stepsize) #30 + frames = [ + cv2.imread(frame_paths[i - 1]) + for i in dense_timestamps(timestamps, dense_n) + ] + + vis_frames = visualize(frames, results) + + try: + import moviepy.editor as mpy + except ImportError: + raise ImportError('Please install moviepy to enable output file') + + vid = mpy.ImageSequenceClip([x[:, :, ::-1] for x in vis_frames], + fps=args.output_fps) + vid.write_videofile(args.out_filename) + print("finish write !") + + # delete tmp files and dirs + shutil.rmtree(frame_dir) + shutil.rmtree(detection_result_dir) + + +if __name__ == '__main__': + args = parse_args() #解析参数 + main(args) diff --git a/tools/export_model.py b/tools/export_model.py new file mode 100644 index 0000000000000000000000000000000000000000..6cdbaaa00f78747f67d8d8300463a87be4021c1f --- /dev/null +++ b/tools/export_model.py @@ -0,0 +1,222 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import os.path as osp +import sys + +import paddle +from paddle.jit import to_static +from paddle.static import InputSpec + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../'))) + +from paddlevideo.modeling.builder import build_model +from paddlevideo.utils import get_config + + +def parse_args(): + parser = argparse.ArgumentParser("PaddleVideo export model script") + parser.add_argument('-c', + '--config', + type=str, + default='configs/example.yaml', + help='config file path') + parser.add_argument("-p", + "--pretrained_params", + default='./best.pdparams', + type=str, + help='params path') + parser.add_argument("-o", + "--output_path", + type=str, + default="./inference", + help='output path') + + parser.add_argument('--save_name', + type=str, + default=None, + help='specify the exported inference \ + files(pdiparams and pdmodel) name,\ + only used in TIPC') + + return parser.parse_args() + + +def trim_config(cfg): + """ + Reuse the trainging config will bring useless attributes, such as: backbone.pretrained model. + and some build phase attributes should be overrided, such as: backbone.num_seg. + Trim it here. + """ + model_name = cfg.model_name + if cfg.MODEL.get('backbone') and cfg.MODEL.backbone.get('pretrained'): + cfg.MODEL.backbone.pretrained = "" # not ued when inference + + return cfg, model_name + + +def get_input_spec(cfg, model_name): + if model_name in ['ppTSM', 'TSM', 'MoViNet']: + input_spec = [[ + InputSpec( + shape=[None, cfg.num_seg, 3, cfg.target_size, cfg.target_size], + dtype='float32'), + ]] + elif model_name in ['TSN', 'ppTSN']: + input_spec = [[ + InputSpec(shape=[ + None, cfg.num_seg * 10, 3, cfg.target_size, cfg.target_size + ], + dtype='float32'), + ]] + elif model_name in ['BMN']: + input_spec = [[ + InputSpec(shape=[None, cfg.feat_dim, cfg.tscale], + dtype='float32', + name='feat_input'), + ]] + elif model_name in ['TimeSformer', 'ppTimeSformer']: + input_spec = [[ + InputSpec(shape=[ + None, 3, cfg.num_seg * 3, cfg.target_size, cfg.target_size + ], + dtype='float32'), + ]] + elif model_name in ['VideoSwin']: + input_spec = [[ + InputSpec(shape=[ + None, 3, cfg.num_seg * cfg.seg_len * 1, cfg.target_size, + cfg.target_size + ], + dtype='float32'), + ]] + elif model_name in ['VideoSwin_TableTennis']: + input_spec = [[ + InputSpec(shape=[ + None, 3, cfg.num_seg * cfg.seg_len * 3, cfg.target_size, + cfg.target_size + ], + dtype='float32'), + ]] + elif model_name in ['AttentionLSTM']: + input_spec = [[ + InputSpec(shape=[None, cfg.embedding_size, cfg.feature_dims[0]], + dtype='float32'), # for rgb_data + InputSpec(shape=[ + None, + ], dtype='int64'), # for rgb_len + InputSpec(shape=[None, cfg.embedding_size, cfg.feature_dims[0]], + dtype='float32'), # for rgb_mask + InputSpec(shape=[None, cfg.embedding_size, cfg.feature_dims[1]], + dtype='float32'), # for audio_data + InputSpec(shape=[ + None, + ], dtype='int64'), # for audio_len + InputSpec(shape=[None, cfg.embedding_size, cfg.feature_dims[1]], + dtype='float32'), # for audio_mask + ]] + elif model_name in ['SlowFast']: + input_spec = [[ + InputSpec(shape=[ + None, 3, cfg.num_frames // cfg.alpha, cfg.target_size, + cfg.target_size + ], + dtype='float32', + name='slow_input'), + InputSpec(shape=[ + None, 3, cfg.num_frames, cfg.target_size, cfg.target_size + ], + dtype='float32', + name='fast_input'), + ]] + elif model_name in ['STGCN', 'AGCN', 'CTRGCN']: + input_spec = [[ + InputSpec(shape=[ + None, cfg.num_channels, cfg.window_size, cfg.vertex_nums, + cfg.person_nums + ], + dtype='float32'), + ]] + elif model_name in ['TransNetV2']: + input_spec = [[ + InputSpec(shape=[ + None, + cfg.num_frames, + cfg.height, + cfg.width, + cfg.num_channels, + ], + dtype='float32'), + ]] + elif model_name in ['MSTCN', 'ASRF']: + input_spec = [[ + InputSpec(shape=[None, cfg.num_channels, None], dtype='float32'), + ]] + elif model_name in ['ADDS']: + input_spec = [[ + InputSpec(shape=[None, cfg.num_channels, cfg.height, cfg.width], + dtype='float32'), + ]] + elif model_name in ['AVA_SlowFast_FastRcnn']: + input_spec = [[ + InputSpec(shape=[ + None, 3, cfg.num_frames // cfg.alpha, cfg.target_size, + cfg.target_size + ], + dtype='float32', + name='slow_input'), + InputSpec(shape=[ + None, 3, cfg.num_frames, cfg.target_size, cfg.target_size + ], + dtype='float32', + name='fast_input'), + InputSpec(shape=[None, None, 4], dtype='float32', name='proposals'), + InputSpec(shape=[None, 2], dtype='float32', name='img_shape') + ]] + return input_spec + + +def main(): + args = parse_args() + cfg, model_name = trim_config(get_config(args.config, show=False)) + print(f"Building model({model_name})...") + model = build_model(cfg.MODEL) + assert osp.isfile( + args.pretrained_params + ), f"pretrained params ({args.pretrained_params} is not a file path.)" + + if not os.path.isdir(args.output_path): + os.makedirs(args.output_path) + + print(f"Loading params from ({args.pretrained_params})...") + params = paddle.load(args.pretrained_params) + model.set_dict(params) + + model.eval() + + input_spec = get_input_spec(cfg.INFERENCE, model_name) + model = to_static(model, input_spec=input_spec) + paddle.jit.save( + model, + osp.join(args.output_path, + model_name if args.save_name is None else args.save_name)) + print( + f"model ({model_name}) has been already saved in ({args.output_path}).") + + +if __name__ == "__main__": + main() diff --git a/tools/paddlevideo_clas.py b/tools/paddlevideo_clas.py new file mode 100644 index 0000000000000000000000000000000000000000..4843e62f8b8e31ad6d6678cb9b1bb24c2e23e718 --- /dev/null +++ b/tools/paddlevideo_clas.py @@ -0,0 +1,333 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +__dir__ = os.path.dirname(__file__) +sys.path.append(os.path.join(__dir__, '')) + + +import numpy as np +import tarfile +import requests +from tqdm import tqdm +from tools import utils +import shutil + +from paddle.inference import Config +from paddle.inference import create_predictor + +__all__ = ['PaddleVideo'] +BASE_DIR = os.path.expanduser("~/.paddlevideo_inference/") +BASE_INFERENCE_MODEL_DIR = os.path.join(BASE_DIR, 'inference_model') +BASE_VIDEOS_DIR = os.path.join(BASE_DIR, 'videos') + +model_names = {'ppTSM','TSM','TSN'} + + +def create_paddle_predictor(args): + config = Config(args.model_file, args.params_file) + + if args.use_gpu: + config.enable_use_gpu(args.gpu_mem, 0) + else: + config.disable_gpu() + if args.enable_mkldnn: + # cache 10 different shapes for mkldnn to avoid memory leak + config.set_mkldnn_cache_capacity(10) + config.enable_mkldnn() + + config.disable_glog_info() + config.switch_ir_optim(args.ir_optim) # default true + if args.use_tensorrt: + config.enable_tensorrt_engine( + precision_mode=Config.Precision.Half + if args.use_fp16 else Config.Precision.Float32, + max_batch_size=args.batch_size) + + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + predictor = create_predictor(config) + + return predictor + +def download_with_progressbar(url, save_path): + response = requests.get(url, stream=True) + total_size_in_bytes = int(response.headers.get('content-length', 0)) + block_size = 1024 # 1 Kibibyte + progress_bar = tqdm(total=total_size_in_bytes, unit='iB', unit_scale=True) + with open(save_path, 'wb') as file: + for data in response.iter_content(block_size): + progress_bar.update(len(data)) + file.write(data) + progress_bar.close() + if total_size_in_bytes == 0 or progress_bar.n != total_size_in_bytes: + raise Exception("Something went wrong while downloading models") + +def maybe_download(model_storage_directory, url): + # using custom model + tar_file_name_list = [ + 'inference.pdiparams', 'inference.pdiparams.info', 'inference.pdmodel' + ] + if not os.path.exists( + os.path.join(model_storage_directory, 'inference.pdiparams') + ) or not os.path.exists( + os.path.join(model_storage_directory, 'inference.pdmodel')): + tmp_path = os.path.join(model_storage_directory, url.split('/')[-1]) + print('download {} to {}'.format(url, tmp_path)) + os.makedirs(model_storage_directory, exist_ok=True) + download_with_progressbar(url, tmp_path) #download + + #save to directory + with tarfile.open(tmp_path, 'r') as tarObj: + for member in tarObj.getmembers(): + filename = None + for tar_file_name in tar_file_name_list: + if tar_file_name in member.name: + filename = tar_file_name + if filename is None: + continue + file = tarObj.extractfile(member) + with open( + os.path.join(model_storage_directory, filename), + 'wb') as f: + f.write(file.read()) + os.remove(tmp_path) + +def load_label_name_dict(path): + result = {} + if not os.path.exists(path): + print( + 'Warning: If want to use your own label_dict, please input legal path!\nOtherwise label_names will be empty!' + ) + else: + for line in open(path, 'r'): + partition = line.split('\n')[0].partition(' ') + try: + result[int(partition[0])] = str(partition[-1]) + except: + result = {} + break + return result + +def parse_args(mMain=True, add_help=True): + import argparse + + def str2bool(v): + return v.lower() in ("true", "t", "1") + + if mMain == True: + + # general params + parser = argparse.ArgumentParser(add_help=add_help) + parser.add_argument("--model_name", type=str,default='') + parser.add_argument("-v", "--video_file", type=str,default='') + parser.add_argument("--use_gpu", type=str2bool, default=True) + + # params for decode and sample + parser.add_argument("--num_seg", type=int, default=8) + parser.add_argument("--seg_len", type=int, default=1) + + # params for preprocess + parser.add_argument("--short_size", type=int, default=256) + parser.add_argument("--target_size", type=int, default=224) + parser.add_argument("--normalize", type=str2bool, default=True) + + # params for predict + parser.add_argument("--model_file", type=str,default='') + parser.add_argument("--params_file", type=str) + parser.add_argument("-b", "--batch_size", type=int, default=1) + parser.add_argument("--use_fp16", type=str2bool, default=False) + parser.add_argument("--ir_optim", type=str2bool, default=True) + parser.add_argument("--use_tensorrt", type=str2bool, default=False) + parser.add_argument("--gpu_mem", type=int, default=8000) + parser.add_argument("--top_k", type=int, default=1) + parser.add_argument("--enable_mkldnn", type=bool, default=False) + parser.add_argument("--label_name_path",type=str,default='') + + return parser.parse_args() + + else: + return argparse.Namespace( + model_name='', + video_file='', + use_gpu=False, + num_seg=8, + seg_len=1, + short_size=256, + target_size=224, + normalize=True, + model_file='', + params_file='', + batch_size=1, + use_fp16=False, + ir_optim=True, + use_tensorrt=False, + gpu_mem=8000, + top_k=1, + enable_mkldnn=False, + label_name_path='') + +def get_video_list(video_file): + videos_lists = [] + if video_file is None or not os.path.exists(video_file): + raise Exception("not found any video file in {}".format(video_file)) + + video_end = ['mp4','avi'] + if os.path.isfile(video_file) and video_file.split('.')[-1] in video_end: + videos_lists.append(video_file) + elif os.path.isdir(video_file): + for single_file in os.listdir(video_file): + if single_file.split('.')[-1] in video_end: + videos_lists.append(os.path.join(video_file, single_file)) + if len(videos_lists) == 0: + raise Exception("not found any video file in {}".format(video_file)) + return videos_lists + +class PaddleVideo(object): + print('Inference models that Paddle provides are listed as follows:\n\n{}'. + format(model_names), '\n') + + def __init__(self, **kwargs): + process_params = parse_args(mMain=False,add_help=False) + process_params.__dict__.update(**kwargs) + + if not os.path.exists(process_params.model_file): + if process_params.model_name is None: + raise Exception( + 'Please input model name that you want to use!') + if process_params.model_name in model_names: + url = 'https://videotag.bj.bcebos.com/PaddleVideo/InferenceModel/{}_infer.tar'.format(process_params.model_name) + if not os.path.exists( + os.path.join(BASE_INFERENCE_MODEL_DIR, + process_params.model_name)): + os.makedirs( + os.path.join(BASE_INFERENCE_MODEL_DIR, + process_params.model_name)) + #create pretrained model download_path + download_path = os.path.join(BASE_INFERENCE_MODEL_DIR, + process_params.model_name) + maybe_download(model_storage_directory=download_path, url=url) + process_params.model_file = os.path.join(download_path, + 'inference.pdmodel') + process_params.params_file = os.path.join( + download_path, 'inference.pdiparams') + process_params.label_name_path = os.path.join( + __dir__, '../data/k400/Kinetics-400_label_list.txt') + else: + raise Exception( + 'If you want to use your own model, Please input model_file as model path!' + ) + else: + print('Using user-specified model and params!') + print("process params are as follows: \n{}".format(process_params)) + self.label_name_dict = load_label_name_dict( + process_params.label_name_path) + + self.args = process_params + self.predictor = create_paddle_predictor(process_params) + + def predict(self,video): + """ + predict label of video with paddlevideo_clas + Args: + video:input video for clas, support single video , internet url, folder path containing series of videos + Returns: + list[dict:{videoname: "",class_ids: [], scores: [], label_names: []}],if label name path is None,label names will be empty + """ + video_list = [] + assert isinstance(video, (str, np.ndarray)) + + input_names = self.predictor.get_input_names() + input_tensor = self.predictor.get_input_handle(input_names[0]) + + output_names = self.predictor.get_output_names() + output_tensor = self.predictor.get_output_handle(output_names[0]) + + if isinstance(video, str): + # download internet video, + if video.startswith('http'): + if not os.path.exists(BASE_VIDEOS_DIR): + os.makedirs(BASE_VIDEOS_DIR) + video_path = os.path.join(BASE_VIDEOS_DIR, 'tmp.mp4') + download_with_progressbar(video, video_path) + print("Current using video from Internet:{}, renamed as: {}". + format(video, video_path)) + video = video_path + video_list = get_video_list(video) + else: + if isinstance(video, np.ndarray): + video_list = [video] + else: + print('Please input legal video!') + + total_result = [] + for filename in video_list: + if isinstance(filename, str): + v = utils.decode(filename, self.args) + assert v is not None, "Error in loading video: {}".format( + filename) + inputs = utils.preprocess(v, self.args) + inputs = np.expand_dims( + inputs, axis=0).repeat( + 1, axis=0).copy() + else: + inputs = filename + + input_tensor.copy_from_cpu(inputs) + + self.predictor.run() + + outputs = output_tensor.copy_to_cpu() + classes, scores = utils.postprocess(outputs, self.args) + label_names = [] + if len(self.label_name_dict) != 0: + label_names = [self.label_name_dict[c] for c in classes] + result = { + "videoname": filename if isinstance(filename, str) else 'video', + "class_ids": classes.tolist(), + "scores": scores.tolist(), + "label_names": label_names, + } + total_result.append(result) + return total_result + +def main(): + # for cmd + args = parse_args(mMain=True) + clas_engine = PaddleVideo(**(args.__dict__)) + print('{}{}{}'.format('*' * 10, args.video_file, '*' * 10)) + result = clas_engine.predict(args.video_file) + if result is not None: + print(result) + + +if __name__ == '__main__': + main() diff --git a/tools/predict.py b/tools/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..9ab30fd025608bfe68422be34dd262cc6509aebe --- /dev/null +++ b/tools/predict.py @@ -0,0 +1,275 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +from os import path as osp +import paddle +from paddle import inference +from paddle.inference import Config, create_predictor + +from utils import build_inference_helper +from paddlevideo.utils import get_config + + +def parse_args(): + def str2bool(v): + return v.lower() in ("true", "t", "1") + + # general params + parser = argparse.ArgumentParser("PaddleVideo Inference model script") + parser.add_argument('-c', + '--config', + type=str, + default='configs/example.yaml', + help='config file path') + parser.add_argument("-i", "--input_file", type=str, help="input file path") + parser.add_argument("--model_file", type=str) + parser.add_argument("--params_file", type=str) + + # params for paddle predict + parser.add_argument("-b", "--batch_size", type=int, default=1) + parser.add_argument("--use_gpu", type=str2bool, default=True) + parser.add_argument("--precision", type=str, default="fp32") + parser.add_argument("--ir_optim", type=str2bool, default=True) + parser.add_argument("--use_tensorrt", type=str2bool, default=False) + parser.add_argument("--gpu_mem", type=int, default=8000) + parser.add_argument("--enable_benchmark", type=str2bool, default=False) + parser.add_argument("--enable_mkldnn", type=str2bool, default=False) + parser.add_argument("--cpu_threads", type=int, default=None) + # parser.add_argument("--hubserving", type=str2bool, default=False) #TODO + + return parser.parse_args() + + +def create_paddle_predictor(args, cfg): + config = Config(args.model_file, args.params_file) + if args.use_gpu: + config.enable_use_gpu(args.gpu_mem, 0) + else: + config.disable_gpu() + if args.cpu_threads: + config.set_cpu_math_library_num_threads(args.cpu_threads) + if args.enable_mkldnn: + # cache 10 different shapes for mkldnn to avoid memory leak + config.set_mkldnn_cache_capacity(10) + config.enable_mkldnn() + if args.precision == "fp16": + config.enable_mkldnn_bfloat16() + + # config.disable_glog_info() + config.switch_ir_optim(args.ir_optim) # default true + if args.use_tensorrt: + # choose precision + if args.precision == "fp16": + precision = inference.PrecisionType.Half + elif args.precision == "int8": + precision = inference.PrecisionType.Int8 + else: + precision = inference.PrecisionType.Float32 + + # calculate real max batch size during inference when tenrotRT enabled + max_batch_size = args.batch_size + if 'num_seg' in cfg.INFERENCE: + # num_seg: number of segments when extracting frames. + # seg_len: number of frames extracted within a segment, default to 1. + # num_views: the number of video frame groups obtained by cropping and flipping, + # uniformcrop=3, tencrop=10, centercrop=1. + num_seg = cfg.INFERENCE.num_seg + seg_len = cfg.INFERENCE.get('seg_len', 1) + num_views = 1 + if 'tsm' in cfg.model_name.lower(): + num_views = 1 # CenterCrop + elif 'tsn' in cfg.model_name.lower(): + num_views = 10 # TenCrop + elif 'timesformer' in cfg.model_name.lower(): + num_views = 3 # UniformCrop + elif 'videoswin' in cfg.model_name.lower(): + num_views = 3 # UniformCrop + max_batch_size = args.batch_size * num_views * num_seg * seg_len + config.enable_tensorrt_engine(precision_mode=precision, + max_batch_size=max_batch_size) + + config.enable_memory_optim() + # use zero copy + config.switch_use_feed_fetch_ops(False) + + # for ST-GCN tensorRT case usage + # config.delete_pass("shuffle_channel_detect_pass") + + predictor = create_predictor(config) + + return config, predictor + + +def parse_file_paths(input_path: str) -> list: + if osp.isfile(input_path): + files = [ + input_path, + ] + else: + files = os.listdir(input_path) + files = [ + file for file in files + if (file.endswith(".avi") or file.endswith(".mp4")) + ] + files = [osp.join(input_path, file) for file in files] + return files + + +def main(): + """predict using paddle inference model + """ + args = parse_args() + cfg = get_config(args.config, show=False) + + model_name = cfg.model_name + print(f"Inference model({model_name})...") + InferenceHelper = build_inference_helper(cfg.INFERENCE) + + inference_config, predictor = create_paddle_predictor(args, cfg) + + # get input_tensor and output_tensor + input_names = predictor.get_input_names() + output_names = predictor.get_output_names() + input_tensor_list = [] + output_tensor_list = [] + for item in input_names: + input_tensor_list.append(predictor.get_input_handle(item)) + for item in output_names: + output_tensor_list.append(predictor.get_output_handle(item)) + + # get the absolute file path(s) to be processed + if model_name in ["MSTCN", "ASRF"]: + files = InferenceHelper.get_process_file(args.input_file) + else: + files = parse_file_paths(args.input_file) + + if model_name == 'TransNetV2': + for file in files: + inputs = InferenceHelper.preprocess(file) + outputs = [] + for input in inputs: + # Run inference + for i in range(len(input_tensor_list)): + input_tensor_list[i].copy_from_cpu(input) + predictor.run() + output = [] + for j in range(len(output_tensor_list)): + output.append(output_tensor_list[j].copy_to_cpu()) + outputs.append(output) + + # Post process output + InferenceHelper.postprocess(outputs) + + elif model_name == 'AVA_SlowFast_FastRcnn': + for file in files: # for videos + inputs = InferenceHelper.preprocess(file) + outputs = [] + for input in inputs: + # Run inference + input_len = len(input_tensor_list) + + for i in range(input_len): + if type(input[i]) == paddle.Tensor: + input_tmp = input[i].numpy() + else: + input_tmp = input[i] + input_tensor_list[i].copy_from_cpu(input_tmp) + predictor.run() + output = [] + for j in range(len(output_tensor_list)): + output.append(output_tensor_list[j].copy_to_cpu()) + outputs.append(output) + + # Post process output + InferenceHelper.postprocess(outputs) + else: + if args.enable_benchmark: + test_video_num = 12 + num_warmup = 3 + + # instantiate auto log + try: + import auto_log + except ImportError as e: + print(f"{e}, [git+https://github.com/LDOUBLEV/AutoLog] " + f"package and it's dependencies is required for " + f"python-inference when enable_benchmark=True.") + pid = os.getpid() + autolog = auto_log.AutoLogger(model_name=cfg.model_name, + model_precision=args.precision, + batch_size=args.batch_size, + data_shape="dynamic", + save_path="./output/auto_log.lpg", + inference_config=inference_config, + pids=pid, + process_name=None, + gpu_ids=0 if args.use_gpu else None, + time_keys=[ + 'preprocess_time', + 'inference_time', + 'postprocess_time' + ], + warmup=num_warmup) + files = [ + args.input_file for _ in range(test_video_num + num_warmup) + ] + + # Inferencing process + batch_num = args.batch_size + for st_idx in range(0, len(files), batch_num): + ed_idx = min(st_idx + batch_num, len(files)) + + # auto log start + if args.enable_benchmark: + autolog.times.start() + + # Pre process batched input + batched_inputs = InferenceHelper.preprocess_batch( + files[st_idx:ed_idx]) + + # get pre process time cost + if args.enable_benchmark: + autolog.times.stamp() + + # run inference + for i in range(len(input_tensor_list)): + input_tensor_list[i].copy_from_cpu(batched_inputs[i]) + predictor.run() + + batched_outputs = [] + for j in range(len(output_tensor_list)): + batched_outputs.append(output_tensor_list[j].copy_to_cpu()) + + # get inference process time cost + if args.enable_benchmark: + autolog.times.stamp() + + InferenceHelper.postprocess(batched_outputs, + not args.enable_benchmark) + + # get post process time cost + if args.enable_benchmark: + autolog.times.end(stamp=True) + + # time.sleep(0.01) # sleep for T4 GPU + + # report benchmark log if enabled + if args.enable_benchmark: + autolog.report() + + +if __name__ == "__main__": + main() diff --git a/tools/summary.py b/tools/summary.py new file mode 100644 index 0000000000000000000000000000000000000000..28bd6f7ebb945acfdb9be3c8c5a9ce62b34156c9 --- /dev/null +++ b/tools/summary.py @@ -0,0 +1,82 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import sys +import os.path as osp + +import paddle +import paddle.nn.functional as F +from paddle.jit import to_static +import paddleslim + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../'))) + +from paddlevideo.modeling.builder import build_model +from paddlevideo.utils import get_config + + +def parse_args(): + + parser = argparse.ArgumentParser("PaddleVideo Summary") + parser.add_argument('-c', + '--config', + type=str, + default='configs/example.yaml', + help='config file path') + + parser.add_argument("--img_size", type=int, default=224) + parser.add_argument("--num_seg", type=int, default=8) + parser.add_argument("--FLOPs", + action="store_true", + help="whether to print FLOPs") + + return parser.parse_args() + + +def _trim(cfg, args): + """ + Reuse the trainging config will bring useless attribute, such as: backbone.pretrained model. Trim it here. + """ + model_name = cfg.model_name + cfg = cfg.MODEL + cfg.backbone.pretrained = "" + + if 'num_seg' in cfg.backbone: + cfg.backbone.num_seg = args.num_seg + return cfg, model_name + + +def main(): + args = parse_args() + cfg, model_name = _trim(get_config(args.config, show=False), args) + print(f"Building model({model_name})...") + model = build_model(cfg) + + img_size = args.img_size + num_seg = args.num_seg + #NOTE: only support tsm now, will refine soon + params_info = paddle.summary(model, (1, 1, num_seg, 3, img_size, img_size)) + print(params_info) + + if args.FLOPs: + flops_info = paddleslim.analysis.flops( + model, [1, 1, num_seg, 3, img_size, img_size]) + print(flops_info) + + +if __name__ == "__main__": + main() diff --git a/tools/utils.py b/tools/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b59cb96a4e690258a0227388139d68b187cfd2b6 --- /dev/null +++ b/tools/utils.py @@ -0,0 +1,1473 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +import shutil +import sys +from typing import List + +import cv2 +try: + import imageio +except ImportError as e: + print( + f"{e}, [imageio] package and it's dependencies is required for VideoSwin." + ) +try: + import matplotlib as mpl + import matplotlib.cm as cm +except ImportError as e: + print( + f"{e}, [matplotlib] package and it's dependencies is required for ADDS." + ) +import numpy as np +import paddle +import paddle.nn.functional as F +import pandas +from PIL import Image + +__dir__ = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.abspath(os.path.join(__dir__, '../'))) +from abc import abstractmethod + +from paddlevideo.loader.builder import build_pipeline +from paddlevideo.loader.pipelines import ( + AutoPadding, CenterCrop, DecodeSampler, FeatureDecoder, FrameDecoder, + GroupResize, Image2Array, ImageDecoder, JitterScale, MultiCrop, + Normalization, PackOutput, Sampler, SamplerPkl, Scale, SkeletonNorm, + TenCrop, ToArray, UniformCrop, VideoDecoder, SegmentationSampler, + SketeonCropSample) +from paddlevideo.metrics.ava_utils import read_labelmap +from paddlevideo.metrics.bmn_metric import boundary_choose, soft_nms +from paddlevideo.utils import Registry, build, get_config +from paddlevideo.modeling.framework.segmenters.utils import ASRFPostProcessing + +from ava_predict import (detection_inference, frame_extraction, + get_detection_result, get_timestep_result, pack_result, + visualize) + +INFERENCE = Registry('inference') + + +def decode(filepath, args): + num_seg = args.num_seg + seg_len = args.seg_len + + cap = cv2.VideoCapture(filepath) + videolen = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + sampledFrames = [] + for i in range(videolen): + ret, frame = cap.read() + # maybe first frame is empty + if ret == False: + continue + img = frame[:, :, ::-1] + sampledFrames.append(img) + average_dur = int(len(sampledFrames) / num_seg) + imgs = [] + for i in range(num_seg): + idx = 0 + if average_dur >= seg_len: + idx = (average_dur - 1) // 2 + idx += i * average_dur + elif average_dur >= 1: + idx += i * average_dur + else: + idx = i + + for jj in range(idx, idx + seg_len): + imgbuf = sampledFrames[int(jj % len(sampledFrames))] + img = Image.fromarray(imgbuf, mode='RGB') + imgs.append(img) + + return imgs + + +def preprocess(img, args): + img = {"imgs": img} + resize_op = Scale(short_size=args.short_size) + img = resize_op(img) + ccrop_op = CenterCrop(target_size=args.target_size) + img = ccrop_op(img) + to_array = Image2Array() + img = to_array(img) + if args.normalize: + img_mean = [0.485, 0.456, 0.406] + img_std = [0.229, 0.224, 0.225] + normalize_op = Normalization(mean=img_mean, std=img_std) + img = normalize_op(img) + return img['imgs'] + + +def postprocess(output, args): + output = output.flatten() + output = F.softmax(paddle.to_tensor(output)).numpy() + classes = np.argpartition(output, -args.top_k)[-args.top_k:] + classes = classes[np.argsort(-output[classes])] + scores = output[classes] + return classes, scores + + +def build_inference_helper(cfg): + return build(cfg, INFERENCE) + + +class Base_Inference_helper(): + def __init__(self, + num_seg=8, + seg_len=1, + short_size=256, + target_size=224, + top_k=1): + """Base_Inference_helper + + Args: + num_seg (int, optional): number of segmentations of an sliced input video. Defaults to 8. + seg_len (int, optional): length of each segmentation. Defaults to 1. + short_size (int, optional): short size of input video. Defaults to 256. + target_size (int, optional): size of cropped video. Defaults to 224. + top_k (int, optional): select topk result in outputs. Defaults to 1. + """ + self.num_seg = num_seg + self.seg_len = seg_len + self.short_size = short_size + self.target_size = target_size + self.top_k = top_k + + @abstractmethod + def preprocess(self, input_file: str): + """preprocess abstractmethod + + Args: + input_file (str): input file path. + """ + pass + + def preprocess_batch(self, file_list: List[str]) -> List[np.ndarray]: + """preprocess for file list + + Args: + file_list (List[str]): file pathes in an list, [path1, path2, ...]. + + Returns: + List[np.ndarray]: batched inputs data, [data_batch[0], data_batch[1], ...]. + """ + batched_inputs = [] + for file in file_list: + inputs = self.preprocess(file) + batched_inputs.append(inputs) + batched_inputs = [ + np.concatenate([item[i] for item in batched_inputs]) + for i in range(len(batched_inputs[0])) + ] + self.input_file = file_list + return batched_inputs + + def postprocess(self, + output: np.ndarray, + print_output: bool = True) -> None: + """postprocess + + Args: + output (np.ndarray): batched output scores, shape of (batch_size, class_num). + print_output (bool, optional): whether to print result. Defaults to True. + """ + if not isinstance(self.input_file, list): + self.input_file = [ + self.input_file, + ] + output = output[0] # [B, num_cls] + N = len(self.input_file) + if output.shape[0] != N: + output = output.reshape([N] + [output.shape[0] // N] + + list(output.shape[1:])) # [N, T, C] + output = output.mean(axis=1) # [N, C] + output = F.softmax(paddle.to_tensor(output), axis=-1).numpy() + for i in range(N): + classes = np.argpartition(output[i], -self.top_k)[-self.top_k:] + classes = classes[np.argsort(-output[i, classes])] + scores = output[i, classes] + if print_output: + print("Current video file: {0}".format(self.input_file[i])) + for j in range(self.top_k): + print("\ttop-{0} class: {1}".format(j + 1, classes[j])) + print("\ttop-{0} score: {1}".format(j + 1, scores[j])) + + +@INFERENCE.register() +class ppTSM_Inference_helper(Base_Inference_helper): + def __init__(self, + num_seg=8, + seg_len=1, + short_size=256, + target_size=224, + top_k=1): + self.num_seg = num_seg + self.seg_len = seg_len + self.short_size = short_size + self.target_size = target_size + self.top_k = top_k + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + results = {'filename': input_file} + img_mean = [0.485, 0.456, 0.406] + img_std = [0.229, 0.224, 0.225] + ops = [ + VideoDecoder(), + Sampler(self.num_seg, self.seg_len, valid_mode=True), + Scale(self.short_size), + CenterCrop(self.target_size), + Image2Array(), + Normalization(img_mean, img_std) + ] + for op in ops: + results = op(results) + + res = np.expand_dims(results['imgs'], axis=0).copy() + return [res] + + +@INFERENCE.register() +class ppTSN_Inference_helper(Base_Inference_helper): + def __init__(self, + num_seg=25, + seg_len=1, + short_size=256, + target_size=224, + top_k=1): + self.num_seg = num_seg + self.seg_len = seg_len + self.short_size = short_size + self.target_size = target_size + self.top_k = top_k + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + results = {'filename': input_file} + img_mean = [0.485, 0.456, 0.406] + img_std = [0.229, 0.224, 0.225] + ops = [ + VideoDecoder(), + Sampler(self.num_seg, + self.seg_len, + valid_mode=True, + select_left=True), + Scale(self.short_size, + fixed_ratio=True, + do_round=True, + backend='cv2'), + TenCrop(self.target_size), + Image2Array(), + Normalization(img_mean, img_std) + ] + for op in ops: + results = op(results) + + res = np.expand_dims(results['imgs'], axis=0).copy() + return [res] + + +@INFERENCE.register() +class BMN_Inference_helper(Base_Inference_helper): + def __init__(self, feat_dim, dscale, tscale, result_path): + self.feat_dim = feat_dim + self.dscale = dscale + self.tscale = tscale + self.result_path = result_path + if not os.path.isdir(self.result_path): + os.makedirs(self.result_path) + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + file_info = json.load(open(input_file)) + self.feat_path = file_info['feat_path'] + self.video_duration = file_info['duration_second'] + feat = np.load(self.feat_path).astype('float32').T + res = np.expand_dims(feat, axis=0).copy() + + return [res] + + def postprocess(self, outputs, print_output=True): + """ + output: list + """ + pred_bm, pred_start, pred_end = outputs + self._gen_props(pred_bm, pred_start[0], pred_end[0], print_output) + + def _gen_props(self, pred_bm, pred_start, pred_end, print_output): + snippet_xmins = [1.0 / self.tscale * i for i in range(self.tscale)] + snippet_xmaxs = [ + 1.0 / self.tscale * i for i in range(1, self.tscale + 1) + ] + + pred_bm = pred_bm[0, 0, :, :] * pred_bm[0, 1, :, :] + start_mask = boundary_choose(pred_start) + start_mask[0] = 1. + end_mask = boundary_choose(pred_end) + end_mask[-1] = 1. + score_vector_list = [] + for idx in range(self.dscale): + for jdx in range(self.tscale): + start_index = jdx + end_index = start_index + idx + if end_index < self.tscale and start_mask[ + start_index] == 1 and end_mask[end_index] == 1: + xmin = snippet_xmins[start_index] + xmax = snippet_xmaxs[end_index] + xmin_score = pred_start[start_index] + xmax_score = pred_end[end_index] + bm_score = pred_bm[idx, jdx] + conf_score = xmin_score * xmax_score * bm_score + score_vector_list.append([xmin, xmax, conf_score]) + + cols = ["xmin", "xmax", "score"] + score_vector_list = np.stack(score_vector_list) + df = pandas.DataFrame(score_vector_list, columns=cols) + + result_dict = {} + proposal_list = [] + df = soft_nms(df, alpha=0.4, t1=0.55, t2=0.9) + for idx in range(min(100, len(df))): + tmp_prop={"score":df.score.values[idx], \ + "segment":[max(0,df.xmin.values[idx])*self.video_duration, \ + min(1,df.xmax.values[idx])*self.video_duration]} + proposal_list.append(tmp_prop) + + result_dict[self.feat_path] = proposal_list + + # print top-5 predictions + if print_output: + print("Current video file: {0} :".format(self.feat_path)) + for pred in proposal_list[:5]: + print(pred) + + # save result + outfile = open( + os.path.join(self.result_path, "bmn_results_inference.json"), "w") + + json.dump(result_dict, outfile) + + +@INFERENCE.register() +class TimeSformer_Inference_helper(Base_Inference_helper): + def __init__(self, + num_seg=8, + seg_len=1, + short_size=224, + target_size=224, + top_k=1, + mean=[0.45, 0.45, 0.45], + std=[0.225, 0.225, 0.225]): + self.num_seg = num_seg + self.seg_len = seg_len + self.short_size = short_size + self.target_size = target_size + self.top_k = top_k + self.mean = mean + self.std = std + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + results = {'filename': input_file} + ops = [ + VideoDecoder(backend='pyav', mode='test', num_seg=self.num_seg), + Sampler(self.num_seg, + self.seg_len, + valid_mode=True, + linspace_sample=True), + Normalization(self.mean, self.std, tensor_shape=[1, 1, 1, 3]), + Image2Array(data_format='cthw'), + JitterScale(self.short_size, self.short_size), + UniformCrop(self.target_size) + ] + for op in ops: + results = op(results) + + # [N,C,Tx3,H,W] + res = np.expand_dims(results['imgs'], axis=0).copy() + return [res] + + +@INFERENCE.register() +class VideoSwin_Inference_helper(Base_Inference_helper): + def __init__(self, + num_seg=4, + seg_len=32, + frame_interval=2, + short_size=224, + target_size=224, + top_k=1, + mean=[123.675, 116.28, 103.53], + std=[58.395, 57.12, 57.375]): + + self.num_seg = num_seg + self.seg_len = seg_len + self.frame_interval = frame_interval + self.short_size = short_size + self.target_size = target_size + self.top_k = top_k + self.mean = mean + self.std = std + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + self.input_file = input_file + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + results = {'filename': input_file} + ops = [ + VideoDecoder(backend='decord', mode='valid'), + Sampler(num_seg=self.num_seg, + frame_interval=self.frame_interval, + seg_len=self.seg_len, + valid_mode=True, + use_pil=False), + Scale(short_size=self.short_size, + fixed_ratio=False, + keep_ratio=True, + backend='cv2', + do_round=True), + CenterCrop(target_size=224, backend='cv2'), + Normalization(mean=self.mean, + std=self.std, + tensor_shape=[3, 1, 1, 1], + inplace=True), + Image2Array(data_format='cthw') + ] + for op in ops: + results = op(results) + + res = np.expand_dims(results['imgs'], axis=0).copy() + return [res] + + def postprocess(self, output, print_output=True): + """ + output: list + """ + if not isinstance(self.input_file, list): + self.input_file = [ + self.input_file, + ] + output = output[0] # [B, num_cls] + N = len(self.input_file) + if output.shape[0] != N: + output = output.reshape([N] + [output.shape[0] // N] + + list(output.shape[1:])) # [N, T, C] + output = output.mean(axis=1) # [N, C] + for i in range(N): + classes = np.argpartition(output[i], -self.top_k)[-self.top_k:] + classes = classes[np.argsort(-output[i, classes])] + scores = output[i, classes] + if print_output: + print("Current video file: {0}".format(self.input_file[i])) + for j in range(self.top_k): + print("\ttop-{0} class: {1}".format(j + 1, classes[j])) + print("\ttop-{0} score: {1}".format(j + 1, scores[j])) + + +@INFERENCE.register() +class VideoSwin_TableTennis_Inference_helper(Base_Inference_helper): + def __init__(self, + num_seg=1, + seg_len=32, + short_size=256, + target_size=224, + top_k=1): + self.num_seg = num_seg + self.seg_len = seg_len + self.short_size = short_size + self.target_size = target_size + self.top_k = top_k + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + results = {'frame_dir': input_file, 'suffix': 'img_{:05}.jpg'} + img_mean = [123.675, 116.28, 103.53] + img_std = [58.395, 57.12, 57.375] + ops = [ + FrameDecoder(), + SamplerPkl(num_seg=self.num_seg, + seg_len=self.seg_len, + backend='cv2', + valid_mode=True), + Scale(short_size=self.short_size, + fixed_ratio=False, + keep_ratio=True, + backend='cv2', + do_round=True), + UniformCrop(target_size=self.target_size, backend='cv2'), + Normalization(mean=img_mean, + std=img_std, + tensor_shape=[3, 1, 1, 1], + inplace=True), + Image2Array(data_format='cthw') + ] + for op in ops: + results = op(results) + + res = np.expand_dims(results['imgs'], axis=0).copy() + return [res] + + def add_text_to_video( + self, + video_path, + output_dir="applications/TableTennis/ActionRecognition/results", + text=None): + os.makedirs(output_dir, exist_ok=True) + if video_path.endswith('.pkl'): + try: + import cPickle as pickle + from cStringIO import StringIO + except ImportError: + import pickle + from io import BytesIO + from PIL import Image + data_loaded = pickle.load(open(video_path, 'rb'), encoding='bytes') + _, _, frames = data_loaded + frames_len = len(frames) + + else: + videoCapture = cv2.VideoCapture() + videoCapture.open(video_path) + + fps = videoCapture.get(cv2.CAP_PROP_FPS) + frame_width = int(videoCapture.get(cv2.CAP_PROP_FRAME_WIDTH)) + frame_height = int(videoCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + frames_len = videoCapture.get(cv2.CAP_PROP_FRAME_COUNT) + print("fps=", int(fps), "frames=", int(frames_len), "scale=", + f"{frame_height}x{frame_width}") + + frames_rgb_list = [] + for i in range(int(frames_len)): + if video_path.endswith('.pkl'): + frame = np.array( + Image.open(BytesIO(frames[i])).convert("RGB").resize( + (240, 135)))[:, :, ::-1].astype('uint8') + else: + _, frame = videoCapture.read() + frame = cv2.putText(frame, text, (30, 30), cv2.FONT_HERSHEY_COMPLEX, + 1.0, (0, 0, 255), 2) + frames_rgb_list.append(frame[:, :, ::-1]) # bgr to rgb + if not video_path.endswith('.pkl'): + videoCapture.release() + cv2.destroyAllWindows() + output_filename = os.path.basename(video_path) + output_filename = output_filename.split('.')[0] + '.gif' + imageio.mimsave(f'{output_dir}/{output_filename}', + frames_rgb_list, + 'GIF', + duration=0.00085) + + def postprocess(self, output, print_output=True, save_gif=True): + """ + output: list + """ + if not isinstance(self.input_file, list): + self.input_file = [ + self.input_file, + ] + output = output[0] # [B, num_cls] + N = len(self.input_file) + if output.shape[0] != N: + output = output.reshape([N] + [output.shape[0] // N] + + list(output.shape[1:])) # [N, T, C] + output = output.mean(axis=1) # [N, C] + for i in range(N): + classes = np.argpartition(output[i], -self.top_k)[-self.top_k:] + classes = classes[np.argsort(-output[i, classes])] + scores = output[i, classes] + if print_output: + print("Current video file: {0}".format(self.input_file[i])) + for j in range(self.top_k): + print("\ttop-{0} class: {1}".format(j + 1, classes[j])) + print("\ttop-{0} score: {1}".format(j + 1, scores[j])) + if save_gif: + self.add_text_to_video( + self.input_file[0], + text=f"{str(classes[0])} {float(scores[0]):.5f}") + + +@INFERENCE.register() +class SlowFast_Inference_helper(Base_Inference_helper): + def __init__(self, + num_frames=32, + sampling_rate=2, + target_size=256, + alpha=8, + top_k=1): + self.num_frames = num_frames + self.sampling_rate = sampling_rate + self.target_size = target_size + self.alpha = alpha + self.top_k = top_k + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + results = { + 'filename': input_file, + 'temporal_sample_index': 0, + 'spatial_sample_index': 0, + 'temporal_num_clips': 1, + 'spatial_num_clips': 1 + } + img_mean = [0.45, 0.45, 0.45] + img_std = [0.225, 0.225, 0.225] + ops = [ + DecodeSampler(self.num_frames, self.sampling_rate, test_mode=True), + JitterScale(self.target_size, self.target_size), + MultiCrop(self.target_size), + Image2Array(transpose=False), + Normalization(img_mean, img_std, tensor_shape=[1, 1, 1, 3]), + PackOutput(self.alpha), + ] + for op in ops: + results = op(results) + + res = [] + for item in results['imgs']: + res.append(np.expand_dims(item, axis=0).copy()) + return res + + def postprocess(self, output, print_output=True): + """ + output: list + """ + if not isinstance(self.input_file, list): + self.input_file = [ + self.input_file, + ] + output = output[0] # [B, num_cls] + + N = len(self.input_file) + if output.shape[0] != N: + output = output.reshape([N] + [output.shape[0] // N] + + list(output.shape[1:])) # [N, T, C] + output = output.mean(axis=1) # [N, C] + # output = F.softmax(paddle.to_tensor(output), axis=-1).numpy() # done in it's head + for i in range(N): + classes = np.argpartition(output[i], -self.top_k)[-self.top_k:] + classes = classes[np.argsort(-output[i, classes])] + scores = output[i, classes] + if print_output: + print("Current video file: {0}".format(self.input_file[i])) + for j in range(self.top_k): + print("\ttop-{0} class: {1}".format(j + 1, classes[j])) + print("\ttop-{0} score: {1}".format(j + 1, scores[j])) + + +@INFERENCE.register() +class STGCN_Inference_helper(Base_Inference_helper): + def __init__(self, + num_channels, + window_size, + vertex_nums, + person_nums, + top_k=1): + self.num_channels = num_channels + self.window_size = window_size + self.vertex_nums = vertex_nums + self.person_nums = person_nums + self.top_k = top_k + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + data = np.load(input_file) + results = {'data': data} + ops = [AutoPadding(window_size=self.window_size), SkeletonNorm()] + for op in ops: + results = op(results) + + res = np.expand_dims(results['data'], axis=0).copy() + return [res] + + +@INFERENCE.register() +class CTRGCN_Inference_helper(Base_Inference_helper): + def __init__(self, + num_channels=3, + vertex_nums=25, + person_nums=2, + window_size=64, + p_interval=[0.95], + top_k=1): + self.window_size = window_size + self.p_interval = p_interval + self.num_channels = num_channels + self.vertex_nums = vertex_nums + self.person_nums = person_nums + self.top_k = top_k + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + data = np.load(input_file) + results = {'data': data} + ops = [ + SketeonCropSample(window_size=self.window_size, + p_interval=self.p_interval) + ] + for op in ops: + results = op(results) + + res = np.expand_dims(results['data'], axis=0).copy() + return [res] + + +@INFERENCE.register() +class MSTCN_Inference_helper(Base_Inference_helper): + def __init__(self, num_channels, actions_map_file_path, feature_path=None): + self.num_channels = num_channels + file_ptr = open(actions_map_file_path, 'r') + actions = file_ptr.read().split('\n')[:-1] + file_ptr.close() + self.actions_dict = dict() + for a in actions: + self.actions_dict[a.split()[1]] = int(a.split()[0]) + + self.feature_path = feature_path + self.file_name_list = [] + + def get_process_file(self, input_file_txt): + with open(input_file_txt, 'r') as file_ptr: + info = file_ptr.read().split('\n')[:-1] + + files = [] + for video_name in info: + if self.feature_path is not None: + file_name = video_name.split('.')[0] + ".npy" + input_file = os.path.join(self.feature_path, file_name) + else: + input_file = video_name + + assert os.path.isfile( + input_file) is not None, "{0} not exists".format(input_file) + files.append(input_file) + + self.file_name_list.append(input_file.split('/')[-1].split('.')[0]) + return files + + def preprocess(self, input_file): + """ + input_file: str, feature file list txt path + return: list + """ + output_list = [] + + data = np.load(input_file) + results = {'video_feat': data, 'video_gt': None} + ops = [] + for op in ops: + results = op(results) + + res = np.expand_dims(results['video_feat'], axis=0).copy() + output_list.append(res) + return output_list + + def postprocess(self, output, print_output=True): + reslut_path = os.path.join("./inference/infer_results/") + if not os.path.isdir(reslut_path): + os.makedirs(reslut_path) + output = [output] + for outputs in output: + output_np = outputs[0] + recognition = [] + for i in range(output_np.shape[0]): + recognition = np.concatenate((recognition, [ + list(self.actions_dict.keys())[list( + self.actions_dict.values()).index(output_np[i])] + ])) + recog_content = list(recognition) + recog_content = [line + "\n" for line in recog_content] + + filename = self.file_name_list.pop(0) + + write_path = os.path.join(reslut_path, filename + ".txt") + f = open(write_path, "w") + f.writelines(recog_content) + f.close() + print("result write in : " + write_path) + + +@INFERENCE.register() +class ASRF_Inference_helper(Base_Inference_helper): + def __init__(self, + num_channels, + actions_map_file_path, + postprocessing_method, + boundary_threshold, + feature_path=None): + self.num_channels = num_channels + file_ptr = open(actions_map_file_path, 'r') + actions = file_ptr.read().split('\n')[:-1] + file_ptr.close() + self.actions_dict = dict() + for a in actions: + self.actions_dict[a.split()[1]] = int(a.split()[0]) + + self.postprocessing_method = postprocessing_method + self.boundary_threshold = boundary_threshold + self.feature_path = feature_path + self.file_name_list = [] + + def get_process_file(self, input_file_txt): + with open(input_file_txt, 'r') as file_ptr: + info = file_ptr.read().split('\n')[:-1] + + files = [] + for video_name in info: + if self.feature_path is not None: + file_name = video_name.split('.')[0] + ".npy" + input_file = os.path.join(self.feature_path, file_name) + else: + input_file = video_name + + assert os.path.isfile( + input_file) is not None, "{0} not exists".format(input_file) + files.append(input_file) + + self.file_name_list.append(input_file.split('/')[-1].split('.')[0]) + return files + + def preprocess(self, input_file): + """ + input_file: str, feature file list txt path + return: list + """ + + output_list = [] + + data = np.load(input_file) + results = {'video_feat': data, 'video_gt': None} + ops = [] + for op in ops: + results = op(results) + + res = np.expand_dims(results['video_feat'], axis=0).copy() + output_list.append(res) + return output_list + + def postprocess(self, output, print_output=True): + reslut_path = os.path.join("./inference/infer_results/") + if not os.path.isdir(reslut_path): + os.makedirs(reslut_path) + output = [output] + for outputs in output: + outputs_cls_np = outputs[0] + outputs_boundary_np = outputs[1] + + output_np = ASRFPostProcessing( + outputs_cls_np, + outputs_boundary_np, + self.postprocessing_method, + boundary_threshold=self.boundary_threshold).numpy()[0, :] + + recognition = [] + for i in range(output_np.shape[0]): + recognition = np.concatenate((recognition, [ + list(self.actions_dict.keys())[list( + self.actions_dict.values()).index(output_np[i])] + ])) + recog_content = list(recognition) + recog_content = [line + "\n" for line in recog_content] + + filename = self.file_name_list.pop(0) + + write_path = os.path.join(reslut_path, filename + ".txt") + f = open(write_path, "w") + f.writelines(recog_content) + f.close() + print("result write in : " + write_path) + + +@INFERENCE.register() +class AttentionLSTM_Inference_helper(Base_Inference_helper): + def __init__( + self, + num_classes, #Optional, the number of classes to be classified. + feature_num, + feature_dims, + embedding_size, + lstm_size, + top_k=1): + self.num_classes = num_classes + self.feature_num = feature_num + self.feature_dims = feature_dims + self.embedding_size = embedding_size + self.lstm_size = lstm_size + self.top_k = top_k + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + results = {'filename': input_file} + ops = [FeatureDecoder(num_classes=self.num_classes, has_label=False)] + for op in ops: + results = op(results) + + res = [] + for modality in ['rgb', 'audio']: + res.append( + np.expand_dims(results[f'{modality}_data'], axis=0).copy()) + res.append( + np.expand_dims(results[f'{modality}_len'], axis=0).copy()) + res.append( + np.expand_dims(results[f'{modality}_mask'], axis=0).copy()) + return res + + +@INFERENCE.register() +class TransNetV2_Inference_helper(): + def __init__(self, + num_frames, + height, + width, + num_channels, + threshold=0.5, + output_path=None, + visualize=True): + self._input_size = (height, width, num_channels) + self.output_path = output_path + self.len_frames = 0 + self.threshold = threshold + self.visualize = visualize + + def input_iterator(self, frames): + # return windows of size 100 where the first/last 25 frames are from the previous/next batch + # the first and last window must be padded by copies of the first and last frame of the video + no_padded_frames_start = 25 + no_padded_frames_end = 25 + 50 - ( + len(frames) % 50 if len(frames) % 50 != 0 else 50) # 25 - 74 + + start_frame = np.expand_dims(frames[0], 0) + end_frame = np.expand_dims(frames[-1], 0) + padded_inputs = np.concatenate([start_frame] * no_padded_frames_start + + [frames] + + [end_frame] * no_padded_frames_end, 0) + + ptr = 0 + while ptr + 100 <= len(padded_inputs): + out = padded_inputs[ptr:ptr + 100] + out = out.astype(np.float32) + ptr += 50 + yield out[np.newaxis] + + def preprocess(self, input_file): + """ + input_file: str, file path + return: iterator + """ + try: + import ffmpeg + except ImportError as e: + print( + f"{e}, [ffmpeg-python] package and it's dependencies is required for TransNetV2." + ) + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + self.input_file = input_file + self.filename = os.path.splitext(os.path.split(self.input_file)[1])[0] + video_stream, err = ffmpeg.input( + self.input_file).output("pipe:", + format="rawvideo", + pix_fmt="rgb24", + s="48x27").run(capture_stdout=True, + capture_stderr=True) + self.frames = np.frombuffer(video_stream, + np.uint8).reshape([-1, 27, 48, 3]) + self.len_frames = len(self.frames) + + return self.input_iterator(self.frames) + + def predictions_to_scenes(self, predictions): + predictions = (predictions > self.threshold).astype(np.uint8) + scenes = [] + t, t_prev, start = -1, 0, 0 + for i, t in enumerate(predictions): + if t_prev == 1 and t == 0: + start = i + if t_prev == 0 and t == 1 and i != 0: + scenes.append([start, i]) + t_prev = t + if t == 0: + scenes.append([start, i]) + + # just fix if all predictions are 1 + if len(scenes) == 0: + return np.array([[0, len(predictions) - 1]], dtype=np.int32) + + return np.array(scenes, dtype=np.int32) + + def visualize_predictions(self, frames, predictions): + from PIL import Image, ImageDraw + + if isinstance(predictions, np.ndarray): + predictions = [predictions] + + ih, iw, ic = frames.shape[1:] + width = 25 + + # pad frames so that length of the video is divisible by width + # pad frames also by len(predictions) pixels in width in order to show predictions + pad_with = width - len(frames) % width if len( + frames) % width != 0 else 0 + frames = np.pad(frames, [(0, pad_with), (0, 1), (0, len(predictions)), + (0, 0)]) + + predictions = [np.pad(x, (0, pad_with)) for x in predictions] + height = len(frames) // width + + img = frames.reshape([height, width, ih + 1, iw + len(predictions), ic]) + img = np.concatenate(np.split( + np.concatenate(np.split(img, height), axis=2)[0], width), + axis=2)[0, :-1] + + img = Image.fromarray(img) + draw = ImageDraw.Draw(img) + + # iterate over all frames + for i, pred in enumerate(zip(*predictions)): + x, y = i % width, i // width + x, y = x * (iw + len(predictions)) + iw, y * (ih + 1) + ih - 1 + + # we can visualize multiple predictions per single frame + for j, p in enumerate(pred): + color = [0, 0, 0] + color[(j + 1) % 3] = 255 + + value = round(p * (ih - 1)) + if value != 0: + draw.line((x + j, y, x + j, y - value), + fill=tuple(color), + width=1) + return img + + def postprocess(self, outputs, print_output=True): + """ + output: list + """ + predictions = [] + for output in outputs: + single_frame_logits, all_frames_logits = output + single_frame_pred = F.sigmoid(paddle.to_tensor(single_frame_logits)) + all_frames_pred = F.sigmoid(paddle.to_tensor(all_frames_logits)) + predictions.append((single_frame_pred.numpy()[0, 25:75, 0], + all_frames_pred.numpy()[0, 25:75, 0])) + single_frame_pred = np.concatenate( + [single_ for single_, all_ in predictions]) + all_frames_pred = np.concatenate( + [all_ for single_, all_ in predictions]) + single_frame_predictions, all_frame_predictions = single_frame_pred[: + self + . + len_frames], all_frames_pred[: + self + . + len_frames] + + scenes = self.predictions_to_scenes(single_frame_predictions) + + if print_output: + print("Current video file: {0}".format(self.input_file)) + print("\tShot Boundarys: {0}".format(scenes)) + + if self.output_path: + if not os.path.exists(self.output_path): + os.makedirs(self.output_path) + predictions = np.stack( + [single_frame_predictions, all_frame_predictions], 1) + predictions_file = os.path.join(self.output_path, + self.filename + "_predictions.txt") + np.savetxt(predictions_file, predictions, fmt="%.6f") + scenes_file = os.path.join(self.output_path, + self.filename + "_scenes.txt") + np.savetxt(scenes_file, scenes, fmt="%d") + + if self.visualize: + pil_image = self.visualize_predictions( + self.frames, + predictions=(single_frame_predictions, + all_frame_predictions)) + image_file = os.path.join(self.output_path, + self.filename + "_vis.png") + pil_image.save(image_file) + + +@INFERENCE.register() +class ADDS_Inference_helper(Base_Inference_helper): + def __init__(self, + frame_idxs=[0], + num_scales=4, + side_map={ + "2": 2, + "3": 3, + "l": 2, + "r": 3 + }, + height=256, + width=512, + full_res_shape=None, + num_channels=None, + img_ext=".png", + K=None): + + self.frame_idxs = frame_idxs + self.num_scales = num_scales + self.side_map = side_map + self.full_res_shape = full_res_shape + self.img_ext = img_ext + self.height = height + self.width = width + self.K = K + + def preprocess(self, input_file): + """ + input_file: str, file path + return: list + """ + assert os.path.isfile(input_file) is not None, "{0} not exists".format( + input_file) + results = { + 'filename': input_file, + 'mode': 'infer', + 'day_or_night': 'day', + } + ops = [ + ImageDecoder( + backend='pil', + dataset='kitti', + frame_idxs=self.frame_idxs, + num_scales=self.num_scales, + side_map=self.side_map, + full_res_shape=self.full_res_shape, + img_ext=self.img_ext, + ), + GroupResize( + height=self.height, + width=self.width, + K=self.K, + scale=1, + mode='infer', + ), + ToArray(), + ] + for op in ops: + results = op(results) + res = results['imgs'][('color', 0, 0)] + res = np.expand_dims(res, axis=0).copy() + return [res] + + def postprocess(self, output, print_output, save_dir='data/'): + """ + output: list + """ + if not isinstance(self.input_file, list): + self.input_file = [ + self.input_file, + ] + print(len(output)) + N = len(self.input_file) + for i in range(N): + pred_depth = output[i] # [H, W] + if print_output: + print("Current input image: {0}".format(self.input_file[i])) + file_name = os.path.basename(self.input_file[i]).split('.')[0] + save_path = os.path.join(save_dir, + file_name + "_depth" + ".png") + pred_depth_color = self._convertPNG(pred_depth) + pred_depth_color.save(save_path) + print(f"pred depth image saved to: {save_path}") + + def _convertPNG(self, image_numpy): + disp_resized = cv2.resize(image_numpy, (1280, 640)) + disp_resized_np = disp_resized + vmax = np.percentile(disp_resized_np, 95) + normalizer = mpl.colors.Normalize(vmin=disp_resized_np.min(), vmax=vmax) + mapper = cm.ScalarMappable(norm=normalizer, cmap='magma') + colormapped_im = (mapper.to_rgba(disp_resized_np)[:, :, :3] * + 255).astype(np.uint8) + im = Image.fromarray(colormapped_im) + return im + + +@INFERENCE.register() +class AVA_SlowFast_FastRCNN_Inference_helper(Base_Inference_helper): + def __init__(self, + detection_model_name, + detection_model_weights, + config_file_path, + predict_stepsize=8, + output_stepsize=4, + output_fps=6, + out_filename='ava_det_demo.mp4', + num_frames=32, + alpha=4, + target_size=256): + self.detection_model_name = detection_model_name + self.detection_model_weights = detection_model_weights + + self.config = get_config(config_file_path, + show=False) #parse config file + self.predict_stepsize = predict_stepsize + self.output_stepsize = output_stepsize + self.output_fps = output_fps + self.out_filename = out_filename + self.num_frames = num_frames + self.alpha = alpha + self.target_size = target_size + + def preprocess(self, input_file): + """ + input_file: str, file path + """ + + frame_dir = 'tmp_frames' + self.frame_paths, frames, FPS = frame_extraction(input_file, frame_dir) + num_frame = len(self.frame_paths) #视频秒数*FPS + assert num_frame != 0 + + # 帧图像高度和宽度 + h, w, _ = frames[0].shape + + # Get clip_len, frame_interval and calculate center index of each clip + data_process_pipeline = build_pipeline( + self.config.PIPELINE.test) #测试时输出处理流水配置 + + clip_len = self.config.PIPELINE.test.sample['clip_len'] + assert clip_len % 2 == 0, 'We would like to have an even clip_len' + frame_interval = self.config.PIPELINE.test.sample['frame_interval'] + + # 此处关键帧每秒取一个 + clip_len = self.config.PIPELINE.test.sample['clip_len'] + assert clip_len % 2 == 0, 'We would like to have an even clip_len' + frame_interval = self.config.PIPELINE.test.sample['frame_interval'] + window_size = clip_len * frame_interval + timestamps = np.arange(window_size // 2, + (num_frame + 1 - window_size // 2), + self.predict_stepsize) + + selected_frame_list = [] + for timestamp in timestamps: + selected_frame_list.append(self.frame_paths[timestamp - 1]) + + # Load label_map + label_map_path = self.config.DATASET.test['label_file'] + self.categories, self.class_whitelist = read_labelmap( + open(label_map_path)) + label_map = {} + for item in self.categories: + id = item['id'] + name = item['name'] + label_map[id] = name + + self.label_map = label_map + + detection_result_dir = 'tmp_detection' + detection_model_name = self.detection_model_name + detection_model_weights = self.detection_model_weights + detection_txt_list = detection_inference(selected_frame_list, + detection_result_dir, + detection_model_name, + detection_model_weights) + assert len(detection_txt_list) == len(timestamps) + + human_detections = [] + data_list = [] + person_num_list = [] + + for timestamp, detection_txt_path in zip(timestamps, + detection_txt_list): + proposals, scores = get_detection_result( + detection_txt_path, h, w, + (float)(self.config.DATASET.test['person_det_score_thr'])) + + if proposals.shape[0] == 0: + #person_num_list.append(0) + human_detections.append(None) + continue + + human_detections.append(proposals) + + result = get_timestep_result(frame_dir, + timestamp, + clip_len, + frame_interval, + FPS=FPS) + result["proposals"] = proposals + result["scores"] = scores + + new_result = data_process_pipeline(result) + proposals = new_result['proposals'] + + img_slow = new_result['imgs'][0] + img_slow = img_slow[np.newaxis, :] + img_fast = new_result['imgs'][1] + img_fast = img_fast[np.newaxis, :] + + proposals = proposals[np.newaxis, :] + + scores = scores[np.newaxis, :] + + img_shape = np.asarray(new_result['img_shape']) + img_shape = img_shape[np.newaxis, :] + + data = [ + paddle.to_tensor(img_slow, dtype='float32'), + paddle.to_tensor(img_fast, dtype='float32'), + paddle.to_tensor(proposals, dtype='float32'), + paddle.to_tensor(img_shape, dtype='int32') + ] + + person_num = proposals.shape[1] + person_num_list.append(person_num) + + data_list.append(data) + + self.human_detections = human_detections + self.person_num_list = person_num_list + self.timestamps = timestamps + self.frame_dir = frame_dir + self.detection_result_dir = detection_result_dir + + return data_list + + def postprocess(self, outputs, print_output=True): + """ + output: list + """ + predictions = [] + + assert len(self.person_num_list) == len(outputs) + + #print("*** self.human_detections",len( self.human_detections)) + #print("*** outputs",len( outputs)) + + index = 0 + for t_index in range(len(self.timestamps)): + if self.human_detections[t_index] is None: + predictions.append(None) + continue + + human_detection = self.human_detections[t_index] + + output = outputs[index] + result = output #长度为类别个数,不包含背景 + + person_num = self.person_num_list[index] + + index = index + 1 + + prediction = [] + + if human_detection is None: + predictions.append(None) + continue + + # N proposals + for i in range(person_num): + prediction.append([]) + + # Perform action score thr + for i in range(len(result)): # for class + if i + 1 not in self.class_whitelist: + continue + for j in range(person_num): + if result[i][j, 4] > self.config.MODEL.head['action_thr']: + prediction[j].append( + (self.label_map[i + 1], result[i][j, 4] + )) # label_map is a dict, label index start from 1 + predictions.append(prediction) + + results = [] + for human_detection, prediction in zip(self.human_detections, + predictions): + results.append(pack_result(human_detection, prediction)) + + def dense_timestamps(timestamps, n): + """Make it nx frames.""" + old_frame_interval = (timestamps[1] - timestamps[0]) + start = timestamps[0] - old_frame_interval / n * (n - 1) / 2 + new_frame_inds = np.arange( + len(timestamps) * n) * old_frame_interval / n + start + return new_frame_inds.astype(np.int) + + dense_n = int(self.predict_stepsize / self.output_stepsize) #30 + frames = [ + cv2.imread(self.frame_paths[i - 1]) + for i in dense_timestamps(self.timestamps, dense_n) + ] + + vis_frames = visualize(frames, results) + + try: + import moviepy.editor as mpy + except ImportError: + raise ImportError('Please install moviepy to enable output file') + + vid = mpy.ImageSequenceClip([x[:, :, ::-1] for x in vis_frames], + fps=self.output_fps) + vid.write_videofile(self.out_filename) + print("finish write !") + + # delete tmp files and dirs + shutil.rmtree(self.frame_dir) + shutil.rmtree(self.detection_result_dir)