如何「優雅」地標數據

最近想做一個識別驗證碼的程序。目標其實很簡單,就是識別出某網站驗證碼的字母和數字。

驗證碼

這種類型的驗證碼已經被做爛了,相應的破解程序也很多。但我只是想學習消遣一下。

我已經通過爬蟲收集了某網站的大量驗證碼圖片,並通過圖像處理的方法把字母和數字分割出來(好在這類驗證碼比較簡單,切割工作相對容易)。之後,便是要對這些圖片進行標記並訓練。我總共爬了 20000 張,每張上面有四個數字或字母,相當於要對 80000 張圖片做標記分類。嗯,這很有趣!

需求分析

通過對原圖進行處理分割後,我已經得到如下的圖片數據(圖片尺寸 32 * 32,除了灰度圖,最好保留對應的原圖):

image set

現在,要將這些圖片分門別類。數字和字母,最多可以組合出 10 + 26 = 36 類,但仔細觀察數據後,我發現有很多數字和字母壓根沒出現。通過粗略地掃描一下數據,我統計出這個網站的驗證碼總共只使用了 23 類數字和字母。於是,我按照如下規則對圖片做了分類:

image_tag = {0: '3', 1: '5', 2: '6', 3: '7', 4: '8', 5: 'a', 6: 'c', 7: 'e', 8: 'f', 9: 'g', 10: 'h', 11: 'j', 12: 'k', 13: 'm', 14: 'n', 15: 'p', 16: 'r', 17: 's', 18: 't', 19: 'v', 20: 'w', 21: 'x', 22: 'y'}

將出現的數字和字母分爲 23 類。然後,接下來的目標,就是把圖片分到如下 23 個文件夾中:

tag folder

實現思路

很多人都覺得標數據這種事情很沒技術含量,純屬「dirty work」。如果你只是單純地用肉眼把一張張圖片分到這些目錄裏面,當然顯得很「笨拙」。而且,仔細想想,80000 張圖片的分類,(一個人)幾乎是不可能人工完成的。我們要用優雅的方法來歸類。

這個優雅的方法其實也很簡單。分以下幾步進行:

  1. 先人工挑出幾個或十幾個樣本,訓練一個分類器出來,這個分類器準確率會很低,但不要緊;
  2. 再從原圖片中,選出幾十上百張,用剛纔的分類器對它們進行分類。由於分類器精度有限,需要從分類後的結果中挑出分錯的樣本,然後人工將它們分到正確的目錄(這個工作比你自己去對上百張圖片做分類真的要輕鬆好多);
  3. 用已經分好類的數據繼續訓練一個新的分類器,重複第 2 步直到數據都分類完(隨着分類器精度提高,可以逐步增加待分類圖片的數量);

這個方法雖然還是需要不少人工輔助,但總體來說,比人工手動分類的效率實在高太多了。

具體實現

人工選取小樣本

要訓練分類器,挑選樣本是必須的,我從分割的圖片中,隨機挑出一兩百張,將它們分類到相應的目錄內:

目錄

然後,我需要一個函數來讀取這些文件夾的數據,方便之後繼續訓練。

'''讀取圖片數據文件,轉換成numpy格式,並保存'''
def maybe_pickle_data(all_image_folder, dest_folder, pickle_file, force=False):
    if os.path.exists(pickle_file) and force==False:
        print("data already pickled, pass")
        return

    image_folders = os.listdir(all_image_folder)
    train_image_data = []
    train_image_label = []

    for folder in image_folders:
        image_folder = os.path.join(all_image_folder, folder)
        if os.path.isdir(image_folder):
            print(image_folder)
            train_image_data.append(load_letter(image_folder))
            train_image_label.append(int(folder))

    # merge all the train data to ndarray
    train_dataset, train_label = merge_datasets(train_image_data, train_image_label)

    # randomize dataset and label
    train_dataset, train_label = randomize(train_dataset, train_label)

    # write to file
    with open(pickle_file, 'wb') as f:
        save = {
            'train_dataset': train_dataset,
            'train_labels': train_label,
        }
        pickle.dump(save, f, pickle.HIGHEST_PROTOCOL)

這個函數的主要工作是循環每一個目錄文件夾裏的文件,將它們依次讀入,變成矩陣形式方便處理,並通過 Pickle 保存成文件。

這裏主要用了其他幾個函數的功能:

  1. load_letter(image_folder)   # 讀取一個tag文件夾裏的推按文件,並返回所有圖片數據的矩陣
  2. merge_datasets(train_image_data, train_image_label)   # 將所有類別的圖片數據合併成一個大的矩陣樣本數據
  3. randomize(train_dataset, train_label)   # 打亂訓練數據

'''讀取同種類別的圖片轉換成numpy數組'''
def load_letter(folder):
    image_files = os.listdir(folder)
    # image_size 爲 32
    dataset = np.ndarray(shape=(len(image_files), image_size, image_size), dtype=np.float32)
    num_images = 0
    for image in image_files:
        image_file = os.path.join(folder, image)
        image_data = cv2.imread(image_file, cv2.IMREAD_GRAYSCALE)
        if image_data is None:
            continue
        if image_data.shape != (image_size, image_size):
            raise  Exception("%s  Unexpected image size: %s" % image_file, str(image_data.shape))
        dataset[num_images, :, :] = image_data
        num_images = num_images + 1

    dataset = dataset[0:num_images, :, :]

    return dataset

代碼比較簡單,就不多解釋了。

def merge_datasets(train_image_data, train_image_label):
    image_number = 0
    for image_datas in train_image_data:
        image_number = image_number + len(image_datas)
    #print(image_number)
    train_dataset, train_labels = make_array(image_number, image_size)

    image_number = 0

    # train_image_data 是所有圖片矩陣的list,list每個元素對應每個tag圖片的矩陣數據
    for label, image_datas in enumerate(train_image_data):
        for image_data in image_datas:
            train_dataset[image_number, :, :] = image_data
            train_labels[image_number] = train_image_label[label]
            image_number = image_number + 1
    #print(train_labels)
    return train_dataset, train_labels

訓練分類器

好了,準備好數據,我們需要訓練一個分類器。簡單起見,這裏選擇用 SVM,並選用 sklearn 函數庫。

其實,可以直接把圖片矩陣轉換成一個向量進行訓練(32 * 32 —> 1 * 1024),但我們擁有的數據量太少,這樣效果較差。所以,我們先提取圖片的 HOG 特徵再進行訓練:

bin_n = 16 # Number of bins

def hog(image):
    gx = cv2.Sobel(image, cv2.CV_32F, 1, 0)
    gy = cv2.Sobel(image, cv2.CV_32F, 0, 1)
    mag, ang = cv2.cartToPolar(gx, gy)
    bins = np.int32(bin_n*ang/(2*np.pi))    # quantizing binvalues in (0...16)
    bin_cells = bins[:16,:16], bins[16:,:16], bins[:16,16:], bins[16:,16:]
    mag_cells = mag[:16,:16], mag[16:,:16], mag[:16,16:], mag[16:,16:]
    hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
    hist = np.hstack(hists)     # hist is a 64 bit vector
    return hist

這個函數代碼摘自 opencv3 的文檔,想了解代碼,請自行去官網閱讀文檔。

有了特徵之後,我們可以正式用 SVM 進行訓練了:

def train_svm(train_datasets, train_labels):
    x = np.ndarray(shape=(len(train_datasets), 64))
    y = np.ndarray(shape=(len(train_datasets)), dtype=np.int32)

    for index, image in enumerate(train_datasets):
        hist = np.float32(hog(image)).reshape(-1, 64)
        x[index] = hist
        y[index] = train_labels[index]

    model = svm.LinearSVC(C=1.0, multi_class='ovr', max_iter=1000)
    model.fit(x, y)
    return model

這個函數代碼一樣很簡單,如果看不懂,證明你需要熟悉 numpysklearn 函數庫的用法。

然後,我們需要選取圖片進行預測分類。可以人工挑出個幾百上千張,放在一個預測目錄內。同時再開一個目錄文件夾如下:

這裏寫圖片描述

這個 test 文件夾和先前人工分類的文件夾要分開,因爲之後還要人工對這裏面的圖片除雜。最後,我們遍歷預測目錄內的圖片,用 SVM 做預測,並將圖片放到預測結果對應的文件夾裏。

測試函數代碼如下:

def test_image(image_folder, result_folder, model):
    image_files = os.listdir(image_folder)

    for image in image_files:
        image_file = os.path.join(image_folder, image)
        image_data = cv2.imread(image_file, cv2.IMREAD_GRAYSCALE)

        if image_data is None:
            continue

        hist = np.float32(hog(image_data)).reshape(-1, 64)
        pred = model.predict(hist)

        shutil.copy(image_file, os.path.join(result_folder+"/"+str(int(pred)), image))

做完這一步,我們最關鍵,同時也是最優雅的一步就完成了。之後,SVM 也幫不了你了。你需要依次打開每個文件夾,看看裏面的圖片有沒有分錯的,然後人工矯正它們,最後把它們歸類到我們一開始挑選樣本分好類的文件夾裏,後者這個文件夾的數據表示已經分類好的。

如果運氣好的,這個初步訓練好的 SVM 已經稍微有點「聰明」了。看看我得到的分類結果:

good result

這個準確率我已經很欣慰了,基本上人工挑出幾張分錯的,剩下的都是同一類了。

當然,肯定有分的不好的情況:

bad result

對於這種,就是發揮你眼力的時候了。基本上,之後所有的工作都是在這一堆類似的圖片裏面找不同。當然,你要相信這種情況會越來越少,因爲隨着訓練樣本逐漸增多,SVM 的訓練效果會越來越好。如果越到後面效果越差,程序員,請你不要懷疑,一定是你的代碼出問題了。

if __name__ == '__main__':
    maybe_create_directory_1(image_real_tag_folder)
    maybe_create_directory_1(image_test_folder)
    maybe_pickle_data(image_real_tag_folder, image_dataset_folder,
                      image_dataset_folder + "/data.pickle", force=True)

    f = open(image_dataset_folder + "/data.pickle", 'rb')
    data = pickle.load(f)
    train_datasets = data['train_dataset']
    train_labels = data['train_labels']

    model = train_svm(train_datasets, train_labels)

    print("remove data in " + image_src_folder)
    remove_files(image_src_folder)
    print("copy data to " + image_src_folder + "...")
    copy_src_to_test(original_image_folder, image_src_folder)

    test_image(image_src_folder, image_test_folder, model)

main 函數就是上面幾個函數的結合。之後,我們就是不斷地 run 一遍代碼,人工除雜精分類,再 run 一遍代碼,再人工……循環往復直到數據分類完爲止。

總結

這個方法可以節省你大量的體力活動,有助於提高逼格。雖然如此,這 80000 個樣本我還是生生花了一天半時間才分完,工作量還是稍微超出預期。如果有小夥伴有逼格更高,更能提高生產效率的方法,望不吝賜教!

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章