譯者注:本文以一段自打24小時耳光的視訊為例子,介紹瞭如何利用均值雜湊演算法來檢查重複視訊幀。以下是譯文。
有人在網上上傳了一段視訊,他打了自己24個小時的耳光。他真的這麼做了嗎?看都不用看,肯定沒有!
前幾天,我瀏覽YouTube的時候,看到了一段非常流行的視訊。在視訊裡,一個人聲稱自己要連續打臉24小時。視訊的長度就是整整的24小時。我跳著看完了這個視訊,確實,他就是在打自己的臉。許多評論都說這個視訊是偽造的,我也是這麼想的,但我想確定這個結論。
計劃
寫一個程式來檢測視訊中是否有迴圈。我之前從來沒有用Python處理過視訊,所以這對我來說有點難度。
首次嘗試
看一個視訊就像是在快速地翻看圖片,這也是使用python讀取視訊資料的方式。我們看到的每個“圖片”都是視訊的一個幀。在視訊播放時,它是以每秒30幀的速度進行播放。
在視訊資料中,每一幀都是一個巨大的陣列。該陣列通過指定數量的紅、綠、藍進行混合來告訴我們每個位置上每個畫素的顏色。我們想看看視訊中是否有多個幀出現了多次,有一個方法,就是計算我們看到的每一幀的次數。
我用兩個字典型別的變數來進行計數。一個跟蹤我已經看到的幀,另一個跟蹤所有完全相同的幀。當我逐個瀏覽每一幀時,首先檢查以前是否看過這一幀。如果沒有,則把這一幀新增到我已看過的幀字典中(見下面的seen_frames)。如果以前看過這一幀,則將它新增到另一個字典(dup_frames)的列表中,這個字典包含了其他一模一樣的幀。
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
def find_duplicates(): # 載入視訊檔案 filename = 'video.mp4' vid = imageio.get_reader(filename, 'ffmpeg') all_frames = vid.get_length() # 重複的幀儲存在這裡 seen_frames = {} dup_frames = {} for x in range(all_frames): # 獲取單個幀 frame = vid.get_data(x) # 取幀的雜湊值 hashed = hash(frame.tostring()) if seen_frames.get( hashed, None): # 如果之前看到過這一幀,則新增到dup_frames中具有相同的雜湊值的幀列表中 dup_frames[hashed].append(x) else: # 如果這是第一次看到這一幀,則儲存到seen_frames中 seen_frames[hashed] = x dup_frames[hashed] = [x] # 返回重複幀列表的列表 return [dup_frames[x] for x in dup_frames if len(dup_frames[x]) > 1] |
這段程式碼在我的macbook pro上跑了大約一個小時。 我們來看看結果:
很好,結果看起來很直觀,從下圖中可以看出,幀5928與幀2048454相同,幀5936與幀2048462相同,以此類推。讓我們目視確認。
完美。所以,這個視訊肯定是偽造的。 然而,幀匹配的數量看起來實在太低了,值得懷疑啊。 真的只有25個相同的幀嗎?在整整24小時的視訊中這25幀的長度幾乎不到1秒鐘。我們來進一步看一下!
情況變複雜了
該程式的作用是確定相同的幀,這樣我就能知道視訊是在迴圈播放。讓我們來看看上面兩幅影象的後2秒的幀(幀5936 + 60和幀2048462 + 60)是什麼樣的。
等等…… 這兩個影象看起來是一樣的啊!但是他們為什麼沒有標記為匹配呢?我們可以把其中一個幀減去另外一個幀來找出不同之處。這個減法是對每個畫素的紅、綠、藍的值分別做減法。
太好了,我們創造出了一個很酷的故障藝術!但是,實際上兩個幀的差值僅僅是視訊被壓縮後的兩個幀的差異。由於經過了壓縮,原來相同的兩個幀可能會受到噪音的影響而導致失真,從而在數值上不再一樣(儘管它們在視覺上看起來是一樣的)。
對上面的說明總結一下,當我將資料儲存在字典中時,我取了每個影象的雜湊。雜湊函式將影象(陣列)轉換為整數。如果兩個影象完全相同,則雜湊函式將得到相同的整數。如果兩個影象不同,我們將得到兩個不同的整數。但是我們實際想要的是,如果兩個影象只是稍微不同,我們然仍然能得到相同的整數。
簡化我們的壓縮問題
有幾種不同的雜湊演算法,每種都有專門的使用場景。我們在這裡將要看到的是感知雜湊。與其他型別的雜湊不同的是,對於靠近在一起的輸入,它們的感知雜湊值是相同的。反向影象搜尋網站顯然使用的是類似的技術,這些網站只是抓取他們遇到的網路和雜湊影象。由於同一張圖片在網際網路上可能存在多種不同的解析度和剪裁,所以檢查其他具有相同雜湊值的東西則更為方便。
然而,對於我們來說,又有新的麻煩了,因為我們處理的並不完全是影象,而是一系列的影象,每一張圖片都是相差1/30秒。這意味著我們的雜湊函式需要:
- 足夠的寬鬆,兩個僅因為壓縮而產生噪聲的幀的雜湊值是相同的
- 足夠的靈敏,兩個相鄰幀的雜湊值是不同的
這可能很複雜。
均值雜湊的引數選擇
我要嘗試使用的雜湊演算法稱為均值雜湊(aHash)。在網上能找到很多的資訊,它的處理過程一般是這樣的:降低影象解析度,轉換為灰度圖,然後取雜湊值。通過降低解析度,我們可以消除噪聲的影響。然而,我們冒著相鄰幀可能會被標記為重複幀的風險,因為它們是相似的。通過調整解析度可以稍稍解決這個問題。
下面,我分別以解析度8×8和64×64顯示均值雜湊的結果。8×8看起來降取樣的太多了,我們失去了太多的資訊,似乎大多數影象看起來都是一樣的了。對於64×64,它看起來和原來的影象沒什麼不同,兩者之間可能沒有足夠大的區別來忽略壓縮產生的噪聲。
為了找到適合我們的解析度,我試著在兩段類似的視訊中通過設定一系列不同的解析度來尋找匹配項。返回的匹配項將出現在以下輸出中:
- [8,108]
- [9,109]
- [10,11,110,111]
上述的解釋是,第8幀和第108幀相同。第9幀和第109幀相同,但不同於8、108。第10、11、110、111幀與其他幀都不同,但彼此相同。這種情況很有可能發生,因為演算法並不完美,偶爾也會混淆,認為兩個相鄰的幀是相同的。我們看看下面這幾個數字:
- 有多少個匹配的桶?從上面可以看到,有3個。
- 每個桶中的平均幀數是多少?平均值為(2 + 2 + 4)/ 3 = 2.7。
- 所有桶中最多的幀是多少? 4。
這裡的目標是獲得大量的桶(第一個數字),並且每個桶內的幀數儘可能的少(平均或最差情況)。理論上來說,由於我正在看的這段視訊有1個迴圈,所以每桶應該只有2幀。
好的,看起來64太極端了,我們幾乎沒有一個桶在這一點上。另一方面,在圖形的左側,桶的大小(Bucket Size)有一個爆炸點,其中所有的幀都被檢測為重複的。這個爆炸點似乎是在20附近。從最大桶的大小(Max Bucket Size)那根曲線來看,20的那個資料點似乎有些奇怪。為了反駁這一段網上視訊,我也只願意做到這些了,那麼,讓我們一起去看看把解析度設定為24後取雜湊的情況吧。
結果
我把原來的雜湊函式換成了這個新的均值雜湊函式,並重新計算分析。瞧,出現了太多的匹配幀!匹配幀太多了,沒辦法全部顯示出來,這裡我顯示了同一桶中的一些資料:
- 4262
- 72096
- 124855
- 132392
- 147466
- 162540
- 170077
- 185151
- 207762
- 252984
- etc…
這些都是我們找到的重複幀。將它們轉換為大概的時間戳(以秒為單位,譯者注:視訊連結指向YouTube網站,請科學上網):
好極了!
如果你想要檢視這些重複的位置,你可以看看這段視訊剪輯。它正好發生在掌摑的中間! 雖說不一定能保證每個匹配幀都能找到,但是這比我們以前做的要詳細得多,我認為這已經夠好了。
我並沒有訂閱這個YouTube使用者,所以我不知道這個視訊是一個內部笑話還是其他什麼(它釋出於4月1日),但這絕對是一個有趣的專案。