1. 程式人生 > >#基於VR環境下的手指識別鍵盤輸入# 利用攝像頭完成簡單的手指識別的方法

#基於VR環境下的手指識別鍵盤輸入# 利用攝像頭完成簡單的手指識別的方法

因為我們在進行虛擬鍵盤的設定的時候,需要利用攝像頭對手指進行拍攝,確定手指當前的位置,然後進行鍵盤輸入內容的估計,所以採用了以下方法:

方法一:重心距離法


        見下圖,紅色點是手的重心,那麼手的邊緣的所有點與重心點的距離按順時針方向或者逆時針方向遍歷,就會出現五個峰值,分別是五個手指,這樣我們就可以簡單找到了。如果你是隻伸出一兩個手指,那麼就只有一兩個峰值了。








簡單的程式碼如下:


1、對影象做高斯模糊;


2、膚色分割(背景不要有類膚色,如果有,就需要加其他資訊來排除干擾);


3、找到手輪廓;


4、計算矩,即重心;


5、尋找指尖。

    
#include "opencv2/opencv.hpp"    
    
using namespace cv;    
using namespace std;    
    
void skinExtract(const Mat &frame, Mat &skinArea);    
    
int main(int argc, char* argv[])    
{    
    Mat frame, skinArea;    
    VideoCapture capture;    
    
    capture.open(0);    
    if (!capture.isOpened())    
    {    
        cout<<"No camera!\n"<<endl;    
        return -1;    
    }    
    
    while (1)    
    {    
        capture >> frame;    
        //Mat frame = imread("fingertips(1).jpg");    
        if (frame.empty())    
            break;    
    
        skinArea.create(frame.rows, frame.cols, CV_8UC1);    
        skinExtract(frame, skinArea);    
        Mat show_img;    
        frame.copyTo(show_img, skinArea);    
    
        vector<vector<Point> > contours;    
        vector<Vec4i> hierarchy;    
    
        //尋找輪廓    
        findContours(skinArea, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);    
    
        // 找到最大的輪廓    
        int index;    
        double area, maxArea(0);    
        for (int i=0; i < contours.size(); i++)    
        {    
            area = contourArea(Mat(contours[i]));    
            if (area > maxArea)    
            {    
                maxArea = area;    
                index = i;    
            }               
        }    
    
        //drawContours(frame, contours, index, Scalar(0, 0, 255), 2, 8, hierarchy );    
            
        Moments moment = moments(skinArea, true);    
        Point center(moment.m10/moment.m00, moment.m01/moment.m00);    
        circle(show_img, center, 8 ,Scalar(0, 0, 255), CV_FILLED);    
    
        // 尋找指尖    
        vector<Point> couPoint = contours[index];    
        vector<Point> fingerTips;    
        Point tmp;    
        int max(0), count(0), notice(0);    
        for (int i = 0; i < couPoint.size(); i++)    
        {    
            tmp = couPoint[i];    
            int dist = (tmp.x - center.x) * (tmp.x - center.x) + (tmp.y - center.y) * (tmp.y - center.y);    
            if (dist > max)    
            {    
                max = dist;    
                notice = i;    
            }    
    
            // 計算最大值保持的點數,如果大於40(這個值需要設定,本來想根據max值來設定,    
            // 但是不成功,不知道為何),那麼就認為這個是指尖    
            if (dist != max)    
            {    
                count++;    
                if (count > 40)    
                {    
                    count = 0;    
                    max = 0;    
                    bool flag = false;    
                    // 低於手心的點不算    
                    if (center.y < couPoint[notice].y )    
                        continue;    
                    // 離得太近的不算    
                    for (int j = 0; j < fingerTips.size(); j++)    
                    {    
                        if (abs(couPoint[notice].x - fingerTips[j].x) < 20)    
                        {    
                            flag = true;    
                            break;    
                        }    
                    }    
                    if (flag) continue;    
                    fingerTips.push_back(couPoint[notice]);    
                    circle(show_img, couPoint[notice], 6 ,Scalar(0, 255, 0), CV_FILLED);    
                    line(show_img, center, couPoint[notice], Scalar(255, 0, 0), 2);                 
                }    
            }    
        }    
    
        imshow("show_img", show_img);    
    
        if ( cvWaitKey(20) == 'q' )    
            break;    
    }    
    
    return 0;    
}    
    
//膚色提取,skinArea為二值化膚色影象    
void skinExtract(const Mat &frame, Mat &skinArea)    
{    
    Mat YCbCr;    
    vector<Mat> planes;    
    
    //轉換為YCrCb顏色空間    
    cvtColor(frame, YCbCr, CV_RGB2YCrCb);    
    //將多通道影象分離為多個單通道影象    
    split(YCbCr, planes);     
    
    //運用迭代器訪問矩陣元素    
    MatIterator_<uchar> it_Cb = planes[1].begin<uchar>(),    
                        it_Cb_end = planes[1].end<uchar>();    
    MatIterator_<uchar> it_Cr = planes[2].begin<uchar>();    
    MatIterator_<uchar> it_skin = skinArea.begin<uchar>();    
    
    //人的面板顏色在YCbCr色度空間的分佈範圍:100<=Cb<=127, 138<=Cr<=170    
    for( ; it_Cb != it_Cb_end; ++it_Cr, ++it_Cb, ++it_skin)    
    {    
        if (138 <= *it_Cr &&  *it_Cr <= 170 && 100 <= *it_Cb &&  *it_Cb <= 127)    
            *it_skin = 255;    
        else    
            *it_skin = 0;    
    }    
    
    //膨脹和腐蝕,膨脹可以填補凹洞(將裂縫橋接),腐蝕可以消除細的凸起(“斑點”噪聲)    
    dilate(skinArea, skinArea, Mat(5, 5, CV_8UC1), Point(-1, -1));    
    erode(skinArea, skinArea, Mat(5, 5, CV_8UC1), Point(-1, -1));    
}   
方法二:曲率分析法


       見下圖,可以看到,在指尖和兩指之間的凹槽的曲率是最大的。假設手的輪廓是通過一系列點{Pi}來儲存的,那麼我們可以通過 [Pi, Pi-k] 和 [Pi, Pi+k] 兩個向量的內積(點乘)來尋找高曲率的點,如下圖的Φ和β的計算結果,因為他們的兩個向量的夾角趨向於90度,也就是它們的內積趨向於0,所以內積結果越小,曲率越大,我們只需要設定一個閾值,就可以把手的指尖和兩指之間的凹槽都可以找到。


       那麼如何分辨指尖和兩指之間的凹槽呢?我們可以通過兩個向量的叉乘來計算,我們想象下手處於3D空間中,那麼就還有一個z方向,β中兩個向量的叉乘是大於0的(叉乘方向垂直紙面朝向你,z軸正方向),而Φ處的叉乘是小於0的(叉乘方向垂直紙面遠離你,z軸負方向)。


        綜上,通過在每個點上構造兩個向量,比較他們的點乘和叉乘就可以確定指尖了。








簡單的程式碼如下:


1、對影象做高斯模糊;


2、膚色分割(背景不要有類膚色,如果有,就需要加其他資訊來排除干擾);


3、找到手輪廓;


4、尋找指尖。

    
#include "opencv2/opencv.hpp"    
    
using namespace cv;    
using namespace std;    
    
void skinExtract(const Mat &frame, Mat &skinArea);    
    
int main(int argc, char* argv[])    
{    
    Mat frame, skinArea;    
    VideoCapture capture;    
    
    capture.open(0);    
    if (!capture.isOpened())    
    {    
        cout<<"No camera!\n"<<endl;    
        return -1;    
    }    
    
    while (1)    
    {    
        capture >> frame;    
        //Mat frame = imread("fingertips(1).jpg");    
        if (frame.empty())    
            break;    
    
        GaussianBlur(frame, frame, Size(3, 3), 0);    
        skinArea.create(frame.rows, frame.cols, CV_8UC1);    
        skinExtract(frame, skinArea);    
        Mat show_img;    
        frame.copyTo(show_img, skinArea);    
    
        vector<vector<Point> > contours;    
        vector<Vec4i> hierarchy;    
    
        //尋找輪廓    
        findContours(skinArea, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);    
    
        // 找到最大的輪廓    
        int index;    
        double area, maxArea(0);    
        for (int i=0; i < contours.size(); i++)    
        {    
            area = contourArea(Mat(contours[i]));    
            if (area > maxArea)    
            {    
                maxArea = area;    
                index = i;    
            }               
        }    
    
        //drawContours(frame, contours, index, Scalar(0, 0, 255), 2, 8, hierarchy );    
            
        Moments moment = moments(skinArea, true);    
        Point center(moment.m10/moment.m00, moment.m01/moment.m00);    
        circle(show_img, center, 8 ,Scalar(0, 0, 255), CV_FILLED);    
    
        // 尋找指尖    
        vector<Point> couPoint = contours[index];    
        int max(0), count(0), notice(0);    
        vector<Point> fingerTips;    
        Point p, q, r;    
        for (int i = 5; (i < (couPoint.size() - 5)) && couPoint.size(); i++)    
        {    
            q = couPoint[i - 5];    
            p = couPoint[i];    
            r = couPoint[i + 5];    
            int dot = (q.x - p.x ) * (q.y - p.y) + (r.x - p.x ) * (r.y - p.y);    
            if (dot < 20 && dot > -20)    
            {    
                int cross = (q.x - p.x ) * (r.y - p.y) - (r.x - p.x ) * (q.y - p.y);    
                if (cross > 0)    
                {    
                    fingerTips.push_back(p);    
                    circle(show_img, p, 5 ,Scalar(255, 0, 0), CV_FILLED);    
                    line(show_img, center, p, Scalar(255, 0, 0), 2);        
                }    
            }    
        }    
    
        imshow("show_img", show_img);    
    
        if ( cvWaitKey(20) == 'q' )    
            break;    
    }    
    
    return 0;    
}    
    
//膚色提取,skinArea為二值化膚色影象    
void skinExtract(const Mat &frame, Mat &skinArea)    
{    
    Mat YCbCr;    
    vector<Mat> planes;    
    
    //轉換為YCrCb顏色空間    
    cvtColor(frame, YCbCr, CV_RGB2YCrCb);    
    //將多通道影象分離為多個單通道影象    
    split(YCbCr, planes);     
    
    //運用迭代器訪問矩陣元素    
    MatIterator_<uchar> it_Cb = planes[1].begin<uchar>(),    
                        it_Cb_end = planes[1].end<uchar>();    
    MatIterator_<uchar> it_Cr = planes[2].begin<uchar>();    
    MatIterator_<uchar> it_skin = skinArea.begin<uchar>();    
    
    //人的面板顏色在YCbCr色度空間的分佈範圍:100<=Cb<=127, 138<=Cr<=170    
    for( ; it_Cb != it_Cb_end; ++it_Cr, ++it_Cb, ++it_skin)    
    {    
        if (138 <= *it_Cr &&  *it_Cr <= 170 && 100 <= *it_Cb &&  *it_Cb <= 127)    
            *it_skin = 255;    
        else    
            *it_skin = 0;    
    }    
    
    //膨脹和腐蝕,膨脹可以填補凹洞(將裂縫橋接),腐蝕可以消除細的凸起(“斑點”噪聲)    
    dilate(skinArea, skinArea, Mat(5, 5, CV_8UC1), Point(-1, -1));    
    erode(skinArea, skinArea, Mat(5, 5, CV_8UC1), Point(-1, -1));    
}    

效果演示如下:



不足之處:容易受到環境中類膚色的影響。