轉載:https://mp.weixin.qq.com/s?__biz=MzI5MjYzNzAyMw==&mid=2247484153&idx=1&sn=b65e9e99047ae20ed44cd99e4b0ff2e0&chksm=ec7f12c9db089bdf84281eaa54dad96679fa15b4c915d739597a57885625bc9a1fef15b8b52e&scene=21#wechat_redirect
目前NLP+OCR的落地應用在市場上愈加火熱,如金融領域的研報分析、司法領域的合同審覈甚至知識圖譜的信息抽取,無不顯示着NLP與OCR融合的巨大魅力。
本文將針對OCR的前序-“預處理”從理論和實戰兩方面進行詳細論述,當然,不會涉及過多的公式,網上對於公式解析已經很全面,若感興趣可自行查找。
本項目完整代碼:
https://github.com/Vincent131499/Chinese-OCR3/tree/master/preprocess
1.理論篇
光學字符識別(Optical Character Recognition,OCR)一般包括文本檢測(主要用於定位文本的位置)和文本識別(主要用於識別文本的具體內容)兩個步驟。而圖像質量的好壞對於檢測率與識別率的高低影響很大,不容忽視。下面將重點介紹圖像預處理中的二值化、去噪和傾斜角檢測校正的常用算法。
1.1 二值化方法
圖像二值化,Image Binarization,即通過將像素點的灰度值設爲0或255使得圖像呈現明顯的黑白效果。在傳統方法甚至是現在的流行方法中,高質量的二值化圖像仍然可以顯著提升OCR效果,一方面減少了數據維度,另一方面排除噪聲凸顯有效區域。目前,二值化方法主要分爲四種:
• 全局閾值方法
• 局部閾值方法
• 基於深度學習的方法
• 基於形態學和閾值的文檔圖像二值化方法
1.1.1 全局閾值方法
(1)固定閾值方法
該方法是對於輸入圖像中的所有像素點統一使用同一個固定閾值,類似於NLP中相似度計算的閾值選擇方法。其基本思想就是個分段函數:
公式中的T就是選擇的固定全局閾值。
在NLP領域的相似度計算中,不同領域的文本閾值不同,而在圖像領域也是一樣,固定閾值方法存在一個致命缺陷:很難爲不同的輸入圖像確定最佳閾值。因此提出了接下來的計算方法。
(2)Ostu方法
Ostu方法又被稱爲最大類間方差法,是一種自適應的閾值確定方法。
對於圖像I(x,y),前景(即目標)和背景的分割閾值記作T,屬於前景的像素點數佔整幅圖像的比例記爲ω0,其平均灰度μ0;背景像素點數佔整幅圖像的比例爲ω1,其平均灰度爲μ1。圖像的總平均灰度記爲U,類間方差記爲G。 假設圖像的背景較暗,並且圖像的大小爲M×N,圖像中像素的灰度值小於閾值T的像素個數記作N0,像素灰度大於閾值T的像素個數記作N1,則有:
將式(5)代入式(6),得到等價公式:
採用遍歷的方法得到使類間方差G最大的閾值T。
注:opencv可以直接調用這種算法,threshold(gray, dst, 0, 255, CV_THRESH_OTSU);
• 優點:算法簡單,當目標與背景的面積相差不大時,能夠有效地對圖像進行分割。
• 缺點:當圖像中的目標與背景的面積相差很大時,表現爲直方圖沒有明顯的雙峯,或者兩個峯的大小相差很大,分割效果不佳,或者目標與背景的灰度有較大的重疊時也不能準確的將目標與背景分開。
• 原因:該方法忽略了圖像的空間信息,同時將圖像的灰度分佈作爲分割圖像的依據,對噪聲也相當敏感。
1.1.2 局部閾值方法
(1)自適應閾值算法
自適應閾值算法用到了積分圖,是一個快速且有效地對網格的矩形子區域計算和的算法。積分圖中任意一點(x,y)的值是從圖左上角到該點形成的矩形區域內所有值之和。
(2)Niblack算法
Niblack算法同樣是根據窗口內的像素值來計算局部閾值的,不同之處在於它不僅考慮到區域內像素點的均值和方差,還考慮到用一個事先設定的修正係數k來決定影響程度。
(3)Sauvola算法
Sauvola是針對文檔二值化進行處理,在Niblack算法基礎上進一步改進。在處理光線不均勻或染色圖像時,比Niblack算法擁有更好的表現。
1.1.3 基於深度學習的方法
2017年提出了一種使用全卷積的二值化方法(Multi-Scale Fully Convolutional Neural Network),利用多尺度全卷積神經網絡對文檔圖像進行二值化,可以從訓練數據中學習並挖掘出像素點在空間上的聯繫,而不是依賴於在局部形狀上人工設置的偏置。
1.1.4 基於形態學和閾值的文檔圖像二值化方法
該方法大體分爲四步操作:
• 1)將RGB圖像轉化爲灰度圖;
• 2)圖像濾波處理;
• 3)數學形態學運算;
• 4)閾值計算。
其中,數學形態學運算包括腐蝕、膨脹、開運算和閉運算。
1.2 圖像去噪
在圖像的採集、量化或傳輸過程中會導致圖像噪聲的出現,這對圖像的後處理、分析會產生極大的影響。目前去噪方法分爲4種:
• 空間濾波
• 小波閾值去噪
• 非局部方法
• 基於神經網絡的方法
(1)空間濾波
空間濾波由一個鄰域和對該鄰域內像素執行的預定義操作組成,濾波器中心遍歷輸入圖像的每個像素點之後就得到了處理後的圖像。其中線性空間濾波器指的是在圖像像素上執行的是線性操作,非線性空間濾波器的執行操作則與之相反。
線性空間濾波器包括平滑線性濾波、高斯濾波器。
非線性空間濾波器包括中值濾波、雙邊濾波。
(2)小波閾值去噪
基本思路包括3個步驟:1)二維圖像的小波分解;2)對高頻係數進行閾值量化;3)二維小波重構。
(3)非局部方法
該類型方法包括NL-means和BM3D。其中BM3D是當前效果最好的算法之一,具體可參考this repo。
(4)基於神經網絡的方法
目前已經逐漸流行使用這種方法進行降噪處理,從簡單的MLP發展到LLNet。
1.3 傾斜角檢測校正
在掃描過程中,很容易出現文檔旋轉和位移的情況,因此後續的OCR處理與傾斜角檢測校正步驟密不可分。常見的方法有:霍夫變換、Randon變換以及基於PCA的方法。
針對霍夫變換的使用一般分爲3個步驟:
1)用霍夫變換探測出圖像中的所有直線;
2)計算出每條直線的傾斜角,求它們的平均值;
3)根據傾斜角旋轉矯正圖片。
2.實戰篇
接下來本文將針對圖像二值化、去噪、水平矯正三個模塊進行實戰演示。
2.1 圖像二值化
以Ostu算法爲例,展示實際效果,核心代碼如下:
#1.將圖像轉換爲灰度圖
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#2.對灰度圖使用ostu算法
ret1, th1 = cv2.threshold(gray_img, 0, 255, cv2.THRESH_OTSU)
輸出效果如下:
2.2 圖像去噪
以圖像處理領域廣爲人知的Lena圖片爲例,展示高斯濾波、NL-means非局部均值、小波閾值以及BM3D四種方法的效果。
核心代碼如下所示:
#1.讀取噪聲圖像
noisy_img = np.float32(imread('../img/lena_noise.bmp'))
noisy_img = cv2.cvtColor(noisy_img, cv2.COLOR_BGR2GRAY) / 255
#高斯濾波
img_Guassian = cv2.GaussianBlur(noisy_img, (5, 5), 0)
# NL-means(非局部均值)
def nlm(X, N, K, sigma):
pad_len = N + K
Xpad = np.pad(X, pad_len, 'constant', constant_values=0)
yy = np.zeros(X.shape)
B = np.zeros([H, W])
for ny in range(-N, N + 1):
for nx in range(-N, N + 1):
ssd = np.zeros((H, W))
# 根據鄰域內像素間相似性確定權重
for ky in range(-K, K + 1):
for kx in range(-K, K + 1):
ssd += np.square(
pad_len + ky:H + pad_len + ky,
pad_len + kx:W + pad_len + kx])
ex = np.exp(-ssd / (2 * sigma ** 2))
B += ex
yy += ex * Xpad[pad_len + ny:H + pad_len + ny, pad_len + nx:W + pad_len + nx]
return yy / B
img_nlm = nlm(noisy_img, 10, 4, 0.6)
# 小波閾值
def wavelet(X, levels, lmain):
def im2wv(img, nLev):
# pyr array
pyr = []
h_mat = np.array([[1, 1, 1, 1],
for i in range(nLev):
# split image up for HWT
a = img[:n:2, :n:2]
b = img[1:n:2, :n:2]
c = img[:n:2, 1:n:2]
d = img[1:n:2, 1:n:2]
vec = np.array([a, b, c, d])
# reshape vector to perform mat mult
D = 1 / 2 * np.dot(h_mat, vec.reshape(4, mid * mid))
img = L
pyr.append(L)
return pyr
def wv2im(pyr):
h_mat = np.array([[1, 1, 1, 1],
h_mat_inv = np.linalg.inv(h_mat)
L = pyr[-1]
for [H1, H2, H3] in reversed(pyr[:-1]):
vec = np.array([L, H1, H2, H3])
D = 2 * np.dot(h_mat_inv, vec.reshape(4, n * n))
img = np.empty((n2, n2))
L = img
return L
def denoise_coeff(y, lmbda):
x = np.copy(y)
return x
pyr = im2wv(X, levels)
for i in range(len(pyr) - 1):
for j in range(2):
im = wv2im(pyr)
return im
# BM3D算法
def run_bm3d(noisy_im, sigma,
k_H = 8 if (tau_2D_H == 'BIOR' or sigma < 40.) else 12
k_W = 8 if (tau_2D_W == 'BIOR' or sigma < 40.) else 12
noisy_im_p = symetrize(noisy_im, n_H)
img_basic = bm3d_1st_step(sigma, noisy_im_p, n_H, k_H, N_H, p_H, lambda3D_H, tauMatch_H, useSD_H, tau_2D_H)
img_basic = img_basic[n_H: -n_H, n_H: -n_H]
assert not np.any(np.isnan(img_basic))
img_basic_p = symetrize(img_basic, n_W)
noisy_im_p = symetrize(noisy_im, n_W)
img_denoised = bm3d_2nd_step(sigma, noisy_im_p, img_basic_p, n_W, k_W, N_W, p_W, tauMatch_W, useSD_W, tau_2D_W)
img_denoised = img_denoised[n_W: -n_W, n_W: -n_W]
return img_basic, img_denoised
輸出效果如下:
高斯濾波輸出
NL-means輸出
小波閾值輸出
BM3D輸出
從以上效果中可以看出,BM3D算法去噪效果最好。
2.3 水平矯正
演示以霍夫變換爲例的算法,核心代碼:
# 度數轉換
def DegreeTrans(theta):
res = theta / np.pi * 180
return res
# 逆時針旋轉圖像degree角度(原尺寸)
def rotateImage(src, degree):
# 旋轉中心爲圖像中心
# 計算二維旋轉的仿射變換矩陣
RotateMatrix = cv2.getRotationMatrix2D((w / 2.0, h / 2.0), degree, 1)
print(RotateMatrix)
# 仿射變換,背景色填充爲白色
rotate = cv2.warpAffine(src, RotateMatrix, (w, h), borderValue=(255, 255, 255))
return rotate
# 通過霍夫變換計算角度
def CalcDegree(srcImage):
midImage = cv2.cvtColor(srcImage, cv2.COLOR_BGR2GRAY)
dstImage = cv2.Canny(midImage, 50, 200, 3)
lineimage = srcImage.copy()
# 通過霍夫變換檢測直線
# 第4個參數就是閾值,閾值越大,檢測精度越高
lines = cv2.HoughLines(dstImage, 1, np.pi / 180, 200)
# 由於圖像不同,閾值不好設定,因爲閾值設定過高導致無法檢測直線,閾值過低直線太多,速度很慢
sum = 0
# 依次畫出每條線段
for i in range(len(lines)):
for rho, theta in lines[i]:
# print("theta:", theta, " rho:", rho)
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
x1 = int(round(x0 + 1000 * (-b)))
y1 = int(round(y0 + 1000 * a))
x2 = int(round(x0 - 1000 * (-b)))
y2 = int(round(y0 - 1000 * a))
# 只選角度最小的作爲旋轉角度
sum += theta
# 對所有角度求平均,這樣做旋轉效果會更好
average = sum / len(lines)
angle = DegreeTrans(average) - 90
return angle
輸出效果如下: