平時看劇看電影的時候,不知大家有沒有想過一個很有趣的問題:各個演員在裡面的出鏡時間分別是多少?差距大嗎?而通過計算出一部劇或一部電影中演員的鏡頭多少,也能得知他們在裡面的重要程度以及在娛樂圈的影響力。想知道你的愛豆在劇裡露臉多久嗎?
機器學習愛好者 Pulkit Sharma 最近就利用深度學習知識解決了這個問題,一起看看是怎麼實現的。
在我(作者 Pulkit Sharma)剛開始研究深度學習的時候,我最先接觸的就是圖像分類。這簡直是計算機視覺領域裡最迷人的部分之一有木有?我當時完全沉迷其中無法自拔。但是後來等我熟練掌握圖像分類之後,慢慢有了一個好奇的想法:能不能把學到的知識用在視頻上。
有沒有這麼一種方法,可以創建一個模型自動識別特定人物在一段視頻中的出現時間?當然是有的,在本文我就把這個方法分享給你。
這裡多扯幾句,讓你更好地理解我們要解決的問題。記住,出鏡時間對一個演員來說是非常重要的,這直接關係到他/她的收入。所以,如果我們能計算出任一視頻中任何演員的出鏡時間,還是很好酷的,對吧?
在本文我會幫你理解如何使用深度學習處理視頻數據,教程會使用動畫片《貓和老鼠》的視頻片段作為例子。我們的目標就是計算視頻中湯姆和傑瑞的出鏡時間。
內容目錄
讀取視頻,提取視頻幀
如何用 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 :讀取視頻,提取視頻幀,將幀保存為圖像
現在我們加載視頻,將其轉換為幀。可以點擊這個連結( https://drive.google.com/file/d/1_DcwBhYo15j7AU-v2gN61qGGd1ZablGK/view ),下載本教程所用的《貓和老鼠》視頻。我們首先使用 VideoCapture() 函數從給定目錄下獲取視頻,然後從視頻中提取出幀,再用 imwrite() 函數把它們保存成圖像。開擼代碼:
count = 0
videoFile = "Tom and jerry.mp4"
cap = cv2.VideoCapture(videoFile)
frameRate = cap.get(5)
x=1
while(cap.isOpened()):
frameId = cap.get(1)
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')
plt.imshow(img)
亦可賽艇!
這是視頻的第一幀。我們從視頻中每一秒提取一幀,視頻一共長 4 分 58 秒(298 秒),所以最後我們總共有 298 張圖像。
我們下面的任務就是識別哪些圖像裡有湯姆,哪些圖像裡有傑瑞。如果我們提取出的圖像在目前最大的圖像數據集 ImageNet 中有相似的圖像,那問題就簡單多了。為啥?因為這樣我們就可以方便地使用基於 ImageNet 的預訓練模型,還能獲得很高的準確度!但是這就沒意思了,對吧?
由於我們用的是卡通圖像,所以任何預訓練模型識別一段視頻中的湯姆和傑瑞會非常困難(如果沒有對應的預訓練模型)。
步驟2:對部分圖像進行標記,用於訓練模型
那麼我們怎麼解決呢?一種可行的方法就是手動標記部分圖像,用它們訓練一個模型。等模型學習了其中的模式後,我們就可以用它來預測之前不可見的圖像。
記住,有些幀上可能既沒湯姆也沒傑瑞。所以,我們要把這個問題看作一個多類分類問題。我將其中的類定義如下:
別擔心,我已經將所有圖像打好了標籤,所以這個活兒你不用幹啦!點擊如下連結,即可下載 CSV 文件,包含了每張圖像的名稱及其對應的類(0 或 1 或 2):
https://drive.google.com/file/d/1NbU8Sdj_YNF5Dl_zbdeBcnqEyU3Xw9TU/view
data = pd.read_csv('mapping.csv')
data.head()
文件包含了 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 = np.array(X)
完成!現在我們已經獲取了圖像,記住訓練模型需要兩樣東西:
訓練圖像
它們對應的類
由於有 3 個類,我們會用 keras.utils 的 to_categorical() 函數將它們進行獨熱編碼。
y = data.Class
dummy_y = np_utils.to_categorical(y)
我們會使用一個 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)
步驟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)
X_valid = X_valid.reshape(90, 7*7*512)
現在我們預處理圖像,將它們零均值化,這樣可以幫助模型更快地轉換。
train = X_train/X_train.max()
X_valid = X_valid/X_train.max()
最後,我們搭建模型。這一步又可以劃分為 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 個類)。現在我們編譯我們的模型:
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
在最後一步中,我們會擬合模型,同時也會檢查模型在不可見圖像(即驗證集中的圖像)上的性能:
model.fit(train, y_train, epochs=100, validation_data=(X_valid, y_valid))
我們可以看到,模型在訓練集和驗證集上均取得了不錯的表現,在不可見圖像上的準確率為 85%。以上就是我們用視頻數據訓練模型並對每個幀進行預測的過程。
在下一部分,我們就來到了本文最有趣也是最重要的部分:計算一段新視頻中湯姆和傑瑞的出鏡時間。
計算出鏡時間——一種簡單方法
首先,我們下載這部分所用的視頻:
https://drive.google.com/file/d/1MQHRosZmeYpK2onCWr_A9p5SI93pEDw0/view
下載後,從視頻中提取出幀。按照前文所展示的相同步驟操作:
count = 0
videoFile = "Tom and Jerry 3.mp4"
cap = cv2.VideoCapture(videoFile)
frameRate = cap.get(5)
x=1
while(cap.isOpened()):
frameId = cap.get(1)
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)
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 文件中找到:
https://drive.google.com/file/d/1blewkgF0M6SlJp4x47MVqQEbu4NZmGuF/view
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)
x=1
while(cap.isOpened()):
frameId = cap.get(1)
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)
x=1
while(cap.isOpened()):
frameId = cap.get(1)
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,)))
model.add(Dense(units=1024, activation='sigmoid'))
model.add(Dropout(0.5))
model.add(Dense(units=512, activation='sigmoid'))
model.add(Dropout(0.5))
model.add(Dense(units=256, activation='sigmoid'))
model.add(Dropout(0.5))
model.add(Dense(3, activation='sigmoid'))
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)
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.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%。
在測試數據上準確度更低的原因可能是訓練數據不足。由於模型並沒有充分了解像湯姆和傑瑞這樣的卡通圖像,所以我們必須在訓練期間向模型輸入更多圖像。我的建議是從更多的《貓和老鼠》視頻片段中提取更多幀,將它們相應地標記,然後用它們訓練模型。等模型把這兩位的圖像看得都快吐了的時候,就應該能得到很好的分類結果了。
像這樣的模型可以在很多方面有所用途:
我們可以計算某個演員在一部電影中的出鏡時間
計算你的偶像在一部電視劇中的鏡頭多少
計算漫威電影中某個英雄的出場時間
···
當然這些只是這項技術的部分應用場景,可以自己想想還有哪些用途,試著搗鼓一下,到時候分享給我們。
參考資料: