Python識別字符型圖片驗證碼

前言

  驗證碼是目前互聯網上很是常見也是很是重要的一個事物,充當着不少系統的 防火牆 功能,可是隨時OCR技術的發展,驗證碼暴露出來的安全問題也愈來愈嚴峻。本文介紹了一套字符驗證碼識別的完整流程,對於驗證碼安全和OCR識別技術都有必定的借鑑意義。python

  GitHub上有大神總結的很是好的源碼及博客,連接以下git

GitHub:github

博客:http://blog.topspeedsnail.com/archives/10858算法

 

本文須要的依賴:安全

 

  • python3.5
  • PIL
  • libsvm

 

破解驗證碼的基本流程:app

  1. 準備原始圖片素材
  2. 圖片預處理
  3. 圖片字符切割
  4. 圖片尺寸歸一化
  5. 圖片字符標記
  6. 字符圖片特徵提取
  7. 生成特徵和標記對應的訓練數據集
  8. 訓練特徵標記數據生成識別模型
  9. 使用識別模型預測新的未知圖片集
  10. 達到根據「圖片」就能返回識別正確的字符集的目標

 

正文

第一步:準備素材ide

  驗證碼圖片以下:測試

  

真正的破解程序須要準備大量的素材,而後在進行大量的訓練後纔可達到相對高的識別度this

def downloads_pic(**kwargs):
    pic_name = kwargs.get('pic_name', None)

    url = 'http://xxxx/rand_code_captcha/'
    res = requests.get(url, stream=True)
    with open(pic_path + pic_name+'.bmp', 'wb') as f:
        for chunk in res.iter_content(chunk_size=1024):
            if chunk:  # filter out keep-alive new chunks
                f.write(chunk)
                f.flush()
        f.close()
#循環執行N次,便可保存N張驗證素材了。
抓取大量驗證碼素材的代碼

 

第二步:圖片預處理url

  1. 二值化圖片

#將RGB彩圖轉爲灰度圖,再按照設定閾值轉化爲二值圖
def get_bin_table(threshold=140):
    """
    獲取灰度轉二值的映射table
    """
    table = []
    for i in range(256):
        if i < threshold:
            table.append(0)
        else:
            table.append(1)

    return table


image = Image.open(img_path)
imgry = image.convert('L')  # 轉化爲灰度圖

table = get_bin_table()
out = imgry.point(table, '1')

由PIL轉化後變成二值圖片:0表示黑色,1表示白色。二值化後帶噪點的 6937 的像素點輸出後以下圖:

1111000111111000111111100001111100000011
1110111011110111011111011110111100110111
1001110011110111101011011010101101110111
1101111111110110101111110101111111101111
1101000111110111001111110011111111101111
1100111011111000001111111001011111011111
1101110001111111101011010110111111011111
1101111011111111101111011110111111011111
1101111011110111001111011110111111011100
1110000111111000011101100001110111011111
#若是你是近視眼,而後離屏幕遠一點,能夠隱約看到 6937 的骨架了。
View Code

 

  2. 去除噪點

  在轉化爲二值圖片後,就須要清除噪點。本文選擇的素材比較簡單,大部分噪點也是最簡單的那種 孤立點,因此能夠經過檢測這些孤立點就能移除大量的噪點。

  關於如何去除更復雜的噪點甚至干擾線和色塊,有比較成熟的算法: 洪水填充法 Flood Fill ,後面有興趣的時間能夠繼續研究一下。

  本文爲了問題簡單化,乾脆就用一種簡單的本身想的 簡單辦法 來解決掉這個問題:

    • 對某個 黑點 周邊的九宮格里面的黑色點計數
    • 若是黑色點少於2個則證實此點爲孤立點,而後獲得全部的孤立點
    • 對全部孤立點一次批量移除。

  下面將詳細介紹關於具體的算法原理。

  將全部的像素點以下圖分紅三大類

    • 頂點A
    • 非頂點的邊界點B
    • 內部點C

  種類點示意圖以下:

                         
 
  其中:
    • A類點計算周邊相鄰的3個點(如上圖紅框所示)  
    • B類點計算周邊相鄰的5個點(如上圖紅框所示)
    • C類點計算周邊相鄰的8個點(如上圖紅框所示)

  固然,因爲基準點在計算區域的方向不一樣,A類點和B類點還會有細分:

    • A類點繼續細分爲:左上,左下,右上,右下
    • B類點繼續細分爲:上,下,左,右
    • C類點不用細分

  而後這些細分點將成爲後續座標獲取的準則。

  主要算法的python實現以下:

def sum_9_region(img, x, y):
    """
    9鄰域框,以當前點爲中心的田字框,黑點個數
    :param x:
    :param y:
    :return:
    """
    # todo 判斷圖片的長寬度下限
    cur_pixel = img.getpixel((x, y))  # 當前像素點的值
    width = img.width
    height = img.height

    if cur_pixel == 1:  # 若是當前點爲白色區域,則不統計鄰域值
        return 0

    if y == 0:  # 第一行
        if x == 0:  # 左上頂點,4鄰域
            # 中心點旁邊3個點
            sum = cur_pixel \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y + 1))
            return 4 - sum
        elif x == width - 1:  # 右上頂點
            sum = cur_pixel \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y + 1))

            return 4 - sum
        else:  # 最上非頂點,6鄰域
            sum = img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y + 1)) \
                  + cur_pixel \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y + 1))
            return 6 - sum
    elif y == height - 1:  # 最下面一行
        if x == 0:  # 左下頂點
            # 中心點旁邊3個點
            sum = cur_pixel \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y - 1)) \
                  + img.getpixel((x, y - 1))
            return 4 - sum
        elif x == width - 1:  # 右下頂點
            sum = cur_pixel \
                  + img.getpixel((x, y - 1)) \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y - 1))

            return 4 - sum
        else:  # 最下非頂點,6鄰域
            sum = cur_pixel \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x, y - 1)) \
                  + img.getpixel((x - 1, y - 1)) \
                  + img.getpixel((x + 1, y - 1))
            return 6 - sum
    else:  # y不在邊界
        if x == 0:  # 左邊非頂點
            sum = img.getpixel((x, y - 1)) \
                  + cur_pixel \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x + 1, y - 1)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y + 1))

            return 6 - sum
        elif x == width - 1:  # 右邊非頂點
            # print('%s,%s' % (x, y))
            sum = img.getpixel((x, y - 1)) \
                  + cur_pixel \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x - 1, y - 1)) \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y + 1))

            return 6 - sum
        else:  # 具有9領域條件的
            sum = img.getpixel((x - 1, y - 1)) \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y + 1)) \
                  + img.getpixel((x, y - 1)) \
                  + cur_pixel \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x + 1, y - 1)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y + 1))
            return 9 - sum
View Code

 

  Tips:這個地方是至關考驗人的細心和耐心程度了,這個地方的工做量仍是蠻大的,花了半個晚上的時間才完成的。

  計算好每一個像素點的周邊像素黑點(注意:PIL轉化的圖片黑點的值爲0)個數後,只須要篩選出個數爲 1或者2 的點的座標即爲 孤立點 。這個判斷方法可能不太準確,可是基本上可以知足本文的需求了。

通過預處理後的圖片以下所示:

  

對比文章開頭的原始圖片,那些 孤立點 都被移除掉,相對比較 乾淨 的驗證碼圖片已經生成。

 

第三步:圖片字符切割——分割算法

  因爲字符型 驗證碼圖片 本質就能夠看着是由一系列的 單個字符圖片 拼接而成,爲了簡化研究對象,咱們也能夠將這些圖片分解到 原子級 ,即: 只包含單個字符的圖片

  因而,咱們的研究對象由 「N種字串的組合對象」 變成 「10種阿拉伯數字」 的處理,極大的簡化和減小了處理對象。  

 

  現實生活中的字符驗證碼的產生千奇百怪,有各類扭曲和變形。關於字符分割的算法,也沒有很通用的方式。這個算法也是須要開發人員仔細研究所要識別的字符圖片的特色來制定的。

  固然,本文所選的研究對象儘可能簡化了這個步驟的難度,下文將慢慢進行介紹。

  使用圖像編輯軟件(PhoneShop或者其它)打開驗證碼圖片,放大到像素級別,觀察其它一些參數特色:

    

能夠獲得以下參數:

  • 整個圖片尺寸是 40*10
  • 單個字符尺寸是 6*10
  • 左右字符和左右邊緣相距2個像素
  • 字符上下緊挨邊緣(即相距0個像素)

這樣就能夠很容易就定位到每一個字符在整個圖片中佔據的像素區域,而後就能夠進行分割了,具體代碼以下:

複製代碼
def get_crop_imgs(img):
    """
    按照圖片的特色,進行切割,這個要根據具體的驗證碼來進行工做. # 見原理圖
    :param img:
    :return:
    """
    child_img_list = []
    for i in range(4):
        x = 2 + i * (6 + 4)  # 見原理圖
        y = 0
        child_img = img.crop((x, y, x + 6, y + 10))
        child_img_list.append(child_img)

    return child_img_list
複製代碼

而後就能獲得被切割的 原子級 的圖片元素了:

 

  基於本部分的內容的討論,相信你們已經瞭解到了,若是驗證碼的干擾(扭曲,噪點,干擾色塊,干擾線……)作得不夠強的話,能夠獲得以下兩個結論:

  • 4位字符和40000位字符的驗證碼區別不大

  • 純數字 和  數字及字母組合 的驗證碼區別不大
    • 純數字。分類數爲10

    • 純字母
      • 不區分大小寫。分類數爲26
      • 區分大小寫。分類數爲52
    • 數字和區分大小寫的字母組合。分類數爲62

  在沒有造成 指數級或者幾何級 的難度增長,而只是 線性有限級 增長計算量時,意義不太大。

 

第四步:尺寸歸一

  本文所選擇的研究對象自己尺寸就是統一狀態:6*10的規格,因此此部分不須要額外處理。可是一些進行了扭曲和縮放的驗證碼,則此部分也會是一個圖像處理的難點。

 

第五步:模型訓練

  在前面的環節,已經完成了對單個圖片的處理和分割了。後面就開始進行 識別模型 的訓練了。

  整個訓練過程以下:

  1. 大量完成預處理並切割到原子級的圖片素材準備
  2. 對素材圖片進行人爲分類,即:打標籤
  3. 定義單張圖片的識別特徵
  4. 使用SVM訓練模型對打了標籤的特徵文件進行訓練,獲得模型文件

 

第六步:素材準備

  本文在訓練階段從新下載了同一模式的4數字的驗證圖片總計:3000張。而後對這3000張圖片進行處理和切割,獲得12000張原子級圖片。

  在這12000張圖片中刪除一些會影響訓練和識別的強幹擾的干擾素材,切割後的效果圖以下:

第七步:素材標記

  因爲本文使用的這種識別方法中,機器在最開始是不具有任何 數字的觀念的。因此須要人爲的對素材進行標識,告訴 機器什麼樣的圖片的內容是 1……。

  這個過程叫作 「標記」

  具體打標籤的方法是:

  1. 爲0~9每一個數字創建一個目錄,目錄名稱爲相應數字(至關於標籤)

  2. 人爲斷定 圖片內容,並將圖片拖到指定數字目錄中

  3. 每一個目錄中存放100張左右的素材
    通常狀況下,標記的素材越多,那麼訓練出的模型的分辨能力和預測能力越強。例如本文中,標記素材爲十多張的時候,對新的測試圖片識別率基本爲零,可是到達100張時,則能夠達到近乎100%的識別率
        

第八步:特徵選擇

  對於切割後的單個字符圖片,像素級放大圖以下:

  

  從宏觀上看,不一樣的數字圖片的本質就是將黑色按照必定規則填充在相應的像素點上,因此這些特徵都是最後圍繞像素點進行。

  字符圖片 寬6個像素,高10個像素 ,理論上能夠最簡單粗暴地能夠定義出60個特徵:60個像素點上面的像素值。可是顯然這樣高維度必然會形成過大的計算量,能夠適當的降維。

  1. 每行上黑色像素的個數,能夠獲得10個特徵
  2. 每列上黑色像素的個數,能夠獲得6個特徵

最後獲得16維的一組特徵,實現代碼以下:

複製代碼
def get_feature(img):
    """
    獲取指定圖片的特徵值,
    1. 按照每排的像素點,高度爲10,則有10個維度,而後爲6列,總共16個維度
    :param img_path:
    :return:一個維度爲10(高度)的列表
    """

    width, height = img.size

    pixel_cnt_list = []
    height = 10
    for y in range(height):
        pix_cnt_x = 0
        for x in range(width):
            if img.getpixel((x, y)) == 0:  # 黑色點
                pix_cnt_x += 1

        pixel_cnt_list.append(pix_cnt_x)

    for x in range(width):
        pix_cnt_y = 0
        for y in range(height):
            if img.getpixel((x, y)) == 0:  # 黑色點
                pix_cnt_y += 1

        pixel_cnt_list.append(pix_cnt_y)

    return pixel_cnt_list
複製代碼

而後就將圖片素材特徵化,按照 libSVM 指定的格式生成一組帶特徵值和標記值的向量文件。內容示例以下:

                

  說明以下:

  1. 第一列是標籤列,即此圖片人爲標記值,後續還有其它數值1~9的標記
  2. 後面是16組特徵值,冒號前面是索引號,後面是值
  3. 若是有1000張訓練圖片,那麼會產生1000行的記錄

  對此文件格式有興趣的同窗,能夠到 libSVM 官網搜索更多的資料。

 

第九步:模型訓練

  到這個階段後,因爲本文直接使用的是開源的 libSVM 方案,屬於應用了,因此此處內容就比較簡單的。只須要輸入特徵文件,而後輸出模型文件便可。

  能夠搜索到不少相關中文資料 。

主要代碼以下:

複製代碼
def train_svm_model():
    """
    訓練並生成model文件
    :return:
    """
    y, x = svm_read_problem(svm_root + '/train_pix_feature_xy.txt')
    model = svm_train(y, x)
    svm_save_model(model_path, model)
複製代碼

備註:生成的模型文件名稱爲 svm_model_file

 

第十步:模型測試

  訓練生成模型後,須要使用 訓練集 以外的全新的標記後的圖片做爲 測試集 來對模型進行測試。

  本文中的測試實驗以下:

  • 使用一組所有標記爲8的21張圖片來進行模型測試
  • 測試圖片生成帶標記的特徵文件名稱爲 last_test_pix_xy_new.txt

  在早期訓練集樣本只有每字符十幾張圖的時候,雖然對訓練集樣本有很好的區分度,可是對於新樣本測試集基本沒區分能力,識別基本是錯誤的。逐漸增長標記爲8的訓練集的樣本後狀況有了比較好的改觀:

  1. 到60張左右的時候,正確率大概80%
  2. 到185張的時候,正確率基本上達到100%

  以數字8的這種模型強化方法,繼續強化對數字0~9中的其它數字的模型訓練,最後能夠達到對全部的數字的圖片的識別率達到近乎 100%。在本文示例中基本上每一個數字的訓練集在100張左右時,就能夠達到100%的識別率了。

模型測試代碼以下

複製代碼
def svm_model_test():
    """
    使用測試集測試模型
    :return:
    """
    yt, xt = svm_read_problem(svm_root + '/last_test_pix_xy_new.txt')
    model = svm_load_model(model_path)
    p_label, p_acc, p_val = svm_predict(yt, xt, model)#p_label即爲識別的結果

    cnt = 0
    for item in p_label:
        print('%d' % item, end=',')
        cnt += 1
        if cnt % 8 == 0:
            print('')
複製代碼

至此,驗證的識別工做算是完滿結束。

相關文章
相關標籤/搜索