前面说过如何用自己的UCF101数据集训练3D识别模型video-caffe,那么怎么制作自己的UCF101数据集呢?这个稍微有点复杂。
UCF101数据集其实是按101个动作类别分类了的短视频的集合,每类动作对应一个目录,每个目录下有很多avi格式的视频文件,我们需要制作自己的项目应用领域的UCF101格式的数据集时,可以像这样把项目应用的视频数据按动作分类这样分目录存放,一个目录对应一个动作,实际项目中一般不会有101种动作,比如有三种或者四种动作,那么对应就按三个或四个目录存放视频文件即可,然后写个shell脚本或者python脚本来生成模型所需的train和test视频数据文件的list文件,train和test的list文件的内容格式根据你所用的3D模型所需要的格式来写,如果模型直接支持UCF101的train/test list格式(从UCF101的trainlist.txt文件中摘取示例如下):
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c01.avi
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c02.avi
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c03.avi
...
ApplyLipstick/v_ApplyLipstick_g01_c01.avi
ApplyLipstick/v_ApplyLipstick_g01_c02.avi
ApplyLipstick/v_ApplyLipstick_g01_c03.avi
ApplyLipstick/v_ApplyLipstick_g01_c04.avi
...
除了上面的数据文件(路径)的列表,还需要有个对应的定义class值的class_index.txt的文件:
1 ApplyEyeMakeup
2 ApplyLipstick
3 Archery
4 BabyCrawling
5 BalanceBeam
6 BandMarching
7 BaseballPitch
8 Basketball
9 BasketballDunk
10 BenchPress
...
很显然,解析这两个文件里的列表,就可以把avi数据文件和class值对应起来。
但是这样做稍显得有点麻烦,所以一般C3D模型在定义自己的train_list.txt和test_list.txt文件格式时进行了简化,直接把数据文件和class值的对应关系定义在一个文件里,也就是类似这样:
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c01.avi 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c02.avi 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c03.avi 1
...
ApplyLipstick/v_ApplyLipstick_g01_c01.avi 2
ApplyLipstick/v_ApplyLipstick_g01_c02.avi 2
ApplyLipstick/v_ApplyLipstick_g01_c03.avi 2
ApplyLipstick/v_ApplyLipstick_g01_c04.avi 2
...
至于每个class值对应的具体的class类别名对模型来说根本不需要,这是调用模型进行识别的应用程序关心的内容,所以调用模型进行识别的应用程序才需要class_index.txt这样的文件。
如果是你所使用模型只支持读取序列图片(例如C3D TensorFlow版),并且每个动作的序列图片保存在一个独立的子目录下,那么train/test列表文件可以是类似这样:
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c01/act1 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c01/act2 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c02/act1 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c03/act1 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c03/act2 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c03/act3 1
...
ApplyLipstick/v_ApplyLipstick_g01_c01/act1 2
ApplyLipstick/v_ApplyLipstick_g01_c02/act1 2
ApplyLipstick/v_ApplyLipstick_g01_c02/act2 2
ApplyLipstick/v_ApplyLipstick_g01_c03/act1 2
ApplyLipstick/v_ApplyLipstick_g01_c04/act1 2
ApplyLipstick/v_ApplyLipstick_g01_c04/act2 2
ApplyLipstick/v_ApplyLipstick_g01_c04/act3 2
ApplyLipstick/v_ApplyLipstick_g01_c04/act4 2
...
当每个视频文件都并不是单纯包含一个动作的小视频,而是几个动作的连接的视频,或者有好几个同类别动作连续的视频,那么上面的train_list.txt/test_list.txt的内容还得进一步改造一下,中间还需要增加一列数据指定动作的起始帧(start_frame),结束帧(end_frame)则一般是你使用的3D模型的配置文件里设置的,比如3D模型的配置文件里设置的length为16,则表示为一次读取16帧来训练或推理识别一个动作,那么从train_list.txt/test_list.txt里读取start_frame的值作为开始帧号,连续读取16帧,结束帧号自然是start_frame+16。此时train_list.txt和test_list.txt文件的内容则类似如下所示:
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c01.avi 1 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c02.avi 1 1
ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c03.avi 1 1
...
ApplyLipstick/v_ApplyLipstick_g01_c01.avi 1 2
ApplyLipstick/v_ApplyLipstick_g01_c02.avi 1 2
ApplyLipstick/v_ApplyLipstick_g01_c03.avi 1 2
ApplyLipstick/v_ApplyLipstick_g01_c04.avi 1 2
...
假若你使用的3D模型只支持一序列图片来识别一个动作,或者是你的项目的视频数据都很大,按动作切分视频或者标注视频不方便,或者觉得相对使用目前某些视频标注工具来标注视频说,把视频抽取成图片后再来分类比较可靠不容易出错 (反正我实验用过MS的VoTT和国内某视频数据标注工具,可靠性离实用还差很远,经常一个地方出错就把全部帧位标注都搞乱了,有时可能标注了大半天就一个错误就全废了,真有气得要吐血的感觉。另外,工具支持的保存格式有限,得自己写脚本转换成类似UCF101这样风格的标注方式),那么写脚本调用ffmpeg之类的工具把对应目录下的每个视频按一定的FPS抽取成jpg图片,如果你使用的模型不支持start_frame标注,那么每个目录下只能存放模型的length参数指定帧数的图片,每一个动作序列的图片(比如16帧图片)对应存放到相应动作类别对应的目录下的对应视频文件对应的目录下的子目录中,全路径可以表示为.../
至于抽取图片的脚本以及生成train_list.txt/test_list.txt的脚本可以使用shell脚本写也可以使用python写,我沿用的C3D Tensorflow版的写法使用的shell写的,其实使用python可能更容易实现也更灵活,因为python功能很强大。
贴出相关命令和示例代码:
从http://ffmpeg.org/download.html 下载linux版本的ffmpeg可执行程序,然后设置环境:
写个shell脚本convert_video2jpg.sh从各个视频中抽取图片并分目录对应存放:
for folder in $1/*
do
for file in "$folder"/*.mp4
do
if [[ ! -d "$2/${file[@]%.mp4}" ]]; then
mkdir -p "$2/${file[@]%.mp4}"
fi
ffmpeg -i "$file" -vf fps=$3 "$2/${file[@]%.mp4}"/%05d.jpg
done
done
假设视频文件都在video目录里,抽取的图片以为文件名为目录存放到父目录video_images下去, 抽取帧率 FPS = 10,那么执行命令:
./convert_video2jpg.sh video video_images 10
对抽取出来的图片进行人工挑选,挑选出属于同一个动作的序列图片到同一个子目录,然后按照
> c3d_ucf101_train_split1.txt
> c3d_ucf101_test_split1.txt
COUNT=-1
for folder in $1/*
do
printf "${folder}\n"
COUNT=$[$COUNT + 1]
for imagesFolder in "$folder"/*/*
do
printf "${imagesFolder}\n"
if (( $(jot -r 1 1 $2) > 1 )); then
echo "${imagesFolder}" 1 $COUNT >> c3d_ucf101_train_split1.txt
else
echo "${imagesFolder}" 1 $COUNT >> c3d_ucf101_test_split1.txt
fi
done
done
里面用到个产生随机数的工具athena-jot,首先安装它:
apt-get install athena-jot
然后执行下面的命令产生train/test列表文件:
./convert_images_to_C3D_list.sh data/video-labeled-3dds 4
生成的train/test列表文件内容示例如下:
另外呢,统计计算图片数据的均值时也需要这样的列表文件,例如我写了个生成这样的列表文件的脚本convert_images_to_caffe_lmdb_list.sh如下:
> caffe_lmdb.list
COUNT=-1
for folder in $1/*
do
printf "${folder}\n"
COUNT=$[$COUNT + 1]
for imagesFolder in "$folder"/*/*
do
printf "${imagesFolder}\n"
for f in "${imagesFolder}"/*.jpg
do
echo "$f" $COUNT >> caffe_lmdb.list
done
done
done
然后执行脚本生成列表文件
./convert_images_to_caffe_lmdb_list.sh data/video-labeled-3dds 4
然后使用这个列表文件去统计生成lmdb database文件
cd /workspace/video-caffe/build/tools
./convert_imageset /workspace/video-caffe/ /workspace/video-caffe/caffe_lmdb.list laundry.lmdb --resize_width=171 --resize_height=128
然后使用lmdb文件算出均值:
./compute_image_mean laundry.lmdb
I0610 21:26:28.706018 2337 db_lmdb.cpp:35] Opened lmdb laundry.lmdb
I0610 21:26:28.707100 2337 compute_image_mean.cpp:70] Starting iteration
I0610 21:26:29.398113 2337 compute_image_mean.cpp:95] Processed 10000 files.
I0610 21:26:29.505965 2337 compute_image_mean.cpp:101] Processed 11584 files.
I0610 21:26:29.506062 2337 compute_image_mean.cpp:108] Write to laundry_mean.binaryproto
I0610 21:26:29.506680 2337 compute_image_mean.cpp:114] Number of channels: 3
I0610 21:26:29.506724 2337 compute_image_mean.cpp:119] mean_value channel [0]: 121.643
I0610 21:26:29.506786 2337 compute_image_mean.cpp:119] mean_value channel [1]: 124.514
I0610 21:26:29.506829 2337 compute_image_mean.cpp:119] mean_value channel [2]: 126.064
然后使用这些均值去设置ucf101训练video-caffe的配置文件c3d_ucf101_ train_test.prototxt,一并设置好train/test列表文件c3d_ucf101_train_split1.txt和c3d_ucf101_test_split1.txt的路径作为训练和测试的数据source:
然后根据需要修改c3d_ucf101_solver.prototxt里的超参数后就可以执行下面的命令开始训练了:
cd /workspace/video-caffe
./examples/c3d_ucf101/train_ucf101.sh