Steven5538

記一次 CAPATHA

Word count: 1.2kReading time: 5 min
2015/06/02 Share

先前已經寫過一篇 CAPATHA OCR 前置處理
不過對於不同的驗證碼會有不同的處理方式,這次的範例是以下這種形式。

第一步驟需要做的是去噪且將圖像做灰階。
如此才可以得到乾淨的圖像來進行辨識。去噪需要先了解雜訊的特性,以這張圖來說,其雜訊不是屬於斑點類型的,可以使用之前文章的作法或者 OpenCV 的 threshold 去做。

1
2
3
4
5
6
from PIL import Image, ImageEnhance
im = Image.open('pic.png')
enhancer = ImageEnhance.Contrast(im)
im = enhancer.enhance(3.0)
enhancer = ImageEnhance.Brightness(im)
im = enhancer.enhance(10.0)

或者

1
2
3
4
5
import cv2
im = cv2.imread('pic.png')
# 115 是 threshold,越高濾掉越多
# 255 是當你將 method 設為 THRESH_BINARY_INV 後,高於 threshold 要設定的顏色
retval, im = cv2.threshold(im, 115, 255, cv2.THRESH_BINARY_INV)

兩者得到的結果其實很類似,但若我們先將圖像做灰階,效果會更好。
這裡用第二種方法做灰階。

1
2
3
import cv2
im = cv2.imread('pic.png', flags=cv2.CV_LOAD_IMAGE_GRAYSCALE)
retval, im = cv2.threshold(im, 115, 255, cv2.THRESH_BINARY_INV)

在這階段我們很快的將背景色去除了,但剩下的雜點該怎麼辦呢?
我們可以對其一個點的周圍 2x2 的範圍進行掃描,並設定 threshold 若高於 threshold 則視為非雜點。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for i in xrange(len(im)):
for j in xrange(len(im[i])):
if im[i][j] == 255:
count = 0
for k in range(-2, 3):
for l in range(-2, 3):
try:
if im[i + k][j + l] == 255:
count += 1
except IndexError:
pass
# 這裡 threshold 設 4,當周遭小於 4 個點的話視為雜點
if count <= 4:
im[i][j] = 0

雜點去除後,我們必須對剩下的影像做強化,Opening 再度用上。

1
im = cv2.dilate(im, (2, 2), iterations=1)

影像更加清楚。

到這裡之後就可以進行 OCR 了,可惜由於這次的過於歪曲,無法套用 tesseract。
這時候有四個步驟:文字位置抓取、切字、轉正、辨識。

文字位置抓取、切字
文字位置的部分首先做影像的邊緣偵測,然後再尋找該影像的最左上角來將數字切割出來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contours, hierarchy = cv2.findContours(im.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted([(c, cv2.boundingRect(c)[0]) for c in contours], key=lambda x:x[1])

arr = []

for index, (c, _) in enumerate(cnts):
(x, y, w, h) = cv2.boundingRect(c)

try:
# 只將寬高大於 8 視為數字留存
if w > 8 and h > 8:
add = True
for i in range(0, len(arr)):
# 這邊是要防止如 0、9 等,可能會偵測出兩個點,當兩點過於接近需忽略
if abs(cnts[index][1] - arr[i][0]) <= 3:
add = False
break
if add:
arr.append((x, y, w, h))
except IndexError:
pass

可以看出切的效果相當不錯。

轉正
切出來之後我們要將原本歪曲的字轉成正的,如此才可以增加辨識率,作法就是左右旋轉 60 度並且計算若最左與最右之白點為最窄的情況,則視為正體。

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
28
29
30
31
32
33
for index, (x, y, w, h) in enumerate(arr):
roi = im[y: y + h, x: x + w]
thresh = roi.copy()

angle = 0
smallest = 999
row, col = thresh.shape

for ang in range(-60, 61):
M = cv2.getRotationMatrix2D((col / 2, row / 2), ang, 1)
t = cv2.warpAffine(thresh.copy(), M, (col, row))

r, c = t.shape
right = 0
left = 999

for i in xrange(r):
for j in xrange(c):
if t[i][j] == 255 and left > j:
left = j
if t[i][j] == 255 and right < j:
right = j

if abs(right - left) <= smallest:
smallest = abs(right - left)
angle = ang

M = cv2.getRotationMatrix2D((col / 2, row / 2), angle, 1)
thresh = cv2.warpAffine(thresh, M, (col, row))
# resize 成相同大小以利後續辨識
thresh = cv2.resize(thresh, (50, 50))

cv2.imwrite('tmp/' + str(index) + '.png', thresh)


幾乎都轉正了。

辨識
這部份我們用最簡單的方法,就是計算圖像誤差值,誤差值最小的就是該圖像所代表的數字。
所以我們必須重複上述步驟來建立資料集,資料集越大則越準確。
當資料集建立好後,我們便可以開始計算誤差值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def mse(im1, im2):
err = np.sum((im1.astype('float') - im2.astype('float')) ** 2)
err /= float(im1.shape[0] * im1.shape[0])

return err

arr = []
for tmp_png in [f for f in os.listdir('tmp') if not f.startswith('.')]:
min_a = 9999999999
min_png = None
pic = cv2.imread('tmp/' + tmp_png)

for directory in [f for f in os.listdir('templates') if not f.startswith('.')]:
for png in [f for f in os.listdir('templates/' + directory) if not f.startswith('.')]:
ref = cv2.imread('templates/' + directory + '/' + png)
if mse(ref, pic) < min_a:
min_a = mse(ref, pic)
min_png = directory

arr.append(min_png)

print ''.join(arr)


準確的辨識出來了。

我的資料集大約每個數字只有10筆,這樣的準確率約莫六成。
以這樣的處理方式來說,缺點便是若在一開始的去噪不足或過多的話,會造成切字時產生問題(切錯、切歪),進而導致後面翻轉失誤,辨識失敗。
若可以改善一開始的去噪使文字能更乾淨的話辨識率便可以上升。

CATALOG