深度学习领域如何正确地读取视频
参考资料:
https://github.com/dmlc/decord
由于最近部门业务调整,让我过去做视频相关的研究了。这篇随笔是为了辨析一下在深度学习的语境下如何规范地去读取视频,如果不注意的话还是容易踩坑的。
首先,我们需要用到decord这个软件,使用pip就可以直接安装,链接在参考资料中放出来了。我们后面的讲解也都围绕着它来展开:
问题:深度学习模型眼中的视频是什么样的?和图片有什么差异?
图片没什么好说的,深度学习模型看到的一般就是三个通道(RGB)的二维栅格矩阵。其值域一般通过将像素值除以255归一化到[0, 1]区间。而视频呢?做一下推广,不就是多了一个帧数的维度吗?比如图片的形状是[b, c, h, w],那么视频的维度显然就是[b, c, t, h, w],其中t是帧的个数。这么考虑大致是对的,但是千万不要遗忘了一点,那就是视频何以成为视频而不是PPT,这就牵扯到视频的另一个属性了,即帧率fps(Frame Per Second)。只有幻灯片以一定的速度(对于人眼可以说是超高速了)放映出来,视频才真正成为视频。但是显然现在的视频生成模型都没有也没有能力对fps进行区分,那么我们其实默认都做了一个假定,即我们的视频生成模型只见过,或者说只能生成固定帧率的视频。
想象一下,如果你忽略了这一点,把视频仅仅等价为多了一个通道的图片序列会发生什么情况?
1. 视频的fps很高,比最终产出的视频结果高很多
由于视频的fps很高,一秒钟之内可能“嗖”的一下过去了很多帧,在此时,如果视频的画面静止不动,那么很有可能我们采样得到的视频的变化就非常之小... 此时我们需要参考视频实际fps和我们期望应用的fps之间的比值差异,在原始视频中跳跃着采样。
2. 视频的fps很低,比最终产出的结果视频低很多
由于视频的fps太低了,作为1)中的对称情况,我们应该进行超采样,即此时我们有可能连续采样到原始视频的相同帧。
这里又引出了另一个点,即我们上述做法的逻辑是什么?在我看来,这牵涉到一个假设:
假设:**对于世界上的所有视频**,其原始fps都是最好的呈现状态,即一定时长内想给阅览者展示的东西是最优的状态。
假设的结果:**对于世界上的所有视频** 在读取数据,调整fps的时候,我们都给模型看到一个固定时长时间段内的内容。
举一个例子,我们期望的结果视频的fps是15,我们想播放5秒钟,最终就是75帧。此时来了另一个视频,帧率是60,在5s钟之内播放了300帧。此时如果我们每4帧每4帧跳着采样,就能同样获得一个15帧的视频,并且最后的帧数也是75帧。这其实就是我们的比较规范的做法。下面直接用decord包给一个使用范例吧,看完就懂了。
import decord decord.bridge.set_bridge('torch') container = decord.VideoReader(video_path) num_total_frame = len(container) num_selected_frames = 75 original_fps = container.get_avg_fps() selected_fps = 15 sample_interval = original_fps / selected_fps frame_cover_range = int(1 + (num_selected_frames - 1) * sample_interval) if (num_total_frame * (1.0 - end_ratio)) >= frame_cover_range: start_frame_index = random.randint(int(num_total_frame * start_ratio), int(num_total_frame)-frame_cover_range) selcted_frame_indexes = np.linspace(start_frame_index, start_frame_index+frame_cover_range-1, num_selected_frames, dtype=int) print(f'----Read_video: video_path: {video_path}, sample_fps:{selected_fps}, select_frames num: {num_selected_frames}, start_frame_index: {start_frame_index}, frame_cover_range: {frame_cover_range}') elif frame_cover_range<=num_total_frame: start_frame_index = random.randint(0, num_total_frame-frame_cover_range) selcted_frame_indexes = np.linspace(start_frame_index, start_frame_index+frame_cover_range-1, num_selected_frames, dtype=int) print(f'----Read_video: frame_cover_range<=num_total_frame! video_path: {video_path}, sample_rate:{selected_fps}, select_frames num: {num_selected_frames}, start_frame_index: {start_frame_index}, frame_cover_range: {frame_cover_range}') else: # selected_fps is not supported in current video, use the smallest fps the video supports raise Exception("selected_fps is not supported in current video, use the smallest fps the video supports") selcted_frame_indexes = selcted_frame_indexes.astype(int).tolist() # frames = torch.from_numpy(container.get_batch(selcted_frame_indexes).asnumpy()).permute(0, 3, 1, 2).float() # [f, h, w, c] => [f, c, h, w] frames = container.get_batch(selcted_frame_indexes).permute(0, 3, 1, 2).float() # [f, h, w, c] => [f, c, h, w] frames = frames / 255.
这里start_ratio和end_ratio是为了跳过视频的开头(可能有黑幕转场之类的东西)。具体思路如下:
1. 导入decord并set_bridge,使得VideoReader的返回值是tensor
2. 使用get_avg_fps获取原视频的fps,并跟指定的fps做比值
3. 如果能够实现掐头就掐头,如果不能实现掐头就将就一下,如果都不行,说明这个视频实在太短了,不满足我们对于帧数的要求,直接报错。
4. 使用get_batch,获取实际的视频帧。