在上一節中,咱們使用了google的開源OCR庫來對字符進行識別,這一節以及下一節咱們將要使用機器學習算法來識別驗證碼。本節的代碼都在https://github.com/nladuo/captcha-break/tree/master/csdn能夠找到。php
在這一節中,將要對CSDN下載的驗證碼進行破解,就是在http://download.csdn.net/下載東西的時候,短期內下載次數過多彈出來的驗證碼。
python
作機器學習的第一個步驟就是採集數據,構建訓練樣本。首先,來看一下CSDN下載中出現的驗證碼。
git
在每次刷新的時候,會有以上這兩種驗證碼出現。在本節中,爲了方便學習K近鄰算法(簡稱爲:KNN),選擇第二種來進行破解,由於第二種的字母分割十分容易,每一個字母的位置都是固定的。
github
因爲兩種驗證碼的圖片大小不同,因此能夠使用圖片大小來判斷哪一個是第一種驗證碼,哪一個是第二種驗證碼,這裏使用python進行驗證碼下載。算法
# coding:utf-8 import requests import uuid from PIL import Image import os url = "http://download.csdn.net/index.php/rest/tools/validcode/source_ip_validate/10.5711163911089325" for i in range(100): resp = requests.get(url) filename = "./captchas/" + str(uuid.uuid4()) + ".png" with open(filename, 'wb') as f: for chunk in resp.iter_content(chunk_size=1024): if chunk: # filter out keep-alive new chunks f.write(chunk) f.flush() f.close() im = Image.open(filename) if im.size != (48, 20): os.remove(filename) else: print filename
下載事後,就須要對字母進行分割。機器學習雖然牛逼,可是也須要對樣本進行預處理,這裏的預處理就是把字母分割出來,而且分割成一樣的尺寸。分割的方式能夠使用代碼分割,固然也能夠經過人用PS等工具進行手動分割。
數組
我這裏使用代碼分割,字母分割的代碼在spliter文件夾下,我使用了boost庫來來讀取全部下載的驗證碼,對圖片進行二值化後,進行定點分割,能夠看到分割好的字母以下。
機器學習
以後,須要人工對字母進行分類,分類好的圖片見recognizer/dataset,我這裏每一個字母須要6個樣本,10個字母,總共60個樣本。
函數
K近鄰算法的定義十分簡單,在百度百科上有這樣的解釋:若是一個樣本在特徵空間中的k個最類似(即特徵空間中最鄰近)的樣本中的大多數屬於某一個類別,則該樣本也屬於這個類別。
工具
也就是說,須要找到要識別的字母在訓練樣本中K個最近的字母,而後找出這K個字母中最多的是某個類的?要識別的圖片也就是該類的。學習
首先,先定義一下距離如何計算,這裏能夠用各類數學上的距離,歐式距離、馬氏距離等等。。
因爲咱們的圖片已經進行了二值化,爲了簡便起見,這裏把兩張圖片的距離定義爲:兩張圖片灰度不一樣的像素點個數。也就是逐個比較圖片的相對位置上的灰度值,若是不相同,距離就加一。
int count_distance(Mat mat1, Mat mat2) { assert(mat1.size().height == mat2.size().height); assert(mat1.size().width == mat2.size().width); assert(mat1.channels() == 1 && mat2.channels() == 1); int distance = 0; for(int i = 0; i < mat1.size().width; i++){ for(int j = 0; j < mat1.size().height; j++){ if(mat1.at<uchar>(j, i) != mat2.at<uchar>(j, i)){//不相等就加1 distance++; } } } return distance; }
數據的加載須要一個圖片數組和一個標籤數組,來記錄圖片數組相應位置的類別。
加載樣本數據:
void load_dataset(Mat dataset[]) { string dataset_dir = "../recognizer/dataset/"; for(int i = 0; i < 6*10; i++){ char buffer[255]; sprintf(buffer, "%d", i/6); string image_path = dataset_dir + string(buffer); sprintf(buffer, "%d", i%6 + 1); image_path += string(buffer) + ".png"; dataset[i] = imread(image_path, CV_LOAD_IMAGE_GRAYSCALE); } }
加載樣本數據標籤:
void create_labels(int labels[]) { for(int i = 0; i < 6*10; i++){ labels[i] = i/6; } }
加載完數據後,就能夠開始實現KNN分類了。
一、計算輸入圖片和全部其餘圖片的距離
int distances[6*10]; int sorted_distances[6*10]; //count distances for(int i = 0; i < 6*10 ;i++){ distances[i] = count_distance(letter, dataset[i]); sorted_distances[i] = distances[i]; }
二、對距離進行排序
sort(sorted_distances, sorted_distances+6*10);
三、獲取K個距離最近的圖片的類別
int* k_nearest = new int[k]; for(int i = 0; i < k; i++){ for(int j = 0; j < 6*10 ; j++){ if(distances[j] == sorted_distances[i]){ k_nearest[i] = labels[j]; break; } } }
四、利用map記錄全部類別中出現k_nearest的次數
map<int, int> labels_map; for(int i = 0; i < k; i++){ if(labels_map.find(k_nearest[i]) == labels_map.end()) labels_map[k_nearest[i]] = 0; else labels_map[k_nearest[i]]++; }
五、獲得出現最多的類別
int max_label = -1; labels_map[max_label] = -1; map<int,int>::iterator it; for(it=labels_map.begin();it!=labels_map.end();++it){ if(it->second > labels_map[max_label]){ max_label = it->first; } } delete[] k_nearest; return max_label;
最後,咱們把驗證碼的4個字母分割出來,再進行K近鄰分類,就能夠獲得識別結果了。
void recognize(string path, Mat dataset[], int labels[]) { Mat test_image = imread(path, CV_LOAD_IMAGE_GRAYSCALE); threshold(test_image, test_image, 100, 255, cv::THRESH_BINARY); Range col_ranges[4] = { Range(5, 5+8), Range(14, 14+8), Range(23, 23+8), Range(32, 32+8) }; cout<<"Result:"; for(int i = 0; i < 4; i++){ Mat letter = test_image.colRange(col_ranges[i]); cout << knn_classify(letter, dataset, labels, 5); } cout<<endl; }
識別圖片:
識別結果:
經過以上,咱們破解了CSDN下載的第二種驗證碼,第一種驗證碼的識別過程也是能夠使用KNN的,可是第一種和第二種的分割字母的方式不一樣,讀者能夠嘗試使用opencv的findCountours函數對字母進行分割,或者使用垂直投影的方式進行分割,須要注意的是第一種驗證碼有一個黑色的邊框,若是不處理會影響findCountours函數的效果。