最近想做一個識別驗證碼的程序乞榨。目標(biāo)其實很簡單熔酷,就是識別出某網(wǎng)站驗證碼的字母和數(shù)字孤紧。
這種類型的驗證碼已經(jīng)被做爛了,相應(yīng)的破解程序也很多拒秘。但我只是想學(xué)習(xí)消遣一下号显。
我已經(jīng)通過爬蟲收集了某網(wǎng)站的大量驗證碼圖片,并通過圖像處理的方法把字母和數(shù)字分割出來(好在這類驗證碼比較簡單躺酒,切割工作相對容易)押蚤。之后,便是要對這些圖片進(jìn)行標(biāo)記并訓(xùn)練羹应。我總共爬了 20000 張揽碘,每張上面有四個數(shù)字或字母,相當(dāng)于要對 80000 張圖片做標(biāo)記分類园匹。嗯雳刺,這很有趣!
需求分析
通過對原圖進(jìn)行處理分割后偎肃,我已經(jīng)得到如下的圖片數(shù)據(jù)(圖片尺寸 32 * 32煞烫,除了灰度圖,最好保留對應(yīng)的原圖):
現(xiàn)在累颂,要將這些圖片分門別類。數(shù)字和字母,最多可以組合出 10 + 26 = 36 類紊馏,但仔細(xì)觀察數(shù)據(jù)后料饥,我發(fā)現(xiàn)有很多數(shù)字和字母壓根沒出現(xiàn)。通過粗略地掃描一下數(shù)據(jù)朱监,我統(tǒng)計出這個網(wǎng)站的驗證碼總共只使用了 23 類數(shù)字和字母岸啡。于是,我按照如下規(guī)則對圖片做了分類:
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'}
將出現(xiàn)的數(shù)字和字母分為 23 類赫编。然后巡蘸,接下來的目標(biāo),就是把圖片分到如下 23 個文件夾中:
實現(xiàn)思路
很多人都覺得標(biāo)數(shù)據(jù)這種事情很沒技術(shù)含量擂送,純屬「dirty work」悦荒。如果你只是單純地用肉眼把一張張圖片分到這些目錄里面,當(dāng)然顯得很「笨拙」嘹吨。而且搬味,仔細(xì)想想,80000 張圖片的分類蟀拷,(一個人)幾乎是不可能人工完成的碰纬。我們要用優(yōu)雅的方法來歸類。
這個優(yōu)雅的方法其實也很簡單问芬。分以下幾步進(jìn)行:
- 先人工挑出幾個或十幾個樣本悦析,訓(xùn)練一個分類器出來,這個分類器準(zhǔn)確率會很低此衅,但不要緊强戴;
- 再從原圖片中,選出幾十上百張炕柔,用剛才的分類器對它們進(jìn)行分類酌泰。由于分類器精度有限,需要從分類后的結(jié)果中挑出分錯的樣本匕累,然后人工將它們分到正確的目錄(這個工作比你自己去對上百張圖片做分類真的要輕松好多)陵刹;
- 用已經(jīng)分好類的數(shù)據(jù)繼續(xù)訓(xùn)練一個新的分類器,重復(fù)第 2 步直到數(shù)據(jù)都分類完(隨著分類器精度提高欢嘿,可以逐步增加待分類圖片的數(shù)量)衰琐;
這個方法雖然還是需要不少人工輔助,但總體來說炼蹦,比人工手動分類的效率實在高太多了羡宙。
具體實現(xiàn)
人工選取小樣本
要訓(xùn)練分類器,挑選樣本是必須的掐隐,我從分割的圖片中狗热,隨機(jī)挑出一兩百張钞馁,將它們分類到相應(yīng)的目錄內(nèi):
然后,我需要一個函數(shù)來讀取這些文件夾的數(shù)據(jù)匿刮,方便之后繼續(xù)訓(xùn)練僧凰。
'''讀取圖片數(shù)據(jù)文件,轉(zhuǎn)換成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)
這個函數(shù)的主要工作是循環(huán)每一個目錄文件夾里的文件训措,將它們依次讀入,變成矩陣形式方便處理光羞,并通過 Pickle
保存成文件绩鸣。
這里主要用了其他幾個函數(shù)的功能:
load_letter(image_folder) # 讀取一個tag文件夾里的推按文件,并返回所有圖片數(shù)據(jù)的矩陣
merge_datasets(train_image_data, train_image_label) # 將所有類別的圖片數(shù)據(jù)合并成一個大的矩陣樣本數(shù)據(jù)
randomize(train_dataset, train_label) # 打亂訓(xùn)練數(shù)據(jù)
下面放點關(guān)鍵函數(shù)的代碼纱兑。
`load_letter()` 函數(shù)代碼如下呀闻,對圖片的讀取用了 `opencv`:
```python
'''讀取同種類別的圖片轉(zhuǎn)換成numpy數(shù)組'''
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
代碼比較簡單,就不多解釋了萍启。
merge_datasets()
函數(shù)代碼:
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每個元素對應(yīng)每個tag圖片的矩陣數(shù)據(jù)
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
訓(xùn)練分類器
好了,準(zhǔn)備好數(shù)據(jù)勘纯,我們需要訓(xùn)練一個分類器局服。簡單起見灾茁,這里選擇用 SVM官扣,并選用 sklearn 函數(shù)庫。
其實振定,可以直接把圖片矩陣轉(zhuǎn)換成一個向量進(jìn)行訓(xùn)練(32 * 32 —> 1 * 1024)堤结,但我們擁有的數(shù)據(jù)量太少唆迁,這樣效果較差。所以竞穷,我們先提取圖片的 HOG 特征再進(jìn)行訓(xùn)練:
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
這個函數(shù)代碼摘自 opencv3 的文檔唐责,想了解代碼,請自行去官網(wǎng)閱讀文檔瘾带。
有了特征之后鼠哥,我們可以正式用 SVM 進(jìn)行訓(xùn)練了:
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
這個函數(shù)代碼一樣很簡單,如果看不懂看政,證明你需要熟悉 numpy
和 sklearn
函數(shù)庫的用法朴恳。
然后,我們需要選取圖片進(jìn)行預(yù)測分類允蚣∮谟保可以人工挑出個幾百上千張,放在一個預(yù)測目錄內(nèi)嚷兔。同時再開一個目錄文件夾如下:
這個 test 文件夾和先前人工分類的文件夾要分開森渐,因為之后還要人工對這里面的圖片除雜做入。最后,我們遍歷預(yù)測目錄內(nèi)的圖片章母,用 SVM 做預(yù)測母蛛,并將圖片放到預(yù)測結(jié)果對應(yīng)的文件夾里翩剪。
測試函數(shù)代碼如下:
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))
做完這一步乳怎,我們最關(guān)鍵,同時也是最優(yōu)雅的一步就完成了前弯。之后蚪缀,SVM 也幫不了你了。你需要依次打開每個文件夾恕出,看看里面的圖片有沒有分錯的询枚,然后人工矯正它們,最后把它們歸類到我們一開始挑選樣本分好類的文件夾里浙巫,后者這個文件夾的數(shù)據(jù)表示已經(jīng)分類好的金蜀。
如果運氣好的,這個初步訓(xùn)練好的 SVM 已經(jīng)稍微有點「聰明」了的畴≡ǔ看看我得到的分類結(jié)果:
這個準(zhǔn)確率我已經(jīng)很欣慰了,基本上人工挑出幾張分錯的丧裁,剩下的都是同一類了护桦。
當(dāng)然,肯定有分的不好的情況:
對于這種煎娇,就是發(fā)揮你眼力的時候了二庵。基本上缓呛,之后所有的工作都是在這一堆類似的圖片里面找不同催享。當(dāng)然,你要相信這種情況會越來越少哟绊,因為隨著訓(xùn)練樣本逐漸增多因妙,SVM 的訓(xùn)練效果會越來越好。如果越到后面效果越差匿情,程序員兰迫,請你不要懷疑,一定是你的代碼出問題了炬称。
接下來我給出整個程序的主體部分:
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
函數(shù)就是上面幾個函數(shù)的結(jié)合汁果。之后,我們就是不斷地 run 一遍代碼玲躯,人工除雜精分類据德,再 run 一遍代碼鳄乏,再人工......循環(huán)往復(fù)直到數(shù)據(jù)分類完為止。
總結(jié)
這個方法可以節(jié)省你大量的體力活動棘利,有助于提高逼格橱野。雖然如此,這 80000 個樣本我還是生生花了一天半時間才分完善玫,工作量還是稍微超出預(yù)期水援。如果有小伙伴有逼格更高,更能提高生產(chǎn)效率的方法茅郎,望不吝賜教蜗元!