FFmpeg中基於深度學習模型的圖像處理filter:dnn_processing介紹(1)

在FFmpeg中,一開始增加了兩個基於深度學習的video filter,分別是用來超分辨率的vf,和用來去除雨點的derain。它們都是對每幀中的內容進行的調整,包括對幀size的改變,用到的算法都是基於深度學習的模型。實際上,相比於這樣爲每一種功能算法增加一個filter的思路,我們還可以採用另外一種思路,即,設計一個通用的filter,可以用一個filter來普適所有基於深度學習模型的圖像處理算法,此即我最近爲FFmpeg增加dnn_processing的設想。本想做個全面的介紹,不過感覺缺少大塊時間,就拆分介紹,也可以順便增加篇數活躍一下。

本篇主要介紹如何使用dnn_processing來完成針對灰度圖的sobel算子的調用,sobel算子主要用作邊緣檢測,通過對像素周圍區域中的灰度值做加權相加,達到檢測邊緣的效果。我們首先要重新編譯FFmpeg源代碼,以增加對TensorFlow的支持,然後再要寫出和Sobel算子等效的TensorFlow網絡模型,最後dnn_processing會加載並執行此網絡模型,對輸入的frame逐一進行處理。爲簡化代碼,我們只關注sobel算子水平方向的特性。

  • 重新編譯FFmpeg

FFmpeg對深度學習模型的支持有兩個後端,一是調用TensorFlow C動態庫來完成,二是調用內部C代碼來實現。前者被稱爲tensorflow後端(將簡寫爲tf後端),後者被稱爲native後端。要啓用tf後端,必須在編譯FFmpeg的機器上有tensorflow c開發庫(包括.h文件和.so文件),而且在configure的時候需要傳入相應的參數。而任何情況下的編譯,native後端總是被自動啓用。這兩個後端本文都會涉及,所以,我們必須重新編譯FFmpeg。

首先,我們下載libtensorflow c開發庫,其官方下載鏈接是https://www.tensorflow.org/install/lang_c ,目前最新版本是1.15.0,順便提一句,主頁上還提到目前還不支持tensorflow 2(There is no libtensorflow support for TensorFlow 2 yet.)。但是,從1.14到1.15版本切換的時候,頭文件發生了變化,而FFmpeg最早是在2018年引入對Tensorflow的支持,所以,我們目前還無法使用1.15版本,必須使用之前的版本,比如1.14版本(derain必須用到1.14版本)。可以搜索下載libtensorflow-cpu-linux-x86_64-1.14.0.tar.gz,這個壓縮包中有以下的文件: file

將這些文件目錄拷貝到/usr/local/下面,成爲系統庫的一部分,比如/usr/local/lib/libtensorflow.so,然後,就可以開始從源代碼開始編譯FFmpeg了。具體步驟如下。

# 首先下載ffmpeg源代碼
$ git clone https://git.ffmpeg.org/ffmpeg.git
$ cd ffmpeg/

# 新建編譯目錄,以和源代碼目錄分離
$ mkdir build
$ cd build/

# 如果本機沒有找到libtensorflow,下面的configure會出錯。
# 成功的話,在輸出信息中會看到如下的libtensorflow字樣。
$ ../configure --enable-libtensorflow
...
External libraries:
alsa                    libxcb                  libxcb_xfixes           xlib
iconv                   libxcb_shape            sdl2                    zlib
libtensorflow           libxcb_shm              sndio
...

# 然後make編譯就可以了。
# 如無必要,不需要make install。
$ make

# 看一下編譯結果,其中,諸如ffmpeg_g等
# 後面帶_g的文件包括symbol信息,方便被gdb加載調試。
$ ls
config.asm  doc      ffmpeg    ffplay    ffprobe    fftools     libavdevice  libavformat  libswresample  Makefile  tests
config.h    ffbuild  ffmpeg_g  ffplay_g  ffprobe_g  libavcodec  libavfilter  libavutil    libswscale     src
  • 準備sobel算子的網絡模型

sobel算子在水平方向的3x3區域的模板如下圖所示,也就是說,如果圖片在水平方向上存在像素值的突變,會被認爲是一個邊緣。 file

上述模板從深度學習的角度來看,其實就是一個卷積層,所以,我們可以用python腳本來生成Tensorflow網絡模型,包括卷積層和必要的後處理,然後將網絡模型保存爲文件sobel.pb。我用的是python 3.5.2 和 tensorflow 1.13.1,如下所示。 file

具體的python腳本如下所示:

import tensorflow as tf
import numpy as np
from skimage import color
from skimage import io

# 讀入原始彩色圖片,因爲我們一般拿到的都是彩色圖
in_img = io.imread('input.jpg')

# 將彩色圖轉換爲灰度圖,並且保存到文件
gray_img = color.rgb2gray(in_img)
io.imsave('ori_gray.jpg', gray_img)

# 調整in_data的shape,以匹配模型輸入
in_data = np.expand_dims(gray_img, axis=0)
in_data = np.expand_dims(in_data, axis=3)

# 設置Sobel算子的水平模板,這裏無需訓練,直接設置即可。
# 卷積核的格式是[filter_height, filter_width, in_channels, out_channels]
filter_data = np.array([-1.,0,1.,-2.,0,2.,-1.,0,1.]).reshape(3,3,1,1).astype(np.float32)
filter = tf.Variable(filter_data)

# 構建網絡模型,用到了conv2d、maximum和minumum這三層。
# sobel_in是模型的輸入變量名,將被dnn_processing引用。
x = tf.placeholder(tf.float32, shape=[1, None, None, 1], name='sobel_in')
conv = tf.nn.conv2d(x, filter, strides=[1, 1, 1, 1], padding='VALID')

# 下面兩個函數將數據clip到有效數值範圍[0.0, 1.0]。
y = tf.math.minimum(conv, 1.0)
# sobel_out是模型的輸出變量名,將被dnn_processing引用。
y = tf.math.maximum(y, 0.0, name='sobel_out')

# 模型的執行準備
sess=tf.Session()
sess.run(tf.global_variables_initializer())

# 將模型文件保存爲sobel.pb,這個文件包括了一切信息,將被ffmpeg所使用
graph_def = tf.graph_util.convert_variables_to_constants(sess, sess.graph_def, ['sobel_out'])
tf.train.write_graph(graph_def, '.', 'sobel.pb', as_text=False)

# 執行模型,並將結果保存爲圖片文件
output = sess.run(y, feed_dict={x: in_data})
output = np.squeeze(output)
io.imsave("out.jpg", output)

隨意的用手機拍了一張照片如下,作爲python腳本的輸入。 輸入原始圖片input.jpg

執行腳本過程中生成的灰度圖如下所示: 輸入灰度圖ori_gray.jpg

腳本執行完畢後得到的輸出圖片如下所示,可以看出,從水平方向分析的邊緣基本都被檢測出來了。我們將在ffmpeg中使用dnn_processing來達到這樣的效果。 輸出圖片out.jpg

作爲腳本最重要的輸出,我們可以看到網絡模型文件sobel.pb已經生成。實際上,sobel.pb就是我們編寫腳本的關鍵目的,腳本的其他部分都是爲了確保腳本的正確性。

$ ls sobel.pb -sh
4.0K sobel.pb

由於sobel.pb只能被用於tf後端,爲了支持native後端,我們還需要從sobel.pb文件生成sobel.model文件,這是native後端支持的模型文件。在FFmpeg內部已經有了自動轉換的方法,如下所示:

$ cd /your_path_to_ffmpeg_source_tree/ffmpeg
$ cd tools/python
$ python convert.py /your_path/sobel.pb
$ ls sobel.model -sh
4.0K sobel.model
  • 執行dnn_processing

萬事俱備,只欠執行。FFmpeg支持視頻作爲輸入,也支持單張圖片作爲輸入。爲了和之前的python腳本有對比,我們繼續用input.jpg作爲輸入,以out.jpg作爲輸出。具體執行命令如下所示:

# 回到一開始的編譯目錄
$ cd /your_path_to_ffmpeg_source_tree/ffmpeg/build

# 爲避免命令行參數過長,可以將input.jpg文件和
# 兩個模型文件sobel.pb和sobel.model拷貝到當前目錄下。

# 調用tf後端執行,會顯式用到兩個filter。
# 首先,調用format,將格式轉換爲grayf32,因爲這是網絡模型需要的格式。
# 然後則是dnn_processing,其參數比較簡單,
# 通過model參數指出模型文件,
# 而input和output則是網絡模型的輸入和輸出變量名,在內部通過變量名實現和AVFrame的數據交互,
# dnn_backend參數則是指出後端使用tf。
# 最後,輸出結果在out.jpg中。
$ ./ffmpeg -i input.jpg -vf \
format=grayf32, \
dnn_processing=model=sobel.pb:input=sobel_in:output=sobel_out:dnn_backend=tensorflow \
out.jpg

# 如果調用native後端執行的話,只需要修改model和dnn_backend這兩個參數即可。
# 由於native後端還不成熟,這裏是還不支持tf.math.minimum,所以這個命令並不會真正成功。
$ ./ffmpeg -i input.jpg -vf \
format=grayf32,\
dnn_processing=model=sobel.model:input=sobel_in:output=sobel_out:dnn_backend=native \
out.jpg

如果模型的輸入輸出接受uint8類型,即0到255的像素值的話,那麼上述命令中的format的參數應是gray8,即format=gray8。

dnn_processing還支持RGB和YUV的輸入格式,將在後續介紹。

以上內容是本人業餘時間興趣之作,限於水平,差錯難免,僅代表個人觀點,和本人任職公司無關。

本文由博客一文多發平臺 OpenWrite 發佈!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章