1. 程式人生 > >iOS開發之opencv學習筆記四:使用feature2d識別圖片

iOS開發之opencv學習筆記四:使用feature2d識別圖片

使用過vuforia或者亮風臺的朋友應該知道,這兩個平臺對圖片的跟蹤的準備工作是很簡單的,只需要幾張樣本圖片就可以做了。

但是按照上篇的介紹,如果用CascadeClassifier進行物體跟蹤就需要非常非常多的樣本,那麼,要對圖片進行識別跟蹤就沒有像上面說的兩個平臺那樣簡便的辦法嗎?

答案是可以選擇feature2d。

1.feature2d是什麼?

字面上就很容易理解,feature2d是做2d影象的特徵處理的。比如我們可以用它做指紋取樣、識別,影象的角點提取,影象的跟蹤等等。

這個模組就在opencv的modules/feature2d裡面。

2.用feature2d做圖片跟蹤需要準備什麼?

說這個之前,先要說一下nonfree模組,我在第一篇提到過這個。

影象的特徵提取有很多演算法,有些演算法並非由opencv提供,opencv提出了特徵提取,比對的準則,而大多演算法是由opencv的使用者們完成的。

在opencv2.x,這些演算法都放在nonfree模組裡,但是到了opencv3.x,nonfree模組就被去掉了。取而代之的是opencv_contrib庫,它同樣一個使用BSD協議的開源庫。

遺憾的是,目前筆者找到的opencv_contrib只能在x86系統上成功編譯,也就是說目前還不支援像android,iOS這樣的使用arm架構的系統。

然而,我們要做的功能只是用了它其中的xfeature2d模組,這只是它諸多模組中的之一,而且跟其他模組沒有任何耦合。

沒錯,我們完全可以把xfeature2d的原始碼單獨做一個庫,然後用在我們的功能裡。筆者已經成功編譯出了一個xfeature2d的靜態庫,然而,限於版權問題,這裡就不上程式碼了。

大家自己動動靈活的小腦袋吧。

3.一切準備就緒,然後怎麼做?

既然要做圖片跟蹤,當然先要做圖片對比,那麼怎麼做圖片的對比呢?我們使用特徵點對比,這樣我們先要做的就是用feature2d提取圖片的特徵點

特徵點提取的演算法有很多,筆者用過的大體有這三個:SURF,SIFT,ORB。在這裡我就介紹SIFT演算法吧,我更願意稱之為運算元(因為聽起來很專業嘛)。

這三種運算元有何區別呢?我可以很大膽的告訴你:不清楚。

我不研究它們的工作原理,哪個好用用哪個,這是我一貫的作風。

第二篇一樣,做一個攝像頭預覽的ViewController。

我們需要一張樣本圖片,就是我們需要識別跟蹤的圖片,用這張圖片建立一個灰度cv::Mat。

NSString *imp = [[NSBundle mainBundle] pathForResource:ifn ofType:@"jpg"];
            
            Mat gray = imread([imp cStringUsingEncoding:NSUTF8StringEncoding], IMREAD_GRAYSCALE);
建立一個detector跟extractor,用這個detector來提取一個KeyPoint陣列,extractor提取一個descriptor


vector<KeyPoint> keypoints;
            Mat descriptor;
Ptr<SIFT> detector = SIFT::create();
            detector->detect(gray, keypoints);
            
            Ptr<SIFT> extractor = SIFT::create();
            extractor->compute(gray, keypoints, descriptor);

這時候樣本的特徵就取好了。

在取到攝像頭幀之後,做第二篇一樣的處理,得到一個Mat,然後灰化

Mat image, gray;
image = [得到的mat];
cvtColor(image,gray,CV_BGR2GRAY);

同樣,用SIFT運算元提取這張灰度圖的特徵kp,d。
vector<KeyPoint> kp;
        Mat d;
Ptr<SIFT> detector = SIFT::create();
        detector->detect(gray, kp);
        
        if (kp.size() <= 0) {
            return;
        }
        
        Ptr<SIFT> extractor = SIFT::create();
        extractor->compute(gray, kp, d);

為什麼要判斷kp.size()?因為你的攝像頭可能會得到一張純色毫無輪廓角點的圖片,這樣kp.size()就是0,這就沒必要做對比了。

接下來就可以做對比,得到一個DMatch陣列

vector<DMatch> matches;
BFMatcher matcher = BFMatcher();
matcher.match(descriptor, d, matches);

從DMatch數組裡面照到最小和最大distance
double maxDist = 0.0;
            double minDist = DBL_MAX;
            for (int i=0;i<matches.size();i++) {
                
                DMatch match = matches[i];
                double dist = match.distance;
                if (dist < minDist) {
                    minDist = dist;
                }
                if (dist > maxDist) {
                    maxDist = dist;
                }
            }

過濾distance過大的DMatch
double maxGoodMatchDist = THRESHOLD * minDist;
            vector<DMatch> goodMatches;
            for( int i = 0; i <descriptor.rows; i++ )
            {
                if( matches[i].distance < maxGoodMatchDist ){
                    goodMatches.push_back(matches[i]);
                }
            }

這裡的THERSHOLD視運算元而定,SIFT可以設成2。根據過濾好的DMatch陣列再來過濾不吻合的keypoint,分別得到樣本和當前取到的幀裡面吻合的點
vector<cv::Point> modePoints;
            vector<cv::Point> scenePoints;
            
            for (int i=0;i<goodMatches.size();i++) {
                
                DMatch goodMatche = goodMatches[i];
                
                if (goodMatche.queryIdx < 0 || goodMatche.queryIdx >= keypoints.size()) {
                    continue;
                }
                
                if (goodMatche.trainIdx < 0 || goodMatche.trainIdx >= kp.size()) {
                    continue;
                }
                
                modePoints.push_back(keypoints[goodMatche.queryIdx].pt);
                scenePoints.push_back(kp[goodMatche.trainIdx].pt);
            }

得到一個homeography
Mat homography = findHomography(modePoints, scenePoints, CV_RANSAC);

找到四個邊界點
                if (homography.data == NULL) {
                    return;
                }
                
                std::vector<Point2f> objCorners(4);
                objCorners[0] = cvPoint(0,0);
                objCorners[1] = cvPoint( mode.mat.cols, 0 );
                objCorners[2] = cvPoint( mode.mat.cols, mode.mat.rows );
                objCorners[3] = cvPoint( 0, mode.mat.rows );
                
                std::vector<Point2f> sceneCorners(4);
                perspectiveTransform( objCorners, sceneCorners, homography);
                
                if (!(sceneCorners[1].x > sceneCorners[0].x
                      && sceneCorners[2].y > sceneCorners[1].y
                      && sceneCorners[3].x < sceneCorners[2].x
                      && sceneCorners[0].y < sceneCorners[3].y)) {

                    return;
                }

最後的判斷是判斷這四個邊界點是否能夠形成一個四邊形。然後利用這四個點畫一個四邊形覆蓋到預覽圖層
dispatch_async(dispatch_get_main_queue(), ^{
                    
                    UIGraphicsBeginImageContext(CGSizeMake(img.size.width, img.size.height));
                    CGContextRef contextRef = UIGraphicsGetCurrentContext();
                    
                    CGContextSetLineWidth(contextRef, 4);
                    CGContextSetRGBStrokeColor(contextRef, 1.0, 0.0, 0.0, 1);
                    
                    CGContextMoveToPoint(contextRef, sceneCorners[0].x, sceneCorners[0].y);
                    CGContextAddLineToPoint(contextRef, sceneCorners[1].x, sceneCorners[1].y);
                    CGContextAddLineToPoint(contextRef, sceneCorners[2].x, sceneCorners[2].y));
                    CGContextAddLineToPoint(contextRef, sceneCorners[3].x, sceneCorners[3].y));
                    CGContextAddLineToPoint(contextRef, sceneCorners[0].x, sceneCorners[0].y);
                    
                    CGContextStrokePath(contextRef);
                    
                    UIImage *rectImage = UIGraphicsGetImageFromCurrentImageContext();
                    _overlayImageView.image = rectImage;
                   
                    UIGraphicsEndImageContext();
                });

這樣就可以看到一個紅框框跟著樣本圖後面跑了。