基於影像視覺詞彙的文字分類方法(完整專案)

大資料探勘DT資料分析發表於2018-03-05

640?wx_fmt=gif&wxfrom=5&wx_lazy=1

 向AI轉型的程式設計師都關注了這個號???


大資料探勘DT資料分析  公眾號: datadw


一年多以前我腦子一熱,想做一款移動應用:一款給學生朋友用的“錯題集”應用,可以將錯題拍照,記錄影像的同時,還能自動分類。比如拍個題目,應用會把它自動分類為"物理/力學/曲線運動"。當然,這個專案其實不靠譜,市場上已經有太多“搜題”類應用了。但過程很有趣,導致我過了一年多,清理磁碟垃圾時,還捨不得刪掉這個專案的“成果”。


本文程式碼及樣本資料在公眾號 datadw 裡 回覆 文字分類  即可獲取。


這個專案,核心要解決的問題就是文字分類。所以最初想到的方案是先 OCR 圖片轉文字,然後分詞,再計算 tf-idf,最後用 SVM 分類。但這個方案的問題是:開源 OCR 普遍需要自己訓練,且需要做大量的優化、調校和訓練,才能在中文識別上有不錯的效果,加上影像上還會有公式、幾何圖形,這些特徵也會決定分類,這又提高了對 OCR 的要求。所以我最終選擇的方案是,不使用 OCR,而是直接從影像中尋找有區分性的、魯棒的特徵,作為視覺詞彙。之後再通過傳統文字分類的方法,訓練分類器。

下面將展示整個訓練過程,訓練的樣本來自《2016 B版 5年高考3年模擬:高考理數》,並手工標註了14個分類,每個分類下約50個樣本,每個樣本為一個題目, 影像為手機拍攝。


640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1



本文中大部分演算法庫來自numpy、scipy、opencv、skimage、sklearn。



  1. 預處理

為了獲取到穩定的特徵,我們需要對影像進行預處理,包括調整影像大小,將影像縮放到合適尺寸;旋轉影像,或者說調整成水平;二值化,去除色彩資訊,產生黑白影像。



1.1. 調整影像大小

調整的目的是為了讓影像中文字的尺寸保持大致相同的畫素尺寸。這裡做了一個簡單假設,即:影像基本是一段完整的文字,比如一個段落,或者一頁文件,那麼不同的影像中,每行文字的字數相差不會很大。這樣我就可以從我所瞭解的、少得可憐的影像工具庫裡找到一個工具了:直線擬合。即通過擬合的直線(線段)長度與影像寬度的比例,調整影像的大小。下圖為兩張不同尺寸影像,經過多次擬合+調整大小後的結果,其中紅色演算法檢查到的直線(線段)。


640?wx_fmt=jpeg


下面是使用 opencv 直線擬合的程式碼:

# Canny演算法提取邊緣特徵, image是256灰度影像

image = cv2.Canny(image, 50, 200)
# 霍夫線變換提取直線

lines = cv2.HoughLinesP(image, 2, math.pi / 180.0, 40, numpy.array([]), 50, 10)[0]


1.2. 影像二值化

二值演算法選用skimage.filters.threshold_adaptive區域性自適應閥值的二值化), 試下來針對這種場景,這個演算法效果最好,其他演算法可以去scikit-image文件瞭解。下圖為全域性閥值和區域性自適應閥值的效果對比:


640?wx_fmt=jpeg

相關程式碼如下:

# 全域性自適應閥值binary_global = image > threshold_otsu(image)
binary_global = numpy.array(binary_global, 'uint8') * 255

binary_global = cv2.bitwise_not(binary_global)
#反轉黑白# 區域性自適應閥值

adaptive = threshold_adaptive(image, 41, offset=10) adaptive = numpy.array(adaptive, 'uint8') * 255

adaptive = cv2.bitwise_not(adaptive)
#反轉黑白

1.3. 旋轉影像

從第一步獲取到的直線,可以計算出影像的傾斜角度,針對只是輕微傾斜的影像,可以反向旋轉進行調整。由於可能存在干擾線條,所以這裡取所有直線傾斜角度的中值比平均值更合適。下圖展示了影像旋轉跳轉前後的效果:

640?wx_fmt=jpeg

相關程式碼如下:

# 先計算所有線條的角度angles = []for line in lines:
    x = (line[2] - line[0])
    y = (line[3] - line[1])
    xy = (x ** 2 + y ** 2) ** 0.5
    if 0 == xy:        continue
    sin = y / xy
    angle = numpy.arcsin(sin) * 360. / 2. / numpy.pi
    angles += [angle]    # 計算中值

angle = numpy.median(angles)
# 旋轉影像

image = ndimage.rotate(image, angle)

2. 提取特徵

這裡的思路是,首先通過形態學處理,可以分割出文字行(的影像),再從文字行中分割出詞彙(的影像),然後從"詞彙"中提取特徵。但這裡的需要克服的困難是:

  1. 很多漢字分左右部,容易被錯分,比如你好, 可能被分割成以4塊影像:

  2. 獨立的“字”並不適合於文字分類,還需能學習出詞彙。

針對以上問題的解決方案是:

  1. 將小的影像塊進行組合,組合後的新影像塊和原來的小塊影像一起作為原始影像的特徵,如你好將得到10個特徵:你女你好爾女爾好

  2. 得益於上面的方案,詞彙資訊也被保留了下來,所以第二個問題也就解決了,同時增加了演算法的魯棒性。


下面將介紹具體實現。

2.1. 提取文字行

由於預處理過程中已經將樣本的影像尺寸基本調整一致,所以可以比較容易的利用形態學的處理方法,分割出文字行。過程如下:

# cv2.Canny 可提取邊緣,並去除噪點# image為調整過大小,但沒有調整水平和二值化的影像

# 二值化後會影響 cv2.Canny 演算法效果,所以這裡用還沒有二值化的圖片

image = cv2.Canny(image, 100, 200)
# 二值化後調整水平image = ndimage.rotate(image, slope)
# 進行四次膨脹和腐蝕操作# 水平方向膨脹和腐蝕,聯通字與字之間的空間

# 垂直方向做較小的膨脹和腐蝕,填補行內的空隙

image = cv2.dilate(image, cv2.getStructuringElement(cv2.MORPH_RECT, (40, 3))) image = cv2.erode(image, cv2.getStructuringElement(cv2.MORPH_RECT, (40, 3))) image = cv2.erode(image, cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))) image = cv2.dilate(image, cv2.getStructuringElement(cv2.MORPH_RECT, (6, 5)))

下圖展示了每一步的變化:

640?wx_fmt=jpeg


接下來可以利用scipy庫中的measurements.label方法,標記出不同的的區域,下圖展示了標註後的效果,不同區域以不同的灰度表示。

640?wx_fmt=png

相關程式碼如下:

# image 為上一步形態學處理後的影像

image = 1 * (image > 64)
# 只保留灰度>64的區域,可以去除一些躁點

labeled, count = measurements.label(image)
# labeled為一個和影像尺寸一致的矩陣,矩陣中每個元素的值即這個畫素位置所屬的區域索引

# count為區域數量figure() gray() imshow(labeled) show()

接下來根據標記的區域,可從影像中裁剪出每行的資料,如下圖:

640?wx_fmt=png

相關程式碼如下:

def bounding_box(src):
    '''
    矩陣中非零元素的邊框
    '''
    B = numpy.argwhere(src)    if B.size == 0:        return [0, 0, 0, 0]
    (ystart, xstart), (ystop, xstop) = B.min(0), B.max(0) + 1
    return [xstart, ystart, xstop - xstart, ystop - ystart]    def clip_lines(image, labeled, count)
    lines = []        for i in range(1, count + 1):
        temp = image.copy()
        temp[labeled != i] = 0
        box = bounding_box(temp)
        x, y, w, h = box
        data = temp[y:y + h, x:x + w]
        lines.append(data)  
    return lines


2.2. 提取特徵(視覺詞彙)

裁剪出單行文字影像後,我們可以將影像中各列的畫素的值各自累加,得到一個一緯陣列,此陣列中的每個區域性最小值所在的位置,即為文字間的空隙。如下圖所示,其中藍色線為畫素值的累加值,綠色線為其通過高斯濾波平滑後的效果,紅色線為最終檢測到的分割點。

640?wx_fmt=png

詳細過程見下面程式碼:

# 1. 將影像中每一列的所有畫素的值累加orisum = image.sum(axis=0) / 255.0

# 2. 累加後的陣列通過高斯濾波器做平滑處理,減少干擾

filtered = filters.gaussian_filter(orisum, 8)
# 3. 找出拐點(上升轉下降、下降轉上升的點)trend = False
 # False 下降,True上升preval = 0  
# 上一個值points = []  # 拐點pos = 0for i in filtered:  
 if preval != i:      
 if trend != (i > preval):            trend = (i > preval)            points += [[pos if pos == 0 else pos - 1, preval, orisum[pos]]]    pos = pos + 1    preval = i # 4. 下降轉上升的拐點即為分割點  ... 程式碼略 ...

將單行的影像按上述方法獲取的分割點進行裁剪,裁剪出單個字元,然後再把相鄰的單個字元進行組合,得到最終的特徵資料。組合相鄰字元是為了使特徵中保留詞彙資訊,同時增加魯棒性。下圖為最終獲得的特徵資訊:

640?wx_fmt=png

本文中使用的所有樣本,最終能提取出約30萬個特徵。


2.3. 選擇特徵描述子

選擇合適的特徵描述子通常需要直覺+運氣+不停的嘗試(好吧我承認這裡沒有什麼經驗可分享),經過幾次嘗試,最終選中了HOG(方向梯度直方圖)描述子。HOG 最讓人熟悉的應用領域應該是行人檢測了,它很適合描述鋼性物體的邊緣特徵(方向),而印刷字型首先是剛性的,其次其關鍵資訊都包含在邊緣的方向上,所以理論上也適合用 HOG 描述。更多關於HOG的介紹請點選這裡

https://link.jianshu.com/?t=http://scikit-image.org/docs/dev/auto_examples/features_detection/plot_hog.html#sphx-glr-auto-examples-features-detection-plot-hog-py


下圖為文字影像及其 HOG 描述子的視覺化:

640?wx_fmt=png

程式碼如下:

# 提取邊緣canny = cv2.Canny(numpy.uint8(img), 50, 200)
# 計算描特徵描述子desc, hog_image = hog(    canny,    orientations=6,    pixels_per_cell=(4, 4),    cells_per_block=(2, 2),    visualise=True)

3. 訓練詞彙分類器

對詞彙進行人工標註工作量太大,所以最好能做到自動分類。我的做法是先聚類,再基於聚類的結果訓練分類器。但有個問題,主流的聚類演算法中,除了 K-Means 外,其他都不適合處理大量樣本(目前有30萬+樣本),但 K-Means 在這個場景上聚類效果不佳,高頻但不相關的詞彙容易被聚成一類,而 DBSCAN 效果很好,但樣本數一多,所需時間幾何級增長(在我的機器上,超過兩萬個樣本就需要耗費數個小時)。下圖來自sklearn 文件,對各聚類演算法做了比較:

640?wx_fmt=png

640?wx_fmt=png

2017/09/21 修改:原此處選擇的聚類方法(即先使用先用 K-Means 做較少的分類然後對每個分類單獨使用 DBSCAN 聚類並單獨訓練 SVC 分類器),準確率保持在70%左右,很難提高,故改用了下面描述的新方法。



本文來自 微信公眾號 datadw  【大資料探勘DT資料分析】


為解決這一問題,我的做法是:
1.  先對每類樣本下的詞彙用 DBSCAN 聚類(約1萬個詞彙樣本),得到一級分類。
2. 聚類後,計算每個一級分類的中心,然後以所有中心為樣本再用DBSCAN聚類,得到二級分類。完成後,原一級分類中心的新分類,即代表其原一級分類下所有元素的分類。

聚類的過程為,使用前面提取的 HOG 特徵,先 PCA 降緯,再 DBSCAN 聚類。這裡注意,計算二級分類時,PCA應使用全域性樣本計算。


分類器使用SGDClassifier,原因是其支援分批計算,不至於導致記憶體不足。

本文中使用的樣本,最終得到3000+詞彙型別。下圖為分類效果,其中每一行為一個分類:

640?wx_fmt=png

4. 訓練文字分類器

有了詞彙分類器,我們終於可以識別出每個文字樣本上所包含的詞彙了(事實上前面步驟的中間過程也能得到每個樣本的詞彙資訊),於是我們可以給每個樣本計算一個詞袋模型(即用每個詞出現的次數表示一篇文字),再通過池袋模型計算TF-IDF模型(即用每個詞的 TF*IDF 值表示一篇文字),並最終訓練 SVM 分類器。下面展示了此過程的主要程式碼:


640?wx_fmt=jpeg


Fitting the classifier to the training set done in 0.034s score : 0.918639053254Predicting on the test set done in 0.004s               precision    recall  f1-score   support    物理-電學-靜電場       1.00      0.67      0.80         3   物理-力學-互相作用       0.56      1.00      0.71         5  物理-機械振動和機械波       0.83      1.00      0.91         5   物理-電學-電磁感應       0.71      1.00      0.83         5   物理-電學-恆定電流       1.00      1.00      1.00         5   物理-力學-曲線運動       0.88      0.78      0.82         9 物理-機械能及其守恆定律       0.62      0.56      0.59         9        物理-光學       1.00      0.50      0.67        
2物理-力學-萬有引力與航天       1.00      0.75      0.86         4 物理-力學-牛頓運動定律       0.62      0.71      0.67         7   物理-電學-交變電流       1.00      1.00      1.00         1     物理-電學-磁場       1.00      0.25      0.40         4        物理-熱學       1.00      1.00      1.00        
2物理-力學-質點的直線運動       0.86      0.86      0.86         7  avg / total       0.81      0.78      0.77        68

[[2 0 0 0 0 0 1 0 0 0 0 0 0 0] [0 5 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 5 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 5 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 5 0 0 0 0 0 0 0 0 0] [0 1 0 0 0 7 0 0 0 0 0 0 0 1] [0 2 0 0 0 1 5 0 0 1 0 0 0 0] [0 0 0 0 0 0 0 1 0 1 0 0 0 0] [0 0 0 0 0 0 1 0 3 0 0 0 0 0] [0 1 0 0 0 0 1 0 0 5 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 1 0 0 0] [0 0 0 2 0 0 0 0 0 1 0 1 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 2 0] [0 0 1 0 0 0 0 0 0 0 0 0 0 6]]


測試集上正確率 81%,召回率 78%。個別分類正確率較低,可能是因為樣本數太少,另外訓練過程大多使用預設引數,若進行細緻調校,應該還有提高空間。

https://www.jianshu.com/p/f774e273a883


人工智慧大資料與深度學習

搜尋新增微信公眾號:weic2c

640?wx_fmt=png

長按圖片,識別二維碼,點關注



大資料探勘DT資料分析

搜尋新增微信公眾號:datadw


教你機器學習,教你資料探勘

640?wx_fmt=jpeg

長按圖片,識別二維碼,點關注



相關文章