Python盤記念幣系列之番外篇:YoloV3識別驗證碼

最近武夷山幣要開始準備預定了,公衆號後臺也有不少朋友向我諮詢和交流腳本。本着技術交流的想法,把以前的代碼從新梳理了一遍,發現識別驗證碼的成功率並無想象中那麼高。本着簡化流程和「殺雞就要用牛刀」的精神,這篇文章會經過 YoloV3來解決這個問題。

本期文章較長,比較熟悉的同窗能夠粗略看看。html

以前的作法

以前的作法是把每一個字符切割出來,而後用一個3層卷積加2層全鏈接的卷積神經網絡模型對字符分類。若是能精確切割,那效果天然沒的說,但實際操做下來發現要想對全部組合的驗證碼做完美的切割仍是有困難的(其實主要仍是笨,沒想到完美健壯的切割方法)。python

目前的切割方法常常會把字符切割成下面這種樣子:git

切割方法常常會把字符切割成下面這種樣子

這明顯是不對的,也致使最終驗證碼的錯誤。雖然頁面上驗證碼填寫錯誤會繼續識別刷新出的新驗證碼,但考慮到這是一個相似於搶購的使用環境,「一擊即中」的驗證碼識別方法是頗有必要的。github

爲何要用YoloV3

最近Yolo系列的目標檢測算法可謂是至關熱鬧。YoloV3提出後,Yolo系列算法好久都沒有比較大的進展,可近段時間YoloV4YoloV5卻如雨後春筍般冒了出來。先不說官方暫時還沒承認YoloV5Yolo命名,但單從算法性能來看,在必定程度上YoloV5已經算是現階段速度與精度並存的SoTA了。算法

新算法層出不窮,真是苦了算法工程師們了,很多同窗苦笑:學不動了!windows

但發牢騷歸發牢騷,新算法出來那是必須得好好擺弄一番的了。從全球最大的男性交友網站上下載YoloV5的代碼,固然同時也不忘給了一個大大的Star。我平時用Keras比較多,但這套代碼是基於Pytorch編寫的。考慮到對KerasYoloV3更加熟悉,此次暫時仍是先用YoloV3來作,因而又下載Keras版本的YoloV3代碼。須要下載地址的能夠在公衆號後臺發送Yolo獲取。微信

正好武夷山幣即將開始預定,又是一次調試代碼交流技術的好機會,下面就來看看YoloV3是怎麼解決驗證碼的識別問題的吧。網絡

數據集構建

準備數據集

在以前的文章中,咱們已經拿到了大量的驗證碼圖片,沒有的同窗能夠到下面這個地址獲取。app

https://github.com/TitusWongCN/WeChatSubscriptionArticlesAutoTokenAppointment/ABC/CaptchaHandler/captchas

(熟悉YoloV3數據集格式的同窗能夠直接跳到下一節)dom

準備打標軟件

YoloV3是一個目標檢測模型,訓練它須要的不僅是圖片自己,還要一塊兒提供要檢測目標的類別和位置,也就是常說的「標籤」。給數據集打標的工具備不少,好比LabelImgLabelme等,這裏使用第一種工具。LabelImg是一款專門用於爲目標檢測數據集生成標籤的開源軟件,用戶能夠很方便的爲目標「畫框貼標籤」。下面提供已經打包好的exe可執行文件,使用Windows 10系統的同窗能夠直接到下面的連接下載使用(其餘Windows系統沒有測試,能夠下載來本身測試):

https://github.com/tzutalin/labelImg/files/2638199/windows_v1.8.1.zip

下載好以後,先把data文件夾下的predefined_classes.txt文件內容改成實際的驗證碼的全部類別。要注意,每一行只能有一個類別名,類別名的順序沒有強制要求,好比:

A
B
C
...
0
2
5
...

這樣作會爲打標籤和後面準備開始訓練提供方便。

開始打標

打開labelImg.exe出現下面的界面:

LabelImg開啓界面

點擊左側菜單欄的Open Dir,在彈出的文件夾選擇框中選擇驗證碼圖片所在的文件夾,這時候軟件就會顯示找到的第一張圖片了。而後在驗證碼圖片文件夾同級目錄新建一個labels文件夾,接着點擊左側菜單欄的Change Save Dir選擇labels文件夾,這樣軟件會默認把生成的標記文件存儲在labels文件夾。

下面能夠正式爲圖片打標了。點擊左側菜單欄的Create\nRectBox,這時鼠標會變成一個十字叉,用鼠標在圖片上畫出一個包裹目標的最小的矩形框。鬆開鼠標的時刻會出來一個對話框要選擇這個框內目標的類別,前面已經配置過類別,在下面的框中選擇對應的類別而後確認便可。

LabelImg打標界面

上面就標記好了一個字符,每張驗證碼圖片都有四個字符,全部每張驗證碼圖片應該都有四個框。

查看標記文件

每一個文件打標後會生成對應的一個.xml格式的標記文件,文件內容以下(省略了部份內容):

<annotation>
    <folder>captchas</folder>
    <filename>1.jpg</filename>
    <path>ABC\CaptchaHandler\captchas\1.jpg</path>
    <source>
        <database>Unknown</database>
    </source>
    <size>
        <width>0</width>
        <height>0</height>
        <depth>3</depth>
    </size>
    <segmented>0</segmented>
    <object>
        <name>4</name>
        <pose>Unspecified</pose>
        <truncated>0</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>38</xmin>
            <ymin>10</ymin>
            <xmax>50</xmax>
            <ymax>26</ymax>
        </bndbox>
    </object>
    <object>
        ...
    </object>
    <object>
        ...
    </object>
    <object>
        ...
    </object>
</annotation>

生成的文件中有被標記文件的路徑和對應圖片中目標的位置以及類別等相關信息,這些信息下面都會用到。

生成Yolo特定格式

相對於其餘的目標檢測模型來講,Yolo系列模型須要的訓練數據的格式比較獨特。參考下載的YoloV3源碼的說明文件,能夠找到支持的數據集格式以下:

One row for one image;

Row format: image_file_path box1 box2 ... boxN;

Box format: x_min,y_min,x_max,y_max,class_id (no space).

這個解釋簡單明瞭,也就是說:

  1. 一張圖片佔一行
  2. 每一行的格式爲:圖片路徑 目標框1 目標框2 ... 目標框N
  3. 目標框的格式爲:X軸最小值,Y軸最小值,X軸最大值,Y軸最大值,類別ID

最後還給出了示例:

path/to/img1.jpg 50,100,150,200,0 30,50,200,120,3

path/to/img2.jpg 120,300,250,600,2

既然人家都說的這麼清楚了,那咱們須要作的就是依葫蘆畫瓢了。代碼以下:

import os
import xml.etree.ElementTree as ET # 用來讀取XML文件的包
import cv2

# 首先根據前面寫的predefined_classes.txt文件內容定義好類別的順序
labels = ['A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q',
          'R','S','T','U','V','W','X','Y','Z','2','3','4','5','6','7','8']
dirpath = r'./capchars/labels'  # 存放xml文件的目錄

for fp in os.listdir(dirpath):
    root = ET.parse(os.path.join(dirpath, fp)).getroot()
    path = root.find('path').text
    img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    height, width = img.shape
    boxes = [path, ]

    for child in root.findall('object'): # 找到圖片中的全部框
        label = child.find('name')
        label_index = labels.index(label.text) # 獲取類別名稱的ID
        sub = child.find('bndbox') # 找到框的標註值並進行讀取
        xmin = sub[0].text
        ymin = sub[1].text
        xmax = sub[2].text
        ymax = sub[3].text
        boxes.append(','.join([xmin, ymin, xmax, ymax, str(label_index)]))
    # 將數據寫入data.txt文件
    with open('./capchars/data.txt', 'a+') as f:
        f.write(' '.join(boxes) + '\n')

寫好的數據是這樣的:

F:\...\capchars\images\1.png 1,9,9,24,24 19,14,29,28,29 38,11,50,26,26 58,7,73,22,17
F:\...\images\10.png 1,8,15,23,7 18,11,29,26,26 39,15,49,29,30 58,14,73,27,17
F:\...\images\100.png 1,6,10,19,26 18,11,29,26,25 38,10,59,25,20 60,9,73,24,6
F:\...\images\101.png 1,8,14,24,21 18,8,33,23,1 38,13,55,28,9 57,12,69,26,8
F:\...\images\102.png 1,12,18,28,11 18,9,34,24,19 38,5,49,20,29 59,10,74,29,14
F:\...\images\103.png 1,10,18,25,20 18,5,29,19,30 38,7,54,22,12 59,7,74,25,14
F:\...\images\104.png 1,10,12,24,18 17,8,32,22,5 38,10,54,25,18 58,14,74,28,9
F:\...\images\105.png 1,13,9,27,8 18,9,34,24,22 37,12,50,27,8 58,11,74,29,14
F:\...\images\106.png 1,14,13,29,12 18,8,34,22,18 39,6,55,24,14 59,13,74,27,6
F:\...\images\107.png 1,8,13,22,2 17,9,32,24,4 38,8,53,23,4 58,11,73,26,11

到這裏,用於YoloV3訓練用的數據集就算是建好了。

模型訓練

修改模型參數配置

打開源代碼根目錄的train.py,須要把主函數_main的前幾行更改爲咱們對應的文件。這裏要注意,因爲要解決的是驗證碼檢測的問題,並非十分複雜,因此選擇使用Yolotiny版本,同時模型的輸入尺寸爲(416,416)。下面是更改後的代碼:

annotation_path = 'data/capchars/data.txt'
log_dir = 'logs/'
classes_path = 'model_data/cap_classes.txt' # 與前面的predefined_classes.txt文件內容同樣
anchors_path = 'model_data/tiny_yolo_anchors.txt'

YoloV3默認的輸入圖像通道數爲3,考慮到要解決的問題的難度,但通道足矣,爲了簡化模型,把train.pycreate_model方法和create_tiny_model方法中的第二行代碼都修改成:

image_input = Input(shape=(None, None, 1))

修改訓練數據讀取方式

查看源代碼中讀取數據的部分,發現yolo3/utils.py中的get_random_data方法在讀取數據的同時作了一些數據加強的操做。但在這個問題中是不太須要這些操做的,因此選擇刪除這些代碼。

【本文來自微信公衆號Titus的小宇宙,ID爲TitusCosmos,轉載請註明!】

【爲了防止網上各類爬蟲一通亂爬還故意刪除原做者信息,故在文章中間加入做者信息,還望各位讀者理解】

另外,前面說到輸入圖像的尺寸應該是(416,416),而驗證碼圖片的尺寸明顯不同,所以就要作一些更改尺寸的操做。更改以後的get_random_data方法是這樣的:

from PIL import Image
import numpy as np

def get_random_data(annotation_line, input_shape, max_boxes=4):
    line = annotation_line.split()
    image = Image.open(line[0])
    iw, ih = image.size
    h, w = input_shape # (416,416)
    boxes = np.array([np.array(list(map(int,box.split(',')))) for box in line[1:]])
    image_resize = image.resize((w, h), Image.BICUBIC)
    box_data = np.zeros((max_boxes,5))
    np.random.shuffle(boxes)
    x_scale, y_scale = float(w / iw), float(h / ih) # 計算驗證碼圖片在橫縱方向上縮放的倍數
    for index, box in enumerate(boxes):
        box[0] = int(box[0] * x_scale)
        box[1] = int(box[1] * y_scale)
        box[2] = int(box[2] * x_scale)
        box[3] = int(box[3] * y_scale)
        box_data[index, :] = box
    image_data = np.expand_dims(image_resize, axis=-1)
    image_data = np.array(image_data)/255.
    return image_data, box_data

模型訓練

一切準備工做就緒,就能夠運行train.py開始訓練了,訓練開始後控制檯會輸出以下信息:

...

Epoch 2/100

 1/16 [>.............................] - ETA: 6s - loss: 511.1613
 2/16 [==>...........................] - ETA: 6s - loss: 489.3632
 3/16 [====>.........................] - ETA: 5s - loss: 474.8986
 4/16 [======>.......................] - ETA: 5s - loss: 458.0243
 5/16 [========>.....................] - ETA: 4s - loss: 443.5792
 6/16 [==========>...................] - ETA: 4s - loss: 430.4511
 7/16 [============>.................] - ETA: 4s - loss: 416.0158
 8/16 [==============>...............] - ETA: 3s - loss: 402.7111
 9/16 [===============>..............] - ETA: 3s - loss: 390.4001
10/16 [=================>............] - ETA: 2s - loss: 378.4502
11/16 [===================>..........] - ETA: 2s - loss: 368.6907
12/16 [=====================>........] - ETA: 1s - loss: 359.1886
13/16 [=======================>......] - ETA: 1s - loss: 350.0055
14/16 [=========================>....] - ETA: 0s - loss: 342.0475
15/16 [===========================>..] - ETA: 0s - loss: 333.6687
16/16 [==============================] - 8s 482ms/step - loss: 325.5808 - val_loss: 211.4188

...

源代碼中默認是先用預訓練模型訓練50輪,此時只解凍最後兩層;而後解凍全部層再訓練50輪。咱們觀察lossval_loss再也不降低時,模型就訓練的差很少了,固然就直接等待程序運行結束通常也是能夠的。

程序運行結束會在logs文件夾下自動生成一個trained_weights_final.h5文件,這就是咱們須要的訓練好的模型文件。

模型測試

模型訓練完成,接下來固然就是激動人心的測試環節啦。

不過,心急吃不了熱豆腐,咱們還得先修改源代碼中的部分測試代碼才能開始對咱們的模型進行測試。

打開根目錄下的yolo.py,模型測試的時候會調用這個腳本生成一個Yolo類的對象,而後用這個對象來預測,因此須要在運行腳本以前先配置好。其實跟前面配置訓練腳本差很少,修改後的代碼以下:

_defaults = {
    "model_path": 'logs/trained_weights_final.h5',
    "anchors_path": 'model_data/tiny_yolo_anchors.txt',
    "classes_path": 'model_data/cap_classes.txt',
    ...
}

這時就能夠開始運行測試模型的腳本yolo_video.py來開始預測了,這時程序須要輸入要預測的圖片路徑。咱們輸入一個沒有訓練過的驗證碼圖片:

Input image filename:data/capchars/images/416.png

很快,結果就出來了:

預測結果

很明顯,結果是LFG8,與實際狀況一致。

再測試幾個:

預測結果

預測結果

總結

這個方法不須要對驗證碼圖片做不穩定的切割,避免了在切割過程當中致使的錯誤。所以,這個驗證碼的識別方法在成功率上是要高於以前的方法的。固然,這個方法還有能夠優化的地方。好比訓練前先根據系列前幾篇文章的內容把驗證碼圖片中的干擾線去掉,這樣準確率確定會更高。

至此,YoloV3識別驗證碼就講完了。

本系列的全部源代碼都會放在下面的github倉庫裏面,有須要能夠參考,有問題歡迎指正,歡迎交流,謝謝!

https://github.com/TitusWongCN/WeChatSubscriptionArticles

【Python盤記念幣系列】往期推薦:

第一期:Python盤記念幣系列之一:簡介

第二期:Python盤記念幣系列之二:識別驗證碼 01

第三期:Python盤記念幣系列之二:識別驗證碼 02

第四期:Python盤記念幣系列之二:識別驗證碼 03

第五期:Python盤記念幣系列之二:識別驗證碼 04

第六期:Python盤記念幣系列之三:自動預定腳本編寫 01

第七期:Python盤記念幣系列之三:自動預定腳本編寫 02

第八期:Python盤記念幣系列之三:自動預定腳本編寫 03 & 系列總結

下面是個人公衆號,有興趣能夠掃一下:

相關文章
相關標籤/搜索