基於 TensorFlow 在手機端實現文檔檢測

做者:馮牮html

前言

  • 本文不是神經網絡或機器學習的入門教學,而是經過一個真實的產品案例,展現了在手機客戶端上運行一個神經網絡的關鍵技術點
  • 在卷積神經網絡適用的領域裏,已經出現了一些很經典的圖像分類網絡,好比 VGG16/VGG19,Inception v1-v4 Net,ResNet 等,這些分類網絡一般又均可以做爲其餘算法中的基礎網絡結構,尤爲是 VGG 網絡,被不少其餘的算法借鑑,本文也會使用 VGG16 的基礎網絡結構,可是不會對 VGG 網絡作詳細的入門教學
  • 雖然本文不是神經網絡技術的入門教程,可是仍然會給出一系列的相關入門教程和技術文檔的連接,有助於進一步理解本文的內容
  • 具體使用到的神經網絡算法,只是本文的一個組成部分,除此以外,本文還介紹瞭如何裁剪 TensorFlow 靜態庫以便於在手機端運行,如何準備訓練樣本圖片,以及訓練神經網絡時的各類技巧等等

需求是什麼

image to point

需求很容易描述清楚,如上圖,就是在一張圖裏,把矩形形狀的文檔的四個頂點的座標找出來。python

傳統的技術方案

Google 搜索 opencv scan document,是能夠找到好幾篇相關的教程的,這些教程裏面的技術手段,也都大同小異,關鍵步驟就是調用 OpenCV 裏面的兩個函數,cv2.Canny() 和 cv2.findContours()。git

demo method

看上去很容易就能實現出來,可是真實狀況是,這些教程,僅僅是個 demo 演示而已,用來演示的圖片,都是最理想的簡單狀況,真實的場景圖片會比這個複雜的多,會有各類干擾因素,調用 canny 函數獲得的邊緣檢測結果,也會比 demo 中的狀況凌亂的多,好比會檢測出不少各類長短的線段,或者是文檔的邊緣線被截斷成了好幾條短的線段,線段之間還存在距離不等的空隙。另外,findContours 函數也只能檢測閉合的多邊形的頂點,可是並不能確保這個多邊形就是一個合理的矩形。所以在咱們的初版技術方案中,對這兩個關鍵步驟,進行了大量的改進和調優,歸納起來就是:github

  • 改進 canny 算法的效果,增長額外的步驟,獲得效果更好的邊緣檢測圖
  • 針對 canny 步驟獲得的邊緣圖,創建一套數學算法,從邊緣圖中尋找出一個合理的矩形區域

傳統技術方案的難度和侷限性

  • canny 算法的檢測效果,依賴於幾個閥值參數,這些閥值參數的選擇,一般都是人爲設置的經驗值,在改進的過程當中,引入額外的步驟後,一般又會引入一些新的閥值參數,一樣,也是依賴於調試結果設置的經驗值。總體來看,這些閥值參數的個數,不能特別的多,由於一旦太多了,就很難依賴經驗值進行設置,另外,雖然有這些閥值參數,可是最終的參數只是一組或少數幾組固定的組合,因此算法的魯棒性又會打折扣,很容易遇到邊緣檢測效果不理想的場景
  • 在邊緣圖上創建的數學模型很複雜,代碼實現難度大,並且也會遇到算法無能爲力的場景

下面這張圖表,可以很好的說明上面列出的這兩個問題:算法

hed vs canny

這張圖表的第一列是輸入的 image,最後的三列(先不用看這張圖表的第二列),是用三組不一樣閥值參數調用 canny 函數和額外的函數後獲得的輸出 image,能夠看到,邊緣檢測的效果,並不老是很理想的,有些場景中,矩形的邊,出現了很嚴重的斷裂,有些邊,甚至被徹底擦除掉了,而另外一些場景中,又會檢測出不少干擾性質的長短邊。可想而知,想用一個數學模型,適應這麼不規則的邊緣圖,會是多麼困難的一件事情。spring

思考如何改善

在初版的技術方案中,負責的同窗花費了大量的精力進行各類調優,終於取得了還不錯的效果,可是,就像前面描述的那樣,仍是會遇到檢測不出來的場景。在初版技術方案中,遇到這種狀況的時候,採用的作法是針對這些不能檢測的場景,人工進行分析和調試,調整已有的一組閥值參數和算法,可能還須要加入一些其餘的算法流程(可能還會引入新的一些閥值參數),而後再整合到原有的代碼邏輯中。通過若干輪這樣的調整後,咱們發現,已經進入一個瓶頸,按照這種手段,很難進一步提升檢測效果了。shell

既然傳統的算法手段已經到極限了,那不如試試機器學習/神經網絡。api

無效的神經網絡算法

end-to-end 直接擬合

首先想到的,就是仿照人臉對齊(face alignment)的思路,構建一個端到端(end-to-end)的網絡,直接回歸擬合,也就是讓這個神經網絡直接輸出 4 個頂點的座標,可是,通過嘗試後發現,根本擬合不出來。後來仔細琢磨了一下,以爲不能直接擬合也是對的,由於:微信

  • 除了分類(classification)問題以外,全部的需求看上去都像是一個迴歸(regression)問題,若是迴歸是萬能的,學術界爲啥還要去搞其餘各類各樣的網絡模型
  • face alignment 之因此能夠用迴歸網絡獲得很好的擬合效果,是由於在輸入 image 上先作了 bounding box 檢測,縮小了人臉圖像範圍後,才作的 regression
  • 人臉上的關鍵特徵點,具備特別明顯的統計學特徵,因此 regression 能夠發揮做用
  • 在須要更高檢測精度的場景中,其實也是用到了更復雜的網絡模型來解決 face alignment 問題的

YOLO && FCN

後來還嘗試過用 YOLO 網絡作 Object Detection,用 FCN 網絡作像素級的 Semantic Segmentation,可是結果都很不理想,好比:網絡

  • 達不到文檔檢測功能想要的精確度
  • 網絡結構複雜,運算量大,在手機上沒法作到實時檢測

有效的神經網絡算法

前面嘗試的幾種神經網絡算法,都不能獲得想要的效果,後來換了一種思路,既然傳統的技術手段裏包含了兩個關鍵的步驟,那能不能用神經網絡來分別改善這兩個步驟呢,通過分析發現,能夠嘗試用神經網絡來替換 canny 算法,也就是用神經網絡來對圖像中的矩形區域進行邊緣檢測,只要這個邊緣檢測可以去除更多的干擾因素,那第二個步驟裏面的算法也就能夠變得更簡單了。

神經網絡的輸入和輸出

image to edge

按照這種思路,對於神經網絡部分,如今的需求變成了上圖所示的樣子。

HED(Holistically-Nested Edge Detection) 網絡

邊緣檢測這種需求,在圖像處理領域裏面,一般叫作 Edge Detection 或 Contour Detection,按照這個思路,找到了 Holistically-Nested Edge Detection 網絡模型。

HED 網絡模型是在 VGG16 網絡結構的基礎上設計出來的,因此有必要先看看 VGG16。

vgg detail

上圖是 VGG16 的原理圖,爲了方便從 VGG16 過渡到 HED,咱們先把 VGG16 變成下面這種示意圖:

vgg to hed 1

在上面這個示意圖裏,用不一樣的顏色區分了 VGG16 的不一樣組成部分。

vgg to hed 2

從示意圖上能夠看到,綠色表明的卷積層和紅色表明的池化層,能夠很明顯的劃分出五組,上圖用紫色線條框出來的就是其中的第三組。

vgg to hed 3

HED 網絡要使用的就是 VGG16 網絡裏面的這五組,後面部分的 fully connected 層和 softmax 層,都是不須要的,另外,第五組的池化層(紅色)也是不須要的。

vgg to hed 4

去掉不須要的部分後,就獲得上圖這樣的網絡結構,由於有池化層的做用,從第二組開始,每一組的輸入 image 的長寬值,都是前一組的輸入 image 的長寬值的一半。

vgg to hed 5

HED 網絡是一種多尺度多融合(multi-scale and multi-level feature learning)的網絡結構,所謂的多尺度,就是如上圖所示,把 VGG16 的每一組的最後一個卷積層(綠色部分)的輸出取出來,由於每一組獲得的 image 的長寬尺寸是不同的,因此這裏還須要用轉置卷積(transposed convolution)/反捲積(deconv)對每一組獲得的 image 再作一遍運算,從效果上看,至關於把第二至五組獲得的 image 的長寬尺寸分別擴大 2 至 16 倍,這樣在每一個尺度(VGG16 的每一組就是一個尺度)上獲得的 image,都是相同的大小了。

vgg to hed 6

把每個尺度上獲得的相同大小的 image,再融合到一塊兒,這樣就獲得了最終的輸出 image,也就是具備邊緣檢測效果的 image。

基於 TensorFlow 編寫的 HED 網絡結構代碼以下:

def hed_net(inputs, batch_size):
    # ref https://github.com/s9xie/hed/blob/master/examples/hed/train_val.prototxt
    with tf.variable_scope('hed', 'hed', [inputs]):
        with slim.arg_scope([slim.conv2d, slim.fully_connected],
                        activation_fn=tf.nn.relu,
                        weights_initializer=tf.truncated_normal_initializer(0.0, 0.01),
                        weights_regularizer=slim.l2_regularizer(0.0005)):
            # vgg16 conv && max_pool layers
            net = slim.repeat(inputs, 2, slim.conv2d, 12, [3, 3], scope='conv1')
            dsn1 = net
            net = slim.max_pool2d(net, [2, 2], scope='pool1')

            net = slim.repeat(net, 2, slim.conv2d, 24, [3, 3], scope='conv2')
            dsn2 = net
            net = slim.max_pool2d(net, [2, 2], scope='pool2')

            net = slim.repeat(net, 3, slim.conv2d, 48, [3, 3], scope='conv3')
            dsn3 = net
            net = slim.max_pool2d(net, [2, 2], scope='pool3')

            net = slim.repeat(net, 3, slim.conv2d, 96, [3, 3], scope='conv4')
            dsn4 = net
            net = slim.max_pool2d(net, [2, 2], scope='pool4')

            net = slim.repeat(net, 3, slim.conv2d, 192, [3, 3], scope='conv5')
            dsn5 = net
            # net = slim.max_pool2d(net, [2, 2], scope='pool5') # no need this pool layer

            # dsn layers
            dsn1 = slim.conv2d(dsn1, 1, [1, 1], scope='dsn1')
            # no need deconv for dsn1

            dsn2 = slim.conv2d(dsn2, 1, [1, 1], scope='dsn2')
            deconv_shape = tf.pack([batch_size, const.image_height, const.image_width, 1])
            dsn2 = deconv_mobile_version(dsn2, 2, deconv_shape) # deconv_mobile_version can work on mobile

            dsn3 = slim.conv2d(dsn3, 1, [1, 1], scope='dsn3')
            deconv_shape = tf.pack([batch_size, const.image_height, const.image_width, 1])
            dsn3 = deconv_mobile_version(dsn3, 4, deconv_shape)

            dsn4 = slim.conv2d(dsn4, 1, [1, 1], scope='dsn4')
            deconv_shape = tf.pack([batch_size, const.image_height, const.image_width, 1])
            dsn4 = deconv_mobile_version(dsn4, 8, deconv_shape)

            dsn5 = slim.conv2d(dsn5, 1, [1, 1], scope='dsn5')
            deconv_shape = tf.pack([batch_size, const.image_height, const.image_width, 1])
            dsn5 = deconv_mobile_version(dsn5, 16, deconv_shape)

            # dsn fuse
            dsn_fuse = tf.concat(3, [dsn1, dsn2, dsn3, dsn4, dsn5])
            dsn_fuse = tf.reshape(dsn_fuse, [batch_size, const.image_height, const.image_width, 5]) #without this, will get error: ValueError: Number of in_channels must be known.

            dsn_fuse = slim.conv2d(dsn_fuse, 1, [1, 1], scope='dsn_fuse')

    return dsn_fuse, dsn1, dsn2, dsn3, dsn4, dsn5

訓練網絡

cost 函數

論文給出的 HED 網絡是一個通用的邊緣檢測網絡,按照論文的描述,每個尺度上獲得的 image,都須要參與 cost 的計算,這部分的代碼以下:

input_queue_for_train = tf.train.string_input_producer([FLAGS.csv_path])
image_tensor, annotation_tensor = input_image_pipeline(dataset_root_dir_string, input_queue_for_train, FLAGS.batch_size)

dsn_fuse, dsn1, dsn2, dsn3, dsn4, dsn5 = hed_net(image_tensor, FLAGS.batch_size)

cost = class_balanced_sigmoid_cross_entropy(dsn_fuse, annotation_tensor) + \
       class_balanced_sigmoid_cross_entropy(dsn1, annotation_tensor) + \
       class_balanced_sigmoid_cross_entropy(dsn2, annotation_tensor) + \
       class_balanced_sigmoid_cross_entropy(dsn3, annotation_tensor) + \
       class_balanced_sigmoid_cross_entropy(dsn4, annotation_tensor) + \
       class_balanced_sigmoid_cross_entropy(dsn5, annotation_tensor)

按照這種方式訓練出來的網絡,檢測到的邊緣線是有一點粗的,爲了獲得更細的邊緣線,經過屢次試驗找到了一種優化方案,代碼以下:

input_queue_for_train = tf.train.string_input_producer([FLAGS.csv_path])
image_tensor, annotation_tensor = input_image_pipeline(dataset_root_dir_string, input_queue_for_train, FLAGS.batch_size)

dsn_fuse, _, _, _, _, _ = hed_net(image_tensor, FLAGS.batch_size)

cost = class_balanced_sigmoid_cross_entropy(dsn_fuse, annotation_tensor)

也就是再也不讓每一個尺度上獲得的 image 都參與 cost 的計算,只使用融合後獲得的最終 image 來進行計算。

兩種 cost 函數的效果對好比下圖所示,右側是優化事後的效果:

edge thickness

另外還有一點,按照 HED 論文裏的要求,計算 cost 的時候,不能使用常見的方差 cost,而應該使用 cost-sensitive loss function,代碼以下:

def class_balanced_sigmoid_cross_entropy(logits, label, name='cross_entropy_loss'):
    """
    The class-balanced cross entropy loss,
    as in `Holistically-Nested Edge Detection
    <http://arxiv.org/abs/1504.06375>`_.
    This is more numerically stable than class_balanced_cross_entropy

    :param logits: size: the logits.
    :param label: size: the ground truth in {0,1}, of the same shape as logits.
    :returns: a scalar. class-balanced cross entropy loss
    """
    y = tf.cast(label, tf.float32)

    count_neg = tf.reduce_sum(1. - y) # the number of 0 in y
    count_pos = tf.reduce_sum(y) # the number of 1 in y (less than count_neg)
    beta = count_neg / (count_neg + count_pos)

    pos_weight = beta / (1 - beta)
    cost = tf.nn.weighted_cross_entropy_with_logits(logits, y, pos_weight)
    cost = tf.reduce_mean(cost * (1 - beta), name=name)

    return cost

轉置卷積層的雙線性初始化

在嘗試 FCN 網絡的時候,就被這個問題卡住過很長一段時間,按照 FCN 的要求,在使用轉置卷積(transposed convolution)/反捲積(deconv)的時候,要把卷積核的值初始化成雙線性放大矩陣(bilinear upsampling kernel),而不是經常使用的正態分佈隨機初始化,同時還要使用很小的學習率,這樣才更容易讓模型收斂。

HED 的論文中,並無明確的要求也要採用這種方式初始化轉置卷積層,可是,在訓練過程當中發現,採用這種方式進行初始化,模型才更容易收斂。

這部分的代碼以下:

def get_kernel_size(factor):
    """
    Find the kernel size given the desired factor of upsampling.
    """
    return 2 * factor - factor % 2


def upsample_filt(size):
    """
    Make a 2D bilinear kernel suitable for upsampling of the given (h, w) size.
    """
    factor = (size + 1) // 2
    if size % 2 == 1:
        center = factor - 1
    else:
        center = factor - 0.5
    og = np.ogrid[:size, :size]
    return (1 - abs(og[0] - center) / factor) * (1 - abs(og[1] - center) / factor)


def bilinear_upsample_weights(factor, number_of_classes):
    """
    Create weights matrix for transposed convolution with bilinear filter
    initialization.
    """
    filter_size = get_kernel_size(factor)
    
    weights = np.zeros((filter_size,
                        filter_size,
                        number_of_classes,
                        number_of_classes), dtype=np.float32)
    
    upsample_kernel = upsample_filt(filter_size)
    
    for i in xrange(number_of_classes):
        weights[:, :, i, i] = upsample_kernel
    
    return weights

訓練過程冷啓動

HED 網絡不像 VGG 網絡那樣很容易就進入收斂狀態,也不太容易進入指望的理想狀態,主要是兩方面的緣由:

  • 前面提到的轉置卷積層的雙線性初始化,就是一個重要因素,由於在 4 個尺度上,都須要反捲積,若是反捲積層不能收斂,那整個 HED 都不會進入指望的理想狀態
  • 另一個緣由,是由 HED 的多尺度引發的,既然是多尺度了,那每一個尺度上獲得的 image 都應該對模型的最終輸出 image 產生貢獻,在訓練的過程當中發現,若是輸入 image 的尺寸是 224*224,仍是很容易就訓練成功的,可是當把輸入 image 的尺寸調整爲 256*256 後,很容易出現一種情況,就是 5 個尺度上獲得的 image,會有 1 ~ 2 個 image 是無效的(所有是黑色)

爲了解決這裏遇到的問題,採用的辦法就是先使用少許樣本圖片(好比 2000 張)訓練網絡,在很短的訓練時間(好比迭代 1000 次)內,若是 HED 網絡不能表現出收斂的趨勢,或者不能達到 5 個尺度的 image 所有有效的狀態,那就直接放棄這輪的訓練結果,從新開啓下一輪訓練,直到滿意爲止,而後才使用完整的訓練樣本集合繼續訓練網絡。

訓練數據集(大量合成數據 + 少許真實數據)

HED 論文裏使用的訓練數據集,是針對通用的邊緣檢測目的的,什麼形狀的邊緣都有,好比下面這種:

hed training dataset 1

用這份數據訓練出來的模型,在作文檔掃描的時候,檢測出來的邊緣效果並不理想,並且這份訓練數據集的樣本數量也很小,只有一百多張圖片(由於這種圖片的人工標註成本過高了),這也會影響模型的質量。

如今的需求裏,要檢測的是具備必定透視和旋轉變換效果的矩形區域,因此能夠大膽的猜想,若是準備一批針對性更強的訓練樣本,應該是能夠獲得更好的邊緣檢測效果的。

藉助初版技術方案收集回來的真實場景圖片,咱們開發了一套簡單的標註工具,人工標註了 1200 張圖片(標註這 1200 張圖片的時間成本也很高),可是這 1200 多張圖片仍然有不少問題,好比對於神經網絡來講,1200 個訓練樣本其實仍是不夠的,另外,這些圖片覆蓋的場景其實也比較少,有些圖片的類似度比較高,這樣的數據放到神經網絡裏訓練,泛化的效果並很差。

因此,還採用技術手段,合成了80000多張訓練樣本圖片。

hed training dataset 2

如上圖所示,一張背景圖和一張前景圖,能夠合成出一對訓練樣本數據。在合成圖片的過程當中,用到了下面這些技術和技巧:

  • 在前景圖上添加旋轉、平移、透視變換
  • 對背景圖進行了隨機的裁剪
  • 經過試驗對比,生成合適寬度的邊緣線
  • OpenCV 不支持透明圖層之間的旋轉和透視變換操做,只能使用最低精度的插值算法,爲了改善這一點,後續改爲了使用 iOS 模擬器,經過 CALayer 上的操做來合成圖片
  • 在不斷改進訓練樣本的過程當中,還根據真實樣本圖片的統計狀況和各類途徑的反饋信息,刻意模擬了一些更復雜的樣本場景,好比凌亂的背景環境、直線邊緣干擾等等

通過不斷的調整和優化,最終才訓練出一個滿意的模型,能夠再次經過下面這張圖表中的第二列看一下神經網絡模型的邊緣檢測效果:

hed vs canny

在手機設備上運行 TensorFlow

在手機上使用 TensorFlow 庫

TensorFlow 官方是支持 iOS 和 Android 的,並且有清晰的文檔,照着作就行。可是由於 TensorFlow 是依賴於 protobuf 3 的,因此有可能會遇到一些其餘的問題,好比下面這兩種,就是咱們在兩個不一樣的 iOS APP 中遇到的問題和解決辦法,能夠做爲一個參考:

  • A 產品使用的是 protobuf 2,同時因爲各類歷史緣由,使用而且停留在了很舊的某個版本的 Base 庫上,而 protobuf 3 的內部也使用了 Base 庫,當 A 產品升級到 protobuf 3 後,protobuf 3 的 Base 庫和 A 源碼中的 Base 庫產生了一些奇怪的衝突,最後的解決辦法是手動修改了 A 源碼中的 Base 庫,避免編譯時的衝突
  • B 產品也是使用的 protobuf 2,並且 B 產品使用到的多個第三方模塊(沒有源碼,只有二進制文件)也是依賴於 protobuf 2,直接升級 B 產品使用的 protobuf 庫就行不通了,最後採用的方法是修改 TensorFlow 和 TensorFlow 中使用的 protobuf 3 的源代碼,把 protobuf 3 換了一個命名空間,這樣兩個不一樣版本的 protobuf 庫就能夠共存了

Android 上由於自己是可使用動態庫的,因此即使 app 必須使用 protobuf 2 也沒有關係,不一樣的模塊使用 dlopen 的方式加載各自須要的特定版本的庫就能夠了。

在手機上使用訓練獲得的模型文件

模型一般都是在 PC 端訓練的,對於大部分使用者,都是用 Python 編寫的代碼,獲得 ckpt 格式的模型文件。在使用模型文件的時候,一種作法就是用代碼從新構建出完整的神經網絡,而後加載這個 ckpt 格式的模型文件,若是是在 PC 上使用模型文件,用這個方法其實也是能夠接受的,複製粘貼一下 Python 代碼就能夠從新構建整個神經網絡。可是,在手機上只能使用 TensorFlow 提供的 C++ 接口,若是仍是用一樣的思路,就須要用 C++ API 從新構建一遍神經網絡,這個工做量就有點大了,並且 C++ API 使用起來比 Python API 複雜的多,因此,在 PC 上訓練完網絡後,還須要把 ckpt 格式的模型文件轉換成 pb 格式的模型文件,這個 pb 格式的模型文件,是用 protobuf 序列化獲得的二進制文件,裏面包含了神經網絡的具體結構以及每一個矩陣的數值,使用這個 pb 文件的時候,不須要再用代碼構建完整的神經網絡結構,只須要反序列化一下就能夠了,這樣的話,用 C++ API 編寫的代碼就會簡單不少,其實這也是 TensorFlow 推薦的使用方法,在 PC 上使用模型的時候,也應該使用這種 pb 文件(訓練過程當中使用 ckpt 文件)。

HED 網絡在手機上遇到的奇怪 crash

在手機上加載 pb 模型文件而且運行的時候,遇到過一個詭異的錯誤,內容以下:

Invalid argument: No OpKernel was registered to support Op 'Mul' with these attrs.  Registered devices: [CPU], Registered kernels:
  device='CPU'; T in [DT_FLOAT]

     [[Node: hed/mul_1 = Mul[T=DT_INT32](hed/strided_slice_2, hed/mul_1/y)]]

之因此詭異,是由於從字面上看,這個錯誤的含義是缺乏乘法操做(Mul),可是我用其餘的神經網絡模型作過對比,乘法操做模塊是能夠正常工做的。

Google 搜索後發現不少人遇到過相似的狀況,可是錯誤信息又並不相同,後來在 TensorFlow 的 github issues 裏終於找到了線索,綜合起來解釋,是由於 TensorFlow 是基於操做(Operation)來模塊化設計和編碼的,每個數學計算模塊就是一個 Operation,因爲各類緣由,好比內存佔用大小、GPU 獨佔操做等等,mobile 版的 TensorFlow,並無包含全部的 Operation,mobile 版的 TensorFlow 支持的 Operation 只是 PC 完整版 TensorFlow 的一個子集,我遇到的這個錯誤,就是由於使用到的某個 Operation 並不支持 mobile 版。

按照這個線索,在 Python 代碼中逐個排查,後來定位到了出問題的代碼,修改先後的代碼以下:

def deconv(inputs, upsample_factor):
    input_shape = tf.shape(inputs)

    # Calculate the ouput size of the upsampled tensor
    upsampled_shape = tf.pack([input_shape[0],
                               input_shape[1] * upsample_factor,
                               input_shape[2] * upsample_factor,
                               1])

    upsample_filter_np = bilinear_upsample_weights(upsample_factor, 1)
    upsample_filter_tensor = tf.constant(upsample_filter_np)

    # Perform the upsampling
    upsampled_inputs = tf.nn.conv2d_transpose(inputs, upsample_filter_tensor,
                                              output_shape=upsampled_shape,
                                              strides=[1, upsample_factor, upsample_factor, 1])

    return upsampled_inputs

def deconv_mobile_version(inputs, upsample_factor, upsampled_shape):
    upsample_filter_np = bilinear_upsample_weights(upsample_factor, 1)
    upsample_filter_tensor = tf.constant(upsample_filter_np)

    # Perform the upsampling
    upsampled_inputs = tf.nn.conv2d_transpose(inputs, upsample_filter_tensor,
                                              output_shape=upsampled_shape,
                                              strides=[1, upsample_factor, upsample_factor, 1])

    return upsampled_inputs

問題就是由 deconv 函數中的 tf.shape 和 tf.pack 這兩個操做引發的,在 PC 版代碼中,爲了簡潔,是基於這兩個操做,自動計算出 upsampled_shape,修改事後,則是要求調用者用 hard coding 的方式設置對應的 upsampled_shape。

裁剪 TensorFlow

TensorFlow 是一個很龐大的框架,對於手機來講,它佔用的體積是比較大的,因此須要儘可能的縮減 TensorFlow 庫佔用的體積。

其實在解決前面遇到的那個 crash 問題的時候,已經指明瞭一種裁剪的思路,既然 mobile 版的 TensorFlow 原本就是 PC 版的一個子集,那就意味着能夠根據具體的需求,讓這個子集變得更小,這也就達到了裁剪的目的。具體來講,就是修改 TensorFlow 源碼中的 tensorflow/tensorflow/contrib/makefile/tf_op_files.txt 文件,只保留使用到了的模塊。針對 HED 網絡,原有的 200 多個模塊裁剪到只剩 46 個,裁剪事後的 tf_op_files.txt 文件以下:

tensorflow/core/kernels/xent_op.cc
tensorflow/core/kernels/where_op.cc
tensorflow/core/kernels/unpack_op.cc
tensorflow/core/kernels/transpose_op.cc
tensorflow/core/kernels/transpose_functor_cpu.cc
tensorflow/core/kernels/tensor_array_ops.cc
tensorflow/core/kernels/tensor_array.cc
tensorflow/core/kernels/split_op.cc
tensorflow/core/kernels/split_v_op.cc
tensorflow/core/kernels/split_lib_cpu.cc
tensorflow/core/kernels/shape_ops.cc
tensorflow/core/kernels/session_ops.cc
tensorflow/core/kernels/sendrecv_ops.cc
tensorflow/core/kernels/reverse_op.cc
tensorflow/core/kernels/reshape_op.cc
tensorflow/core/kernels/relu_op.cc
tensorflow/core/kernels/pooling_ops_common.cc
tensorflow/core/kernels/pack_op.cc
tensorflow/core/kernels/ops_util.cc
tensorflow/core/kernels/no_op.cc
tensorflow/core/kernels/maxpooling_op.cc
tensorflow/core/kernels/matmul_op.cc
tensorflow/core/kernels/immutable_constant_op.cc
tensorflow/core/kernels/identity_op.cc
tensorflow/core/kernels/gather_op.cc
tensorflow/core/kernels/gather_functor.cc
tensorflow/core/kernels/fill_functor.cc
tensorflow/core/kernels/dense_update_ops.cc
tensorflow/core/kernels/deep_conv2d.cc
tensorflow/core/kernels/xsmm_conv2d.cc
tensorflow/core/kernels/conv_ops_using_gemm.cc
tensorflow/core/kernels/conv_ops_fused.cc
tensorflow/core/kernels/conv_ops.cc
tensorflow/core/kernels/conv_grad_filter_ops.cc
tensorflow/core/kernels/conv_grad_input_ops.cc
tensorflow/core/kernels/conv_grad_ops.cc
tensorflow/core/kernels/constant_op.cc
tensorflow/core/kernels/concat_op.cc
tensorflow/core/kernels/concat_lib_cpu.cc
tensorflow/core/kernels/bias_op.cc
tensorflow/core/ops/sendrecv_ops.cc
tensorflow/core/ops/no_op.cc
tensorflow/core/ops/nn_ops.cc
tensorflow/core/ops/nn_grad.cc
tensorflow/core/ops/array_ops.cc
tensorflow/core/ops/array_grad.cc

須要強調的一點是,這種操做思路,是針對不一樣的神經網絡結構有不一樣的裁剪方式,原則就是用到什麼模塊就保留什麼模塊。固然,由於有些模塊之間還存在隱含的依賴關係,因此裁剪的時候也是要反覆嘗試屢次才能成功的。

除此以外,還有下面這些通用手段也能夠實現裁剪的目的:

  • 編譯器級別的 strip 操做,在連接的時候會自動的把沒有調用到的函數去除掉(集成開發環境裏一般已經自動將這些參數設置成了最佳組合)
  • 藉助一些高級技巧和工具,對二進制文件進行瘦身

藉助全部這些裁剪手段,最終咱們的 ipa 安裝包的大小隻增長了 3M。若是不作手動裁剪這一步,那 ipa 的增量,則是 30M 左右。

裁剪 HED 網絡

按照 HED 論文給出的參考信息,獲得的模型文件的大小是 56M,對於手機來講也是比較大的,並且模型越大也意味着計算量越大,因此須要考慮可否把 HED 網絡也裁剪一下。

HED 網絡是用 VGG16 做爲基礎網絡結構,而 VGG 又是一個獲得普遍驗證的基礎網絡結構,所以修改 HED 的總體結構確定不是一個明智的選擇,至少不是首選的方案。

考慮到如今的需求,只是檢測矩形區域的邊緣,而並非檢測通用場景下的廣義的邊緣,能夠認爲前者的複雜度比後者更低,因此一種可行的思路,就是保留 HED 的總體結構,修改 VGG 每一組卷積層裏面的卷積核的數量,讓 HED 網絡變的更『瘦』。

按照這種思路,通過屢次調整和嘗試,最終獲得了一組合適的卷積核的數量參數,對應的模型文件只有 4.2M,在 iPhone 7P 上,處理每幀圖片的時間消耗是 0.1 秒左右,知足實時性的要求。

神經網絡的裁剪,目前在學術界也是一個很熱門的領域,有好幾種不一樣的理論來實現不一樣目的的裁剪,可是,也並非說每一種網絡結構都有裁剪的空間,一般來講,應該結合實際狀況,使用合適的技術手段,選擇一個合適大小的模型文件。

TensorFlow API 的選擇

TensorFlow 的 API 是很靈活的,也比較底層,在學習過程當中發現,每一個人寫出來的代碼,風格差別很大,並且不少工程師又採用了各類各樣的技巧來簡化代碼,可是這其實反而在無形中又增長了代碼的閱讀難度,也不利於代碼的複用。

第三方社區和 TensorFlow 官方,都意識到了這個問題,因此更好的作法是,使用封裝度更高但又保持靈活性的 API 來進行開發。本文中的代碼,就是使用 TensorFlow-Slim 編寫的。

OpenCV 算法

雖然用神經網絡技術,已經獲得了一個比 canny 算法更好的邊緣檢測效果,可是,神經網絡也並非萬能的,干擾是仍然存在的,因此,第二個步驟中的數學模型算法,仍然是須要的,只不過由於第一個步驟中的邊緣檢測有了大幅度改善,因此第二個步驟中的算法,獲得了適當的簡化,並且算法總體的適應性也更強了。

這部分的算法以下圖所示:

find rect 1

按照編號順序,幾個關鍵步驟作了下面這些事情:

  1. 用 HED 網絡檢測邊緣,能夠看到,這裏獲得的邊緣線仍是存在一些干擾的
  2. 在前一步獲得的圖像上,使用 HoughLinesP 函數檢測線段(藍色線段)
  3. 把前一步獲得的線段延長成直線(綠色直線)
  4. 在第二步中檢測到的線段,有一些是很接近的,或者有些短線段是能夠鏈接成一條更長的線段的,因此能夠採用一些策略把它們合併到一塊兒,這個時候,就要藉助第三步中獲得的直線。定義一種策略判斷兩條直線是否相等,當遇到相等的兩條直線時,把這兩條直線各自對應的線段再合併或鏈接成一條線段。這一步完成後,後面的步驟就只須要藍色的線段而不須要綠色的直線了
  5. 根據第四步獲得的線段,計算它們之間的交叉點,臨近的交叉點也能夠合併,同時,把每個交叉點和產生這個交叉點的線段也要關聯在一塊兒(每個藍色的點,都有一組紅色的線段和它關聯)
  6. 對於第五步獲得的全部交叉點,每次取出其中的 4 個,判斷這 4 個點組成的四邊形是不是一個合理的矩形(有透視變換效果的矩形),除了常規的判斷策略,好比角度、邊長的比值以外,還有一個判斷條件就是每條邊是否能夠和第五步中獲得的對應的點的關聯線段重合,若是不能重合,則這個四邊形就不太多是咱們指望檢測出來的矩形
  7. 通過第六步的過濾後,若是獲得了多個四邊形,能夠再使用一個簡單的過濾策略,好比排序找出周長或面積最大的矩形

對於上面這個例子,初版技術方案中檢測出來的邊緣線以下圖所示:

find rect 2

有興趣的讀者也能夠考慮一下,在這種邊緣圖中,如何設計算法才能找出咱們指望的那個矩形。

總結

算法角度

  • 神經網絡的參數/超參數的調優,一般只能基於經驗來設置,有 magic trick 的成分
  • 神經網絡/機器學習是一門試驗科學
  • 對於監督學習,數據的標註成本很高,這一步很容易出現瓶頸
  • 論文、參考代碼和本身的代碼,這三者之間不徹底一致也是正常現象
  • 對於某些需求,能夠在模型的準確度、大小和運行速度之間找一個平衡點

工程角度

  • end-to-end 網絡無效的時候,能夠用 pipeline 的思路考慮問題、拆分業務,針對性的使用神經網絡技術
  • 至少要熟練掌握一種神經網絡的開發框架,並且要追求代碼的工程質量
  • 要掌握神經網絡技術中的一些基本套路,觸類旁通
  • 要在學術界和工業界中間找平衡點,儘量多的學習一些不一樣問題領域的神經網絡模型,做爲技術儲備

參考文獻

Hacker's guide to Neural Networks
神經網絡淺講:從神經元到深度學習
分類與迴歸區別是什麼?
神經網絡架構演進史:全面回顧從LeNet5到ENet十餘種架構

數據的遊戲:冰與火
爲何「高大上」的算法工程師變成了數據民工?
Facebook人工智能負責人Yann LeCun談深度學習的侷限性

The best explanation of Convolutional Neural Networks on the Internet!
從入門到精通:卷積神經網絡初學者指南
Transposed Convolution, Fractionally Strided Convolution or Deconvolution
A technical report on convolution arithmetic in the context of deep learning

Visualizing what ConvNets learn
Visualizing Features from a Convolutional Neural Network

Neural networks: which cost function to use?
difference between tensorflow tf.nn.softmax and tf.nn.softmax_cross_entropy_with_logits
Why You Should Use Cross-Entropy Error Instead Of Classification Error Or Mean Squared Error For Neural Network Classifier Training

Tensorflow 3 Ways
TensorFlow-Slim
TensorFlow-Slim image classification library

Holistically-Nested Edge Detection
深度卷積神經網絡在目標檢測中的進展
全卷積網絡:從圖像級理解到像素級理解
圖像語義分割之FCN和CRF

Image Classification and Segmentation with Tensorflow and TF-Slim
Upsampling and Image Segmentation with Tensorflow and TF-Slim
Image Segmentation with Tensorflow using CNNs and Conditional Random Fields

How to Build a Kick-Ass Mobile Document Scanner in Just 5 Minutes
MAKE DOCUMENT SCANNER USING PYTHON AND OPENCV
Fast and Accurate Document Detection for Scanning


更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:

騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索