在上一篇文章中,我們介紹了KNN算法的原理,並詳細闡述了使用Opencv的KNN算法模塊對手寫數字圖像進行識別,發現識別的準確率還是比較高的,達到90%以上,這是因為手寫數字圖像的特徵比較簡單的緣故。本文我們將使用KNN來對更加複雜的CIFAR-10數據集進行識別分類,並嘗試提高分類的準確率。
1. CIFAR-10數據集介紹
CIFAR-10是一個專門用於測試圖像分類的公開數據集,其包含的彩色圖像分為10種類型:飛機、轎車、鳥、貓、鹿、狗、蛙、馬、船、貨車。且這10種類型圖像的標籤依次為0、1、2、3、4、5、6、7、8、9。該數據集分為Python、Matlab、C/C++三個不同的版本,顧名思義,三個版本分別適用於對應的三種程式語言。因為我們使用的是C/C++語言,所以使用對應的C/C++版本就好,該版本的數據集包含6個bin文件,如下圖所示,其中data_batch_1.bin~data_batch_5.bin通常用於訓練,而test_batch.bin則用於訓練之後的識別測試。
如下圖所示,每個bin文件包含10000*3073個字節數據,在每個3073數據塊中,第一個字節是0~9的標籤,後面3072位元組則是彩色圖像的三通道數據:紅通道 --> 綠通道 --> 藍通道 (1024 --> 1024 --> 1024)。其中每1024位元組的數據就是一幀單通道的32*32圖像,3幀32*32位元組的單通道圖像則組成了一幀彩色圖像。所以總體來說,每一個bin文件包含了10000幀32*32的彩色圖像。
我們編程把每一個bin文件中包含的圖像解析出來,並保存成圖像文件。比如對於data_batch_1.bin文件,新建文件夾batch1,然後在batch1文件夾下面再新建名為0~9的10個文件夾,分別保存標籤為0~9的圖像。
上解析代碼:
void read_cifar_bin(char *bin_path, char *save_path){ const int img_num = 10000; const int img_size = 3073; const int img_size_1 = 1024; const int data_size = img_num*img_size; const int row = 32; const int col = 32; uchar *cifar_data = (uchar *)malloc(data_size); if (cifar_data == NULL) { cout << "malloc failed" << endl; return; }
FILE *fp = fopen(bin_path, "rb"); if (fp == NULL) { cout << "fopen file failed" << endl; free(cifar_data); return; }
fread(cifar_data, 1, data_size, fp);
int cnt[10] = {0};
for (int i = 0; i < img_num; i++) { cout << i << endl; long int offset = i*img_size; long int offset0 = offset + 1; long int offset1 = offset0 + img_size_1; long int offset2 = offset1 + img_size_1; uchar label = cifar_data[offset]; Mat img(row, col, CV_8UC3);
for (int y = 0; y < row; y++) { for (int x = 0; x < col; x++) { int idx = y*col + x; img.at<Vec3b>(y, x) = Vec3b(cifar_data[offset2+idx], cifar_data[offset1+idx], cifar_data[offset0+idx]); } }
char str[100] = {0}; sprintf(str, "%s/%d/%d.tif", save_path, label, cnt[label]); imwrite(str, img); cnt[label]++; }
fclose(fp); free(cifar_data);}運行上述代碼分別解析數據集的6個bin文件,得到6*10000張圖像,這時我們有6個文件夾(對應6個bin文件):
以上每個文件夾下又包含了0~9的子文件夾,分別保存對應標籤的圖像:
2. CIFAR-10數據集的訓練與識別
把圖像從bin文件解析出來之後,就可以進行訓練和識別了,代碼與上篇文章類似:
void KNN_cifar_test(void){ char ad[128] = { 0 }; int testnum = 0, truenum = 0; const int K = 8; cv::Ptr<cv::ml::KNearest> knn = cv::ml::KNearest::create(); knn->setDefaultK(K); knn->setIsClassifier(true); knn->setAlgorithmType(cv::ml::KNearest::BRUTE_FORCE);
Mat traindata, trainlabel; const int trainnum = 900;
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch1/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
srcimage = srcimage.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch2/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
srcimage = srcimage.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch3/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
srcimage = srcimage.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch4/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
srcimage = srcimage.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch5/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
srcimage = srcimage.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
traindata.convertTo(traindata, CV_32F); knn->train(traindata, cv::ml::ROW_SAMPLE, trainlabel);
for (int i = 0; i < 10; i++) { for (int j = 0; j < 800; j++) { testnum++; sprintf_s(ad, "cifar/test_batch/%d/%d.tif", i, j); Mat testdata = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
testdata = testdata.reshape(1, 1); testdata.convertTo(testdata, CV_32F);
Mat result; int response = knn->findNearest(testdata, K, result); if (response == i) { truenum++; } } } cout << "測試總數" << testnum << endl; cout << "正確分類數" << truenum << endl; cout << "準確率:" << (float)truenum / testnum * 100 << "%" << endl;}運行上述代碼,首先加載訓練數據,然後對8000張測試圖像進行分類,得到的結果如下。可以看到僅2349張圖像識別成功了,29.3625%的識別準確率是非常低的了。
3. 使用HOG特徵提高識別準確率
我們識別熟人的時候,主要根據臉部的主要特徵來識別的,比如嘴巴、鼻子、眼睛、臉型,或者臉上的痣等,但如果我們根據臉頰的某一小塊毫無特點的皮膚來識別,則很可能認錯人。同樣的道理,計算機的圖像識別也是一樣的,如果能提取圖像的主要特徵來進行識別,則可以大大提高識別的準確率。常使用的圖像特徵有Shift特徵、Surf特徵、HOG特徵等。本文中,我們通過提取圖像的HOG特徵來進行識別。
首先介紹一下HOG特徵。HOG特徵算法的核心思想是提取圖像的梯度加權直方圖。下面分步介紹其提取過程:
(1) 計算圖像的梯度圖與梯度方向。
圖像的梯度分為x方向梯度和y方向梯度,對於圖像中任意點(x,y),像素值為I(x,y),其x方向梯度和y方向梯度可分別按下式計算:
得到x方向梯度和y方向梯度之後,即可得到該點的梯度與梯度方向(夾角):
(2) 統計梯度加權直方圖。
這裡面涉及到三個概念:檢測窗口、檢測塊、檢測單元,我們分別介紹一下這三個概念。
檢測窗口:在圖像中選擇的一個固定大小的矩形窗口,該窗口從左往右、從上往下滑動,每次滑動都有一個固定的步長。
檢測塊:在檢測窗口中選擇的一個固定大小的矩形窗口,該窗口從左往右、從上往下滑動,每次滑動都有一個固定的步長。
檢測單元:把檢測塊分成若干個小塊,每一個小塊就是一個檢測單元。比如上圖中,把檢測快分為2*2的檢測單元。
梯度加權直方圖的統計,則是以檢測單元為單位的,統計每個檢測單元中所有點的梯度值與梯度方向。梯度方向的範圍為0~359°,通常將其分成9段:0~39、40~79、80~119、120~159、160~199、200~239、240~279、280~319、320~359。根據每一個點的梯度方向,將其劃分到對應的段中,同時該段的特徵值加上該點的梯度值,比如檢測單元中某個點的梯度值為25,梯度方向為67°,則將其劃分到40~79段中,同時把40~79段的特徵值加上25,這就是加權梯度直方圖。
因此,每一個檢測單元有9個特徵值,檢測窗口與檢測塊滑動過程中的所有檢測單元的特徵值,就組成了圖像的特徵值。
Opencv中已經實現了HOG算法。下面我們使用Opencv中的HOG算法模塊來檢測圖像的HOG特徵,並對HOG特徵進行識別,上代碼:
void KNN_cifar_test_hog(void){ char ad[128] = { 0 }; int testnum = 0, truenum = 0; const int K = 8; cv::Ptr<cv::ml::KNearest> knn = cv::ml::KNearest::create(); knn->setDefaultK(K); knn->setIsClassifier(true); knn->setAlgorithmType(cv::ml::KNearest::BRUTE_FORCE);
Mat traindata, trainlabel; const int trainnum = 900; HOGDescriptor *hog = new HOGDescriptor(cvSize(16, 16), cvSize(8, 8), cvSize(8, 8), cvSize(2, 2), 9); vector<float> descriptors;
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch1/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE); hog->compute(srcimage, descriptors, Size(4, 4), Size(0, 0)); Mat hogg(descriptors); srcimage = hogg.reshape(1, 1);
traindata.push_back(srcimage); trainlabel.push_back(i); } }
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch2/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
hog->compute(srcimage, descriptors, Size(4, 4), Size(0, 0)); Mat hogg(descriptors);
srcimage = hogg.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch3/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
hog->compute(srcimage, descriptors, Size(4, 4), Size(0, 0)); Mat hogg(descriptors);
srcimage = hogg.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch4/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
hog->compute(srcimage, descriptors, Size(4, 4), Size(0, 0)); Mat hogg(descriptors);
srcimage = hogg.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
for (int i = 0; i < 10; i++) { for (int j = 0; j < trainnum; j++) { printf("i=%d, j=%d\n", i, j); sprintf_s(ad, "cifar/batch5/%d/%d.tif", i, j); Mat srcimage = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
hog->compute(srcimage, descriptors, Size(4, 4), Size(0, 0)); Mat hogg(descriptors);
srcimage = hogg.reshape(1, 1); traindata.push_back(srcimage); trainlabel.push_back(i); } }
traindata.convertTo(traindata, CV_32F); knn->train(traindata, cv::ml::ROW_SAMPLE, trainlabel);
for (int i = 0; i < 10; i++) { for (int j = 0; j < 800; j++) { testnum++; sprintf_s(ad, "cifar/test_batch/%d/%d.tif", i, j); Mat testdata = imread(ad, CV_LOAD_IMAGE_GRAYSCALE);
hog->compute(testdata, descriptors, Size(4, 4), Size(0, 0)); Mat hogg(descriptors);
testdata = hogg.reshape(1, 1); testdata.convertTo(testdata, CV_32F);
Mat result; int response = knn->findNearest(testdata, K, result); if (response == i) { truenum++; } } } cout << "測試總數" << testnum << endl; cout << "正確分類數" << truenum << endl; cout << "準確率:" << (float)truenum / testnum * 100 << "%" << endl;}運行上述代碼,得到結果如下。可以看到,識別的準確率從之前的29.3625%提升到了53.0625%,提升幅度還是很可觀的,不過準確率還是達不到理想的水平,這是KNN算法本身的局限導致的,KNN算法對複雜圖像的識別並不擅長,因此在接下來的文章中我們將嘗試一下別的圖像識別算法。
如果感興趣,麻煩您動動手指識別下方的二維碼,關注本公眾號,多謝!