圖像識別——傳統的驗證碼識別
寫這篇目的是總結記錄一下工作過程中遇到的問題和解決方案,同時接受各位大神的指導。
目前,很多網站為了反爬都會採取各種各樣的策略,比較簡單粗暴的一種做法就是圖片驗證碼,隨著爬蟲技術與反爬技術的演變,目前驗證碼也越來越複雜,比較高端的如Google的I『m not a robot,極驗等等。這些新的反爬方式大多都基於用戶行為分析用戶點擊前的滑鼠軌跡來判斷是訪問者是程序還是人。
這篇文章介紹的是破解一般「傳統」的圖片驗證碼的步驟。上面提到的極驗(目前應用比較廣)也已經可以被破解,知乎上有相關的專欄,我早前無聊的時候跟著做用Java破解過,這裡就不重複了。
即便是傳統的圖片驗證碼,也是有難度區分的(第一張圖是我母校官網上的驗證碼,基本形同虛設;第二張圖則是某網站的會員登錄時的驗證碼增加了一些干擾信息,字元也有所粘連),但是破解的流程大致是一樣的。
圖1
圖2
識別步驟
一、獲取樣本
從目標網站獲取了5000個驗證碼圖片到本地,作為樣本。因為後期需要進行機器學習樣本量要足夠大。
二,樣本去噪
1,先二值化圖片
這一步是為了增強圖片的對比度,利於後期圖片圖像處理,代碼如下(沒怎麼寫過python如果有什麼低級錯誤請見諒):
# 二值化圖片 @staticmethod def two_value_img(img_path, threshold): img = Image.open(img_path).convert("L") # setup a converting table with constant threshold tables = [] for i in range(256): if i < threshold: tables.append(0) else: tables.append(1) # convert to binary image by the table bim = img.point(tables, "1") return bim
效果如下:
2,圖片去噪
該案例中就是去除兩條幹擾線,常規的去噪演算法有很多(洪水法等等),這裡根據圖片的特點採用了兩種去噪演算法,一種是自己根據圖片的特徵實現的演算法,另一種是「八值法」。去噪後的效果如下,可以看到去除了大部分的干擾線(剩下的根據字寬可以直接過濾掉),但是部分字元也變細了,所以這一步的去噪閥值需要不斷調整,在去噪的基礎上要盡量保持原圖的完整和可讀性。
代碼如下:
# 根據圖片特點,自己寫的降噪演算法def clean_img(img, threshold): width, height = img.size for j in range(height): for i in range(width): point = img.getpixel((i, j)) if point == 0: for x in range(threshold): if j + x >= height: break else: if point != img.getpixel((i, j + x)): img.putpixel((i, j), 1) break return img# 八值法降噪def clean_img_eight(img, threshold): width, height = img.size arr = [[0 for col in range(width)] for row in range(height)] arr = array(arr) for j in range(height): for i in range(width): point = img.getpixel((i, j)) if point == 0: sum = 0 for x in range(-1, 2): for y in range(-1, 2): if i + x > width - 1 or j + y > height - 1 or i + x < 0 or j + y < 0: sum += 1 else: sum += img.getpixel((i + x, j + y)) if sum >= threshold: arr[j, i] = 1 for i in range(len(arr)): for j in range(len(arr[i])): if arr[i, j] == 1: img.putpixel((j, i), 1) return img
效果如下:
三,圖片切割
圖片切割有很多演算法如投影法、CFS以及滴水法等。投影法適用於字元垂直方向上沒有粘連和重合的情況,CFS能夠很好的切割垂直方向有粘連但是沒有粘連的字元,水滴法可以分割粘連字元。目前採用的CFS切割法。切割效果如下圖,對於非粘連字元,效果很不錯。CFS聯通域切割的實現演算法主要用的是圖的廣度/深度遍歷,上代碼:
# CFS圖像切割 @staticmethod def cut_img(img, threshold, cut_width, cut_height, width_min, width_max, height_min): charters_imgs = [] width, height = img.size charters_pixels = [] visited_pixels = [] pixel_arr = ImgTools.get_pixel_arr(img) for i in range(width): for j in range(height): pixel = img.getpixel((i, j)) if pixel == 0 and [i, j] not in visited_pixels: charter_pixels = Node(i, j, pixel_arr, []).traversal() visited_pixels.extend(charter_pixels) if len(charter_pixels) > threshold: charters_pixels.append(charter_pixels) for i in range(len(charters_pixels)): x_min = 0 y_min = 0 x_max = 0 y_max = 0 # 這裡是為了處理沒有粘連但是垂直方向有重合的字元 width, height = img.size tmp_img = Image.new("1", (width * 2, height * 2), 255) for j in range(len(charters_pixels[i])): x, y = charters_pixels[i][j] tmp_img.putpixel((x, y), 0) if x > x_max: x_max = x else: if x < x_min or x_min == 0: x_min = x if y > y_max: y_max = y else: if y < y_min or y_min == 0: y_min = y if width_min < x_max - x_min < width_max and y_max - y_min > height_min: # charters_imgs.append(tmp_img.crop((x_min, y_min, x_max, y_max))) # 這裡是為了將所有的圖片切成一樣大,便於後期的特徵提取 charters_imgs.append(tmp_img.crop((x_min, y_min, x_min + cut_width, y_min + cut_height))) return charters_imgs
class Node: x = 0 y = 0 graph_arr = [] visited_neighbors = [] def __init__(self, x, y, graph_arr, visited_neighbors): self.x = x self.y = y self.graph_arr = graph_arr self.visited_neighbors = visited_neighbors def traversal(self): for i in range(-1, 2): for j in range(-1, 2): p = self.x + i q = self.y + j if (0 <= p < len(self.graph_arr)) and (0 <= q < len(self.graph_arr[0])): if array(self.graph_arr)[p, q] == 0 and [p, q] not in self.visited_neighbors: self.visited_neighbors.append([p, q]) next_node = Node(p, q, self.graph_arr, self.visited_neighbors) next_node.traversal() return self.visited_neighbors
效果如下:
四,提取feature並訓練特徵模型
1,提取feature
每個字元用了40個樣本(每個字元都切成了60×60)進行打標籤,如果效果不好後續可以增加樣本量(由於M大多數粘連嚴重,所以切出來的M很少,沒有達到40個,直接導致後面M的識別結果也很不好)。
2,訓練模型
這裡採用了libsvm來訓練模型,從個樣本中預留了1/10個作為訓練集,accuracy達到95%。
五,識別效果
先手動挑選了「乍一看」粘連不是很嚴重的30個樣本,進行訓練,結果如下,在80%左右。
六,總結和優化方向
1,目前整個識別流程已經走通,驗證碼識別服務也初具對外服務的能力;
2,雖然目前對於整體驗證碼的識別效果不是很好,但是,驗證碼服務拼的是識別率,比如說一個驗證碼需要識別,我在對其進行預處理和切割之後發現字元粘連效果不好,則完全可以拋棄,這並不影響識別率。換句話講我只是別切出來是四個字元的驗證碼即可(如果遇到一個網站每個圖片的粘連都比較嚴重,這條路就走不通了);
3,優化方向有兩個:(1)優化切割字元的演算法,目前的機器學習演算法在圖片切割比較好的情況下識別率是非常高的,因此目前這類驗證碼的切割是整個過程中的難點,對於該案例可以採用波斯平滑後通過垂直投影圖找到極值點作為水滴法的起點是一個思路;(2)增加樣本量,目前是40個識別率已經可以接受,如果增加到100識別率應該會有所提升。
推薦閱讀:
※有哪些關於 Python 的技術博客?
※有沒有相對比較成熟的python寫的類似jekyll的靜態頁面生成器,可以利用github pages搭建博客的?
※Python 初學者想通過 Django 框架寫一個博客,一個月內完成任務,大致的學習路線怎麼安排?
※Python爬蟲之微打賞爬蟲
※用 Kleene 遞歸定理構造輸出自己的 Python 程序