基於OPENCV的手勢識別技術

Lingyoha發表於2020-12-11

基於OPENCV的手勢識別技術

 

前言:
  本篇部落格主要介紹基於OPENCV的手勢識別程式,程式碼為C++,OPENCV版本為OPENCV3會有較為詳細的實現流程和原始碼,並且做到原始碼儘量簡單,註釋也自認為較為清晰,希望能幫助到大家。(原始碼將放在文章末尾的連結中,程式碼較為粗糙,有錯誤歡迎大家指出。)

 
 
一、手勢識別流程圖
流程圖
  首先是對於流程圖的簡單說明:兩條線是分開進行的,兩者在比對分類之前是不會互相影響的(當然有部分函式,例如提取特徵的函式,是在兩條線中都是會使用到的),因此可以分別完成兩條線的工作。如果作為需要分工的專案,可以按照這兩部分進行分工,最後進行整合。
 
  而關於兩條線的順序,個人認為是優先進行神經網路的訓練部分(也就是下方線路),原因是對於要識別的手勢,首先要有對應的樣本,優先去尋找樣本,以此確定能夠識別的手勢。(當然,要是已經找到了樣本,先做上方線路也沒啥太大問題)
 
本篇文章的順序:
先講對於圖片的處理:
  因為先明白圖片如何處理,並知道提取特徵的方法,才能理解要把什麼東西放到神經網路裡,得到的資料又是什麼。
再講神經網路的搭建:
  神經網路的搭建其實就是一個模板性的東西,計算機並不知道你要識別的東西到底是什麼,是數字還是手勢,對於計算機來說它只是一堆資料,它只負責給你找到——你輸入的資料在網路裡跟哪個資料最為匹配,然後就給你輸出。
 
 
二、讀取圖片、獲取皮膚部分及二值化
 
1)讀取圖片部分:
  並沒有過多好說的,直接使用imread函式對圖片進行讀入。(注意,讀入的圖片應該是彩色的而不是灰度圖,否則無法進行後面的皮膚區域獲取)
 
2)獲取皮膚部分及二值化:
  關於皮膚部分的獲取,這裡列出幾種演算法。由於圖片的光照等的不同,不同演算法的優劣也很難對比,各位自行選擇演算法。
 
①基於RGB顏色空間的簡單閾值膚色識別:
 
  根據他人的研究,我們可以知道有這樣的一條判別式來用於膚色檢測
 
R>95 && G>40 && B>20 && R>G && R>B && Max(R,G,B)-Min(R,G,B)>15 && Abs(R-G)>15
 
  有了條判別式,我們就能夠很容易地寫出程式碼來實現膚色檢測。但該演算法對於光線的抗干擾能力較弱,光線稍微不好就識別不出皮膚點。

Mat getSkin(Mat& ImageIn)//獲取皮膚的區域,返回二值化影像
{
	vector<Mat> r_g_b;//用於存放RGB分量
	split(ImageIn,r_g_b);//分離RGB分量,順序為B,G,R

	Mat Binary = Mat::zeros(ImageIn.size(),CV_8UC1);

	Mat R = r_g_b[2];
	Mat G = r_g_b[1];
	Mat B = r_g_b[0];

	for (int i = 0; i < ImageIn.rows; i++)
	{
		for (int j = 0; j < ImageIn.cols; j++)
		{
			if (R.at<uchar>(i, j) > 95 && G.at<uchar>(i, j) > 40 && B.at<uchar>(i, j) > 20 &&
				R.at<uchar>(i, j) > G.at<uchar>(i, j) && R.at<uchar>(i, j) > B.at<uchar>(i, j) &&
				MyMax(R.at<uchar>(i, j), G.at<uchar>(i, j), B.at<uchar>(i, j)) - MyMin(R.at<uchar>(i, j), G.at<uchar>(i, j), B.at<uchar>(i, j)) > 15
				&& abs(R.at<uchar>(i, j) - G.at<uchar>(i, j)) > 15)
			{
				Binary.at<uchar>(i, j) = 255;
			}
		}
	}
	
	return Binary;
}

  程式碼說明:首先對彩色圖片分離開R、G、B分量,然後根據公式,將每一個點的R、G、B分量代入公式中進行判斷,符合條件的點我們可以認為它是皮膚中的一個點。(其中的MyMax和MyMin是自寫的判斷大小函式)將認為是皮膚的點的值置為255,就可以達到二值化的效果。

二值化後的結果
  可以看到除了部分因為光線問題導致的陰影沒有被識別出來以外,幾乎所有皮膚點都被識別出來了,效果還過得去。
 
 
②基於橢圓皮膚模型的皮膚檢測
 
  研究發現,將皮膚對映到YCrCb空間,則在YCrCb空間中皮膚的畫素點近似成一個橢圓的分佈。因此如果我們得到了一個CrCb的橢圓,對於一個點的座標(Cr, Cb),我們只需判斷它是否在橢圓內(包括邊界)就可以得知它是不是膚色點。
  該演算法對於光線的敏感性沒有這麼高,基本上該檢測到的皮膚都能夠檢測到,抗干擾能力相對較強。(原因大概是YCrCb中Y分量表示明亮度,而而“Cr”和“Cb” 表示的則是色度,作用是描述影像色彩及飽和度)

Mat getSkin2(Mat& ImageIn)
{
	Mat Image = ImageIn.clone();//複製輸入的圖片

	//利用OPENCV自帶的ellipse函式生成一個橢圓的模型
	Mat skinCrCbHist = Mat::zeros(Size(256, 256), CV_8UC1);
	ellipse(skinCrCbHist, Point(113, 155.6), Size(23.4, 15.2), 43.0, 0.0, 360.0, Scalar(255, 255, 255), -1);

	Mat ycrcb_Image;
	cvtColor(Image, ycrcb_Image, COLOR_BGR2YCrCb);//用cvtColor函式將圖片轉換為YCrCb色彩空間的圖片

	Mat Binary = Mat::zeros(Image.size(), CV_8UC1);//輸出的二值化圖片
	vector<Mat>y_cr_cb;//用於存放分離開的YCrCb分量
	split(ycrcb_Image, y_cr_cb);//分離YCrCb分量,順序是Y、Cr、Cb

	Mat CR = y_cr_cb[1];
	Mat CB = y_cr_cb[2];

	for (int i = 0; i < Image.rows; i++)
	{
		for (int j = 0; j < Image.cols; j++)
		{
			if (skinCrCbHist.at<uchar>(CR.at<uchar>(i,j), CB.at<uchar>(i,j)) > 0)//在橢圓內的點置為255
			{
				Binary.at<uchar>(i, j) = 255;
			}
		}
	}
	return Binary;
}

  程式碼說明:首先用OPENCV自帶的函式生成一個橢圓的模型,然後將RGB圖片轉換為YCrCb圖片,分離開Y,Cr,Cb,對於原圖片裡的每一個點,判斷其是否在橢圓內,在,則認為該點是一個皮膚點。
二值化後的結果2
  可以看到幾乎所有的皮膚部分都被識別出來了,效果比上一個演算法看上去要好不少。
 
 
③基於YCrCb顏色空間Cr,Cb範圍篩選法
 
  這種方法與第一種方法在原理上是一樣的,只不過這次將顏色空間變為了YCrCb空間。同樣的,我們有公式
 
Cr>133 && Cr<173 && Cb>77 && Cb<127
 
  將CrCb分量代入這條公式進行判斷,就能得到皮膚點,此處不進行過多的講述。

Mat getSkin3(Mat& ImageIn)
{
	Mat Image = ImageIn.clone();
	Mat ycrcb_Image;
	cvtColor(Image, ycrcb_Image,COLOR_BGR2YCrCb);
	vector<Mat>y_cr_cb;
	split(ycrcb_Image, y_cr_cb);
	Mat CR = y_cr_cb[1];
	Mat CB = y_cr_cb[2];
	Mat ImageOut = Mat::zeros(Image.size(), CV_8UC1);

	for (int i = 0; i < Image.rows; i++)
	{
		for (int j = 0; j < Image.cols; j++)
		{
			if (CR.at<uchar>(i, j) > 133 && CR.at<uchar>(i, j) < 173 && CB.at<uchar>(i, j) > 77 && CB.at<uchar>(i, j) < 127)
			{
				ImageOut.at<uchar>(i, j) = 255;
			}
		}
	}

	return ImageOut;
}

二值化後的結果3
  我們可以看到效果也是挺不錯的(相比第一種方法得到的),與第二種方法不分伯仲。
 
 
④YCrCb顏色空間Cr分量+Otsu法閾值分割
 
  首先我們要知道YCrCb色彩空間是什麼:YCrCb即YUV,其中“Y”表示明亮度(Luminance或Luma),也就是灰階值;而“U”和“V” 表示的則是色度(Chrominance或Chroma),作用是描述影像色彩及飽和度,用於指定畫素的顏色。其中,Cr反映了RGB輸入訊號紅色部分與RGB訊號亮度值之間的差異。(來自百度百科)
 
所以,該方法的原理也十分簡單:
 
1、將RGB影像轉換到YCrCb顏色空間,提取Cr分量影像

2、對Cr做自二值化閾值分割處理(Otsu法)

Mat getSkin4(Mat& ImageIn)
{
	Mat Image = ImageIn.clone();
	Mat ycrcb_Image;
	cvtColor(Image, ycrcb_Image, COLOR_BGR2YCrCb);//轉換色彩空間

	vector<Mat>y_cr_cb;
	split(ycrcb_Image, y_cr_cb);//分離YCrCb

	Mat CR = y_cr_cb[1];//圖片的CR分量
	Mat CR1;

	Mat Binary = Mat::zeros(Image.size(), CV_8UC1);
	GaussianBlur(CR, CR1, Size(3, 3), 0, 0);//對CR分量進行高斯濾波,得到CR1(注意這裡一定要新建一張圖片存放結果)
	threshold(CR1, Binary, 0, 255, THRESH_OTSU);//用系統自帶的threshold函式,對CR分量進行二值化,演算法為自適應閾值的OTSU演算法

	return Binary;
}

  程式碼說明:前面的轉換色彩空間不再贅述,關鍵點在於系統的二值化函式threshold,使用了OTSU的演算法對影像前景和背景進行區分。(請不清楚threshold函式使用和OTSU演算法的讀者自行查詢資料,這裡由於篇幅問題不展開解釋)
二值化後的結果4
可以看到效果也不錯,基本也能識別出來。
 
 
⑤OPENCV自帶的膚色檢測類AdaptiveSkinDetector
(但是我沒用過,僅是放上來讓大家知道…)
 
 
總結:
  對於以上幾種方法,很難說到底哪一種會更加準確,根據環境不同,圖片不同,演算法之間的優劣也有差別。因此有條件的可以每一種都嘗試,看看在自己的環境下哪種演算法的效果最好。我在程式碼中使用的是方法②,也僅供參考。
 
 
三、獲取輪廓及特徵(特徵為傅立葉描繪子)
 
原理:(請務必看看)
 
1)什麼是影像的特徵?
  或許換個問題你就理解了,什麼是人的特徵?我們或許可以認為,會直立行走,能製造和使用工具,這些就是人的特徵。然後,進一步,什麼是手勢0的特徵?是一根手指都沒有伸出來,這就是手勢0的特徵。手勢1呢?只伸出了一根手指對吧?這就是特徵。
 
2)為什麼要獲取影像特徵?
  我們舉個比較簡單(但是並不真實)的例子:你輸入了一張二值化後的圖片(假設是手勢2,即我們前面的圖片裡的手勢),我們通過一個獲取特徵的函式,獲得了這個手勢的特徵值,假設這一連串的值是[1,2,3,4,5,6]。
  而在神經網路裡,手勢0的特徵值被認為是[4,4,3,3,2,2],手勢1被認為是[9,8,7,6,5,4,],手勢2被認為是[1,2,3,4,5,5]。那麼你輸入了[1,2,3,4,5,6],你認為最匹配的是哪個手勢呢?顯然就是手勢2。
  而這就是神經網路的工作原理。你輸入一串代表特徵的數值,預測函式會在神經網路裡去找跟這串數值最相似的那個結果,把它告訴你:這就是我判斷的手勢結果。
  而影像的特徵,就是上面我們所說的[1,2,3,4,5,6]這串數字。我們要想辦法獲取這個特徵向量,扔到神經網路裡去讓它識別。至於神經網路怎麼知道[1,2,3,4,5,5]代表手勢2,[4,4,3,3,2,2]代表手勢0,我們在後面的神經網路部分會說到。
 
3)要獲取什麼特徵,以及怎麼獲取?
  首先,對於手勢來說,我們獲取的特徵應該是對旋轉和縮放都不敏感的。為什麼呢?你總不希望你的手旋轉了45°系統就識別不出來了吧?你總不希望你離得遠一點系統就識別不出來了吧?所以,我們要找到一個方法來描述你的手勢,並且這個方法對於旋轉和縮放都不那麼敏感。
  而在本專案中,我們使用的是傅立葉描繪子,這是一種邊界描繪子(就是專門用來描繪邊界的),它對於旋轉以及縮放並不十分敏感。這裡簡單說一下傅立葉描繪子的原理。對於座標系內的一個點,通常我們用(X,Y)來表示。但這就是兩個數值了,有沒有什麼辦法用一個數值來表示?幸好我們有複數這個東西!對於點(X,Y)我們可以寫成X+iY,這不就變成一個數了嗎?這能將一個二維的問題轉變為一維的問題。
  而我們應該知道,任何一個周期函式都可以展開為傅立葉級數,所以我們可以通過傅立葉級數來描繪一個週期變換的函式。那麼你可能會問,這個二值化後的手勢圖,跟週期變換函式有什麼關係啊?還真有!沿邊界曲線上一個動點s[k] = [x(k),y(k)]就是一個以形狀邊界周長為週期的函式。
  為什麼外輪廓是周期函式?假設有一個點在外輪廓上向著一個方向移動,轉了一圈,兩圈…,那這是不是周期函式?所以問題就解決了,我們可以用傅立葉描繪子來描繪手勢的外輪廓,從而描繪手勢。
  關於傅立葉描繪子更詳細的介紹以及推導建議各位去查閱相關資料,這裡不打算過多說(畢竟估計也沒幾個人感興趣),我這裡直接給出計算公式:
傅立葉描繪子
  當然,這個公式可以使用尤拉公式進行展開,對於程式設計實現來說會更加簡單。但是僅僅是這個公式是不夠的,因為這樣計算出來的傅立葉描繪子與形狀尺度,方向和曲線起始點S0都有關係,顯然不是我們想要的。所以,我們還要以a(1)為基準,進行歸一化,最終得到:
歸一化後傅立葉描繪子
  由於形狀的能量大多集中在低頻部分,高頻部分一般很小且容易受到干擾,所以我們只取前14位就足夠了。
 
 
具體實現:
 
  首先,要描繪手勢的外輪廓,我們就需要先得到手勢的外輪廓。幸好,OPENCV有對應的函式讓我們使用,就是findContours函式。

	Mat ImageBinary = ImageIn;//二值化的圖片(傳入時應該就已經是二值化的圖片了)
	vector<vector<Point>> contours;//定義輪廓向量

	findContours(ImageBinary, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);//尋找輪廓

	int max_size = 0;//最大的輪廓的size(通常來說就是我們的手勢輪廓)
	int contour_num = 0;//最大的輪廓是圖片中的第contour_num個輪廓
	for (int i = 0; i < contours.size(); i++)//找到最大的輪廓,記錄在contour_num中
	{
		if (contours[i].size() > max_size)
		{
			max_size = contours[i].size();
			contour_num = i;
		}
	}

這樣,我們就找到了圖片的全部輪廓,並且存在了二維向量contours中。然後我們就可以計算傅立葉描繪子了。

	/***計算影像的傅立葉描繪子***/
	/***傅立葉變換後的係數儲存在f[d]中***/
	vector<float>f;
	vector<float>fd;//最終傅立葉描繪子前14位
	Point p;
	for (int i = 0; i < max_size; i++)//主要的計算部分
	{
		float x, y, sumx = 0, sumy = 0;
		for (int j = 0; j < max_size; j++)
		{
			p = contours[contour_num].at(j);
			x = p.x;
			y = p.y;
			sumx += (float)(x * cos(2 * CV_PI * i * j / max_size) + y * sin(2 * CV_PI * i * j / max_size));
			sumy += (float)(y * cos(2 * CV_PI * i * j / max_size) - x * sin(2 * CV_PI * i * j / max_size));
		}
		f.push_back(sqrt((sumx * sumx) + (sumy * sumy)));
	}

	fd.push_back(0);//放入了標誌位‘0’,並不影響最終結果

	for (int k = 2; k < 16; k++)//進行歸一化,然後放入最終結果中
	{
		f[k] = f[k] / f[1];
		fd.push_back(f[k]);
	}

	out = Mat::zeros(1, fd.size(), CV_32F);//out是用於輸出的手勢特徵
	for (int i = 0; i < fd.size(); i++)
	{
		out.at<float>(i) = fd[i];
	}

計算完了傅立葉描繪子,我們就算是拿到了手勢的特徵了。
 
 
四:找到訓練樣本
 
  首先,我們要明確找到訓練樣本是很重要的,因為訓練樣本決定了你能識別到的是什麼。要是你找到了身份證數字的訓練樣本,那麼你就能識別身份證的數字;要是你找到了車牌號碼的數字樣本,那麼你就能識別車牌;而我們在這個專案要找到手勢的樣本。
  不過在這裡我可以將我找到的樣本分享給大家,我將其一併打包在了專案檔案中,大家有需要的可以下載我的工程檔案。其中有10個手勢,從0到9,希望能幫助到大家。
 
 
五:樣本的特徵提取
 
  對於樣本的特徵提取,其實跟前面我們說到的特徵提取沒有什麼不同。只不過我們這裡需要的是對大量的樣本進行特徵提取,並且將其存放在一個檔案中,方便我們後續進行的神經網路訓練。

void getAnnXML()
{
	FileStorage fs("ann_xml.xml", FileStorage::WRITE);
	if (!fs.isOpened())
	{
		cout << "failed to open " << "/n";
	}

	Mat trainData;//用於存放樣本的特徵資料
	Mat classes = Mat::zeros(200, 1, CV_8UC1);//用於標記是第幾類手勢
	char path[60];//樣本路徑
	Mat Image_read;//讀入的樣本

	for (int i = 0; i < 4; i++)//第i類手勢 比如手勢1、手勢2
	{
		for (int j = 1; j < 51; j++)//每個手勢設定50個樣本
		{
			sprintf_s(path, "D:\\數字影像處理\\Gesture_Picture\\%d_ (%d).png", i, j);
			Image_read = imread(path, 1);
			Mat Binary = OTSU_Binary(Image_read, 1);//對輸入的圖片進行二值化
			Mat dst_feature;//該樣本對應的特徵值
			getFeatures(Binary,dst_feature);
			trainData.push_back(dst_feature);

			classes.at<uchar>(i * 50 + j - 1) = i;
		}
	}
	fs << "TrainingData" << trainData;
	fs << "classes" << classes;
	fs.release();

	cout << "訓練矩陣和標籤矩陣搞定了!" << endl;

  在該段程式碼執行完之後,在資料夾下會生成一個ann_xml.xml檔案,裡面存放著所有樣本的特徵值和一個標籤矩陣(標籤矩陣如下圖)
標籤矩陣
  其中的getFeatures函式與前面用到的特徵提取函式是完全一致的。但是在這部分我們是可以省去找到皮膚部分的函式,因為樣本給到我們的就已經是隻有皮膚部分的圖片了。所以我們只需要進行二值化並找到最大的輪廓,提取特徵。
 
 
六:訓練神經網路
 
  一些想法:神經網路的訓練看起來像是比較難的一個部分,但其實並不是,看起來難的原因也許是之前並沒有接觸過神經網路,覺得無從下手。(所以在寫這部分程式碼之前應該先去了解一下什麼是神經網路,不需要過於深入,知道基本的工作原理即可)但其實只要寫過一次,就會發現這是一個模板性的東西,寫過了就是從0到1的變化。當然在寫該部分的程式碼時會遇到很多的小問題,比如OPENCV版本的不同,有部分網上的程式碼使用的是OPENCV2,對於OPENCV3就不適用了。
 
廢話不多說了直接開始吧,首先是訓練部分的函式程式碼:

void ann_train(Ptr<ANN_MLP>& ann, int numCharacters, int nlayers)//神經網路訓練函式,numCharacters設定為4,nlayers設定為24
{
	Mat trainData, classes;
	FileStorage fs;
	fs.open("ann_xml.xml", FileStorage::READ);

	fs["TrainingData"] >> trainData;
	fs["classes"] >> classes;

	Mat layerSizes(1, 3, CV_32SC1);     //3層神經網路
	layerSizes.at<int>(0) = trainData.cols;   //輸入層的神經元結點數,設定為15
	layerSizes.at<int>(1) = nlayers; //1個隱藏層的神經元結點數,設定為24
	layerSizes.at<int>(2) = numCharacters; //輸出層的神經元結點數為:4

	ann->setLayerSizes(layerSizes);
	ann->setTrainMethod(ANN_MLP::BACKPROP, 0.1, 0.1);//後兩個引數: 權梯度項的強度(一般設定為0.1) 動量項的強度(一般設定為0.1)
	ann->setActivationFunction(ANN_MLP::SIGMOID_SYM);
	ann->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 5000, 0.01));//後兩項引數為迭代次數和誤差最小值

	Mat trainClasses;//用於告訴神經網路該特徵對應的是什麼手勢
	trainClasses.create(trainData.rows, numCharacters, CV_32FC1);
	for (int i = 0; i < trainData.rows; i++)
	{
		for (int k = 0; k < trainClasses.cols; k++)
		{
			if (k == (int)classes.at<uchar>(i))
			{
				trainClasses.at<float>(i, k) = 1;
			}
			else
				trainClasses.at<float>(i, k) = 0;

		}

	}
	//Mat weights(1 , trainData.rows , CV_32FC1 ,Scalar::all(1) );
	ann->train(trainData, ml::ROW_SAMPLE, trainClasses);

	cout << " 訓練完了! " << endl;
}

程式碼說明:
 
1、首先我們要將ann_xml.xml檔案中的資料讀入到程式中,包括樣本的特徵還有標籤矩陣。
 
2、然後設定神經網路的基本引數。首先,設定3層的神經網路,即1層輸入層,1層隱藏層,1層輸出層。
 
輸入層的結點數 這卻決於我們一張圖片獲得的特徵值的數量。(根據第三大點中說到的,一個0標誌位加前14位傅立葉描繪子,總共有15位)
 
隱藏層的結點數 我們在該專案中將隱藏層設定為24。
 
輸出層的結點數 我們最後想讓神經網路識別幾個手勢,就設定為多少。比如該專案最後要識別四個手勢,就設定為4。
 
setTrainMethod(訓練方法) 使用ANN_MLP::BACKPROP,即反向傳播神經網路,這個較為常用。
 
setActivationFunction(啟用函式) 一般常用的就是Sigmoid 函式,即下圖的函式
sigmoid函式
setTermCriteria為迭代終止準則的設定
 
trainClasses變數  這個變數建議大家畫個圖理解一下。到底和class的區別是什麼?雖然都是作為一個標籤矩陣,但還是有一些不同。
 
train函式 這個就是神經網路的訓練函式,把相應的引數代入進去,就能夠訓練出一個神經網路。
 
  需要注意的是,這個函式不需要每一次都識別都執行,(而且執行該函式需要花費較多的時間)只要我們執行一次,把神經神經網路儲存下來,下次識別的時候直接讀取神經網路,就可以進行識別了。具體如下:

		Ptr<ANN_MLP> ann = ANN_MLP::create();
		ann_train(ann, 4, 24);
		ann->save("ann_param");

這樣就能儲存一個ann_param的檔案了,下次使用只需要

Ptr<ANN_MLP> ann = ANN_MLP::load("ann_param");//讀取神經網路

 
 
七:比對分類,預測結果
 
預測函式的作用就是幫助我們對比圖片的特徵與神經網路裡哪個最像,然後把這個作為結果輸出。所以在這裡我們要獲取要預測的圖片的特徵,然後使用預測函式predict進行預測。最後找到邏輯最大值,作為結果。

int classify(Ptr<ANN_MLP>& ann, Mat& Gesture)//預測函式,找到最符合的一個手勢(輸入的圖片是二值化的圖片)
{
	int result = -1;

	Mat output(1, 4, CV_32FC1); //1*4矩陣
	
	Mat Gesture_feature;
	getFeatures(Gesture, Gesture_feature);
	ann->predict(Gesture_feature, output);

	Point maxLoc;
	double maxVal;
	minMaxLoc(output, 0, &maxVal, 0, &maxLoc);//對比值,看哪個是最符合的。
	
	result = maxLoc.x;

	return result;
}

output變數儲存的是“該手勢對應每個手勢的可能性”,找到最大的那個可能,作為結果輸出。至此,我們獲得了我們想要的那個結果。
 
 
結尾:
 
我們最後把main函式放上來,這樣就能更清楚地看清流程:

int main(void)
{
	Mat ImageIn = imread("D://VSprogram/DistinguishGesture/MyTestPicture/test.jpg", 1);//輸入彩色圖片

	Mat Binary = skinMask(ImageIn);//獲取二值化後的手勢圖
	
	Mat Binary2 = OPENorCLOSE_Operation(Binary, 0, 3, 3);//對手勢圖進行閉運算,使一些小的點連起來

		//只需要執行一次,獲取了標籤矩陣和神經網路後就不再需要執行
		//getAnnXML();
		//Ptr<ANN_MLP> ann = ANN_MLP::create();
		//ann_train(ann, 4, 24);
		//ann->save("ann_param");
		//cout << "搞定" << endl;

	//Mat contour = showContour(Binary2);//獲得手勢的輪廓圖
	//imshow("Contour", contour);//畫出手勢的輪廓圖

	Ptr<ANN_MLP> ann = ANN_MLP::load("ann_param");//讀取神經網路

	int result = classify(ann, Binary2);//預測最終結果

	cout << "手勢是:"<<result << endl;//輸出
	

	waitKey(0);
	return 0;
}

然後我再把原始碼連結放在這裡,需要的可以下載來看看(我儘量把積分調低,讓大家即使覺得沒啥用也不會虧多少積分)
 
(資源還在稽核中,如果急需可以私信我)
 
 
參考文獻及連結:
 
趙三琴,丁為民,劉徳營.基於傅立葉描述子的稻飛蝨形狀識別[J].農業機械學報,2009,40(8):181~184
 
https://blog.csdn.net/qq_41562704/article/details/88975569
https://blog.csdn.net/javastart/article/details/97615918

相關文章