集智专栏
资源加载中,请稍后...
集智专栏

深度学习教程:计算你的爱豆在荧屏上的出镜时间

船长

平时看剧看电影的时候,不知大家有没有想过一个很有趣的问题:各个演员在里面的出镜时间分别是多少?差距大吗?而通过计算出一部剧或一部电影中演员的镜头多少,也能得知他们在里面的重要程度以及在娱乐圈的影响力。想知道你的爱豆在剧里露脸多久吗?

机器学习爱好者 Pulkit Sharma 最近就利用深度学习知识解决了这个问题,一起看看是怎么实现的。

在我(作者 Pulkit Sharma)刚开始研究深度学习的时候,我最先接触的就是图像分类。这简直是计算机视觉领域里最迷人的部分之一有木有?我当时完全沉迷其中无法自拔。但是后来等我熟练掌握图像分类之后,慢慢有了一个好奇的想法:能不能把学到的知识用在视频上。

有没有这么一种方法,可以创建一个模型自动识别特定人物在一段视频中的出现时间?当然是有的,在本文我就把这个方法分享给你。

这里多扯几句,让你更好地理解我们要解决的问题。记住,出镜时间对一个演员来说是非常重要的,这直接关系到他/她的收入。所以,如果我们能计算出任一视频中任何演员的出镜时间,还是很好酷的,对吧?

在本文我会帮你理解如何使用深度学习处理视频数据,教程会使用动画片《猫和老鼠》的视频片段作为例子。我们的目标就是计算视频中汤姆和杰瑞的出镜时间。

注:本文的前提是你对使用深度学习执行图片分类已经有一定了解。如果不熟悉,可以看看这篇文章,帮你快速了解图像识别和图像分类方面的知识:

《机器学习有意思! 03》- 深度学习与卷积神经网络 景略集智:8分钟Keras入门使用指南(中文字幕)

内容目录

  • 读取视频,提取视频帧
  • 如何用 Python 处理视频文件
  • 计算出镜时间—— 一种简单方法
  • 我的经验体会—— 哪些有效,哪些无效

读取视频,提取视频帧

听说过手翻书吗?如果没听说过,那你就落伍啦!就是下面这个:

在手翻书的每一页,我们都有不同的图像,随着我们一页页翻过去,会看到一个鲨鱼在跳舞的动态画面。我们甚至可以把它称为一种视频。翻书的速度越快,效果就越好。简而言之,手翻书所展现的视觉画面就是一系列以特定顺序排列的各不相同的图像。

同样,视频其实也是由一系列的图像组成。这些图像被称为“帧”,把各个帧合在一起就形成了视频。因此,与视频数据有关的问题和图像分类或对象检测问题并没有什么不同。只是多了从视频中提取视频帧这一步。

记住,我们这里的挑战是计算给定视频中汤姆和杰瑞的出镜时间。我们首先来总结一下本文解决这个问题的步骤:

  • 导入和读取视频,提取视频帧,将帧保存为图像
  • 将部分图像进行标记用于训练模型(别担心,我都做好了)
  • 用训练数据创建我们的模型
  • 对剩余图像进行预测

相信我,按照以上步骤,能帮你解决深度学习中大部分和视频有关的问题。

我们接着往下走,用 Python 处理视频。

如何用 Python 处理视频文件

我们首先导入所需的各种库,如果你还没装它们,就先安装好:

  • Numpy
  • Pandas
  • Matplotlib
  • Keras
  • Skimage
  • OpenCV
import cv2     # 用于获取视频
import math   # 用于数学计算
import matplotlib.pyplot as plt    # 用于绘制图像
%matplotlib inline
import pandas as pd
from keras.preprocessing import image   # 用于预处理图像
import numpy as np    # 用于数学计算
from keras.utils import np_utils
from skimage.transform import resize   # 用于调整图像大小

步骤1 :读取视频,提取视频帧,将帧保存为图像

现在我们加载视频,将其转换为帧。可以点击这个链接,下载本教程所用的《猫和老鼠》视频。我们首先使用 VideoCapture() 函数从给定目录下获取视频,然后从视频中提取出帧,再用 imwrite() 函数把它们保存成图像。开撸代码:

count = 0
videoFile = "Tom and jerry.mp4"
cap = cv2.VideoCapture(videoFile)   # 从给定路径中获取视频
frameRate = cap.get(5) #frame rate
x=1
while(cap.isOpened()):
    frameId = cap.get(1) #current frame number
    ret, frame = cap.read()
    if (ret != True):
        break
    if (frameId % math.floor(frameRate) == 0):
        filename ="frame%d.jpg" % count;count+=1
        cv2.imwrite(filename, frame)
cap.release()
print ("Done!")

完成!

等这一步完成后,会在屏幕上输出一个‘Done!’来确认已经创建完所有的帧。

我们试着可视化一张图像(帧)。首先用 matplotlib 的 imread() 函数读取图像,然后用 imshow() 函数绘制出来。

img = plt.imread('frame0.jpg')   # reading image using its name
plt.imshow(img)

亦可赛艇!

这是视频的第一帧。我们从视频中每一秒提取一帧,视频一共长 4 分 58 秒(298 秒),所以最后我们总共有 298 张图像。

我们下面的任务就是识别哪些图像里有汤姆,哪些图像里有杰瑞。如果我们提取出的图像在目前最大的图像数据集 ImageNet 中有相似的图像,那问题就简单多了。为啥?因为这样我们就可以方便地使用基于 ImageNet 的预训练模型,还能获得很高的准确度!但是这就没意思了,对吧?

由于我们用的是卡通图像,所以任何预训练模型识别一段视频中的汤姆和杰瑞会非常困难(如果没有对应的预训练模型)。

步骤2:对部分图像进行标记,用于训练模型

那么我们怎么解决呢?一种可行的方法就是手动标记部分图像,用它们训练一个模型。等模型学习了其中的模式后,我们就可以用它来预测之前不可见的图像。

记住,有些帧上可能既没汤姆也没杰瑞。所以,我们要把这个问题看作一个多类分类问题。我将其中的类定义如下:

0 - 既无汤姆也无杰瑞 1 - 杰瑞 2 - 汤姆

别担心,我已经将所有图像打好了标签,所以这个活儿你不用干啦!点击链接,即可下载 CSV 文件,包含了每张图像的名称及其对应的类(0 或 1 或 2):

data = pd.read_csv('mapping.csv')     # 读取CSV文件
data.head()      # 打印文件的前5行

文件包含了 2 列:

  • Image_ID:含有每张图像的名称
  • Class.Image_ID:含有每张图像对应的类

我们下一步是读取图像,根据它们的名称(即 Image_ID 列)来读取。

X = [ ]     # 创建一个空数组
for img_name in data.Image_ID:
    img = plt.imread('' + img_name)
    X.append(img)  # 将每张图像保存为数组X
X = np.array(X)    # 将列表转换为数组

完成!现在我们已经获取了图像,记住训练模型需要两样东西:

  • 训练图像
  • 它们对应的类

由于有 3 个类,我们会用 keras.utils 的 to_categorical() 函数将它们进行独热编码。

y = data.Class
dummy_y = np_utils.to_categorical(y)    # 独热编码Classes

我们会使用一个 VGG16 预训练模型,它接受大小为 2242243 的图像为输入。由于我们的图像大小不同,所以需要重新调整大小。我们使用 skimage.transform 的 resize() 完成这一步。

image = []
for i in range(0,X.shape[0]):
    a = resize(X[i], preserve_range=True, output_shape=(224,224)).astype(int)      # reshaping to 224*224*3
    image.append(a)
X = np.array(image)

最终,所有的图像都被重新调整为 2242243 大小。但是在将输入传入模型之前,我们必须按照模型的要求将输入预处理。否则,模型的表现会大打折扣。使用 keras.application.vgg16 的 preprocess_input() 函数来完成这一步。

from keras.applications.vgg16 import preprocess_input
X = preprocess_input(X, mode='tf')      # 预处理输入数据

我们也需要一个验证集来检查模型在不可见图像上的表现。我们使用 sklearn.model_selection 模块的 train_test_split() 函数,将图像随机划分为训练集和验证集。

from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid = train_test_split(X, dummy_y, test_size=0.3, random_state=42)    # preparing the validation set

步骤3:创建模型

下一步是创建我们的模型。前面说过,我们在这个任务中会使用 VGG16 预训练模型。首先导入所需的库来创建模型:

from keras.models import Sequential
from keras.applications.vgg16 import VGG16
from keras.layers import Dense, InputLayer, Dropout

我们现在加载 VGG16 预训练模型,将其保存为 base_model:

base_model = VGG16(weights='imagenet', include_top=False, input_shape=(224, 224, 3))    # include_top=False to remove the top layer

我们使用该模型预测 X_train 和 X_valid,获取特征,然后用这些特征重新训练模型。

X_train = base_model.predict(X_train)
X_valid = base_model.predict(X_valid)
X_train.shape, X_valid.shape

X_train 和 X_valid 的大小(shape)分别为 (208, 7, 7, 512)和(90, 7, 7, 512),为了能将其输入到我们的神经网络中,我们必须将其重新调整为 1-D。

X_train = X_train.reshape(208, 7*7*512)      # 转换为1-D
X_valid = X_valid.reshape(90, 7*7*512)

现在我们预处理图像,将它们零均值化,这样可以帮助模型更快地转换。

train = X_train/X_train.max()      # 将数据零均值化
X_valid = X_valid/X_train.max()

最后,我们搭建模型。这一步又可以划分为 3 小步:

  1. 创建模型
  2. 编译模型
  3. 训练模型
# i. Building the model
model = Sequential()
model.add(InputLayer((7*7*512,)))    # 输入层
model.add(Dense(units=1024, activation='sigmoid')) # 隐藏层
model.add(Dense(3, activation='sigmoid'))    # 输出层

我们用summary()函数检查模型的总结:

model.summary()

模型有一个含有 1024 个神经元的隐藏层和一个有 3 个神经元的输出层(因为我们需要预测 3 个类)。现在我们编译我们的模型:

# ii. Compiling the model
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

在最后一步中,我们会拟合模型,同时也会检查模型在不可见图像(即验证集中的图像)上的性能:

# iii. 训练模型
model.fit(train, y_train, epochs=100, validation_data=(X_valid, y_valid))

我们可以看到,模型在训练集和验证集上均取得了不错的表现,在不可见图像上的准确率为 85%。以上就是我们用视频数据训练模型并对每个帧进行预测的过程。

在下一部分,我们就来到了本文最有趣也是最重要的部分:计算一段新视频中汤姆和杰瑞的出镜时间。

计算出镜时间——一种简单方法

首先,我们下载这部分所用的视频。

下载后,从视频中提取出帧。按照前文所展示的相同步骤操作:

count = 0
videoFile = "Tom and Jerry 3.mp4"
cap = cv2.VideoCapture(videoFile)
frameRate = cap.get(5) #frame rate
x=1
while(cap.isOpened()):
    frameId = cap.get(1) #current frame number
    ret, frame = cap.read()
    if (ret != True):
        break
    if (frameId % math.floor(frameRate) == 0):
        filename ="test%d.jpg" % count;count+=1
        cv2.imwrite(filename, frame)
cap.release()
print ("Done!")

完成!

在从新视频中提取出帧后,我们现在加载包含了每个提取后的帧的名称的 test.csv 文件。下载 test.csv 文件并加载它:

test = pd.read_csv('test.csv')

接下来,我们会导入用于训练的图像,然后按照之前提到的预训练模型的要求,重新调整图像大小:

test_image = []
for img_name in test.Image_ID:
    img = plt.imread('' + img_name)
    test_image.append(img)
test_img = np.array(test_image)

test_image = []
for i in range(0,test_img.shape[0]):
    a = resize(test_img[i], preserve_range=True, output_shape=(224,224)).astype(int)
    test_image.append(a)
test_image = np.array(test_image)

我们需要对这些图像做些变动,和我们对训练图像所做的处理相同。使用 base_model.predict() 函数对图像进行预处理,用 VGG16 预训练模型从图像中提取出帧,将图像重塑为 1-D 形式,并使其零均值化:

# 预处理图像
test_image = preprocess_input(test_image, mode='tf')

# 用预训练模型从图像中提取特征
test_image = base_model.predict(test_image)

# 将图像转换为 1-D 形式
test_image = test_image.reshape(186, 7*7*512)

# 将图像零均值化
test_image = test_image/test_image.max()

由于我们前面训练了模型,所以用这个模型对这些图像进行预测。

步骤4:对剩余图像进行预测

predictions = model.predict_classes(test_image)

步骤5:计算汤姆和杰瑞的出镜时间

回想一下,类别‘1’表示杰瑞的出镜时间,类别‘2’表示汤姆的出镜时间。我们利用上面的预测结果来计算这两位传奇“演员”的出镜时间:

print("The screen time of JERRY is", predictions[predictions==1].shape[0], "seconds")
print("The screen time of TOM is", predictions[predictions==2].shape[0], "seconds")

大功告成!这样我们就得出了这一段动画片中汤姆和杰瑞的出镜时间:

汤姆139秒,杰瑞6秒。

杰瑞不哭。

我的经验体会——哪些有效,哪些无效

对于这个任务,我不断尝试和测试了很多次,有些方法效果很好,有些就不行。下面我就谈谈我在操过过程中遇到了困难以及解决方法。之后,我会提供最终获得最高准确度的模型的完整代码。

我开始用的是没有最上面一层的预训练模型,结果非常不满意。原因可能是因为数据都是卡通图像,而我使用的预训练模型是用真实场景的图像训练而成,因此无法分类卡通图像。为了解决这个问题,我使用部分标注后的图像重新训练了预训练模型,结果比之前好多了。

但是,即便是用有标签图像训练模型后,准确度也不令人满意。模型在训练图像上表现很糟,所以我试着增加了网络层的数量。结果证明增加层的数量是提高训练准确度的好方法,但验证准确度没有提升。模型出现了过拟合,在不可见数据上的表现并不理想。所以我在每个致密层后面添加了 Dropout 层,然后验证准确度就好多了。

我注意到,类别还存在不平衡。汤姆的镜头要多出很多,因此预测结果基本上被他“垄断”了,大多数帧都被预测为汤姆(虽然大部分出镜都是汤姆被虐)。

为了解决这个问题,让类别更平衡,我使用了 sklearn.utils.class_weight 模块的 compute_class_weight() 函数。它会为 value_counts 较低的类赋予更高的权重。

我还用了 Model Checkpointing 来保存最佳模型,即生成的验证损失最低的模型。然后用该模型进行最终预测。我下面总结上面所提的全部步骤,并给出最终代码。测试图像的实际类可以在 testing.csv 文件中找到:

import cv2
import math
import matplotlib.pyplot as plt
import pandas as pd
%matplotlib inline
from keras.preprocessing import image
import numpy as np
from skimage.transform import resize

count = 0
videoFile = "Tom and jerry.mp4"
cap = cv2.VideoCapture(videoFile)
frameRate = cap.get(5) #frame rate
x=1
while(cap.isOpened()):
    frameId = cap.get(1) #current frame number
    ret, frame = cap.read()
    if (ret != True):
        break
    if (frameId % math.floor(frameRate) == 0):
        filename ="frame%d.jpg" % count;count+=1
        cv2.imwrite(filename, frame)
cap.release()
print ("Done!")

完成!

count = 0
videoFile = "Tom and Jerry 3.mp4"
cap = cv2.VideoCapture(videoFile)
frameRate = cap.get(5) #frame rate
x=1
while(cap.isOpened()):
    frameId = cap.get(1) #current frame number
    ret, frame = cap.read()
    if (ret != True):
        break
    if (frameId % math.floor(frameRate) == 0):
        filename ="test%d.jpg" % count;count+=1
        cv2.imwrite(filename, frame)
cap.release()
print ("Done!")

完成!

data = pd.read_csv('mapping.csv')
test = pd.read_csv('testing.csv')

X = []
for img_name in data.Image_ID:
    img = plt.imread('' + img_name)
    X.append(img)
X = np.array(X)

test_image = []
for img_name in test.Image_ID:
    img = plt.imread('' + img_name)
    test_image.append(img)
test_img = np.array(test_image)

from keras.utils import np_utils
train_y = np_utils.to_categorical(data.Class)
test_y = np_utils.to_categorical(test.Class)

image = []
for i in range(0,X.shape[0]):
    a = resize(X[i], preserve_range=True, output_shape=(224,224,3)).astype(int)
    image.append(a)
X = np.array(image)

test_image = []
for i in range(0,test_img.shape[0]):
    a = resize(test_img[i], preserve_range=True, output_shape=(224,224)).astype(int)
    test_image.append(a)
test_image = np.array(test_image)

from keras.applications.vgg16 import preprocess_input
X = preprocess_input(X, mode='tf')
test_image = preprocess_input(test_image, mode='tf')

from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid = train_test_split(X, train_y, test_size=0.3, random_state=42)

from keras.models import Sequential
from keras.applications.vgg16 import VGG16
from keras.layers import Dense, InputLayer, Dropout

base_model = VGG16(weights='imagenet', include_top=False, input_shape=(224, 224, 3))

X_train = base_model.predict(X_train)
X_valid = base_model.predict(X_valid)
test_image = base_model.predict(test_image)

X_train = X_train.reshape(208, 7*7*512)
X_valid = X_valid.reshape(90, 7*7*512)
test_image = test_image.reshape(186, 7*7*512)

train = X_train/X_train.max()
X_valid = X_valid/X_train.max()
test_image = test_image/test_image.max()

model = Sequential()
model.add(InputLayer((7*7*512,)))    # input layer
model.add(Dense(units=1024, activation='sigmoid'))   # hidden layer
model.add(Dropout(0.5))      # adding dropout
model.add(Dense(units=512, activation='sigmoid'))    # hidden layer
model.add(Dropout(0.5))      # adding dropout
model.add(Dense(units=256, activation='sigmoid'))    # hidden layer
model.add(Dropout(0.5))      # adding dropout
model.add(Dense(3, activation='sigmoid'))            # output layer

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

from sklearn.utils.class_weight import compute_class_weight, compute_sample_weight
class_weights = compute_class_weight('balanced',np.unique(data.Class), data.Class)  # computing weights of different classes

from keras.callbacks import ModelCheckpoint
filepath="weights.best.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
callbacks_list = [checkpoint]      # model check pointing based on validation loss

model.fit(train, y_train, epochs=100, validation_data=(X_valid, y_valid), class_weight=class_weights, callbacks=callbacks_list)

model.load_weights("weights.best.hdf5")

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
scores = model.evaluate(test_image, test_y)

print("%s: %.2f%%" % (model.metrics_names[1], scores[1]*100))

总结

最终我们使用这个模型在验证数据上得到了约为 88% 的准确度,在测试数据上的准确度为 64%。

在测试数据上准确度更低的原因可能是训练数据不足。由于模型并没有充分了解像汤姆和杰瑞这样的卡通图像,所以我们必须在训练期间向模型输入更多图像。我的建议是从更多的《猫和老鼠》视频片段中提取更多帧,将它们相应地标记,然后用它们训练模型。等模型把这两位的图像看得都快吐了的时候,就应该能得到很好的分类结果了。

像这样的模型可以在很多方面有所用途:

  • 我们可以计算某个演员在一部电影中的出镜时间
  • 计算你的偶像在一部电视剧中的镜头多少
  • 计算漫威电影中某个英雄的出场时间 ···

当然这些只是这项技术的部分应用场景,可以自己想想还有哪些用途,试着捣鼓一下,到时候分享给我们。


参考资料:Deep Learning Tutorial to Calculate the Screen Time of Actors in any Video (with Python codes)

您也许喜欢这些文章

集智专栏

[2018.03.08直播] Python之字典

发表至系列教程
2018年3月8日直播配套专栏,讲解Python另一种重要数据结构——字典,介绍字典的基本性质,常用方法,以及与控制流、列表的结合应用案例,并实现数位统计小程序。
集智专栏

[2018.01.14] “女生科技体验节” TensorFlow Workshop

发表至业界新闻
Kaiser在2018年1月14日“女生科技体验节”主持关于TensorFlow的workshop,这次用Fashion-MNIST替换了过度使用的MNIST(手写数字)数据集,并以此为实例展示TensorFlow的基本操作。
集智专栏

[2018.04.03直播] Python之文件

发表至系列教程
2018年4月1日直播配套专栏,讲解如何利用Python对本地文件进行读写等操作,并初步地完成一个数据科学实例。

文章评论(0)