本文原始地址:OpenCV for Ios 學習筆記(5)-標記檢測2
相關性搜尋
void MarkerDetector::findMarkerCandidates( const ContoursVector& contours, std::vector<Marker>& detectedMarkers){ PointsVector approxCurve;//相似形狀 std::vector<Marker> possibleMarkers;//可能的標記 //分析每個標記,如果是一個類似標記的平行六面體... for (size_t i = 0; i < contours.size(); i++) { //近似一個多邊形 double eps = contours[i].size() * 0.05; //使多邊形邊緣平滑,得到近似的多邊形 cv::approxPolyDP(contours[i], approxCurve, eps, true); //這裡只考慮四邊形 if (approxCurve.size() != 4) continue; //而且必須是凸面的 if (!cv::isContourConvex(approxCurve)) continue; //確保相鄰的兩點間的距離“足夠大”-大到是一條邊而不是短線段就是了 float minDist = std::numeric_limits<float>::max(); for (int i = 0; i < 4; i++) { cv::Point side = approxCurve[i] - approxCurve[(i + 1)% 4] ; float squaredSideLength = side.dot(side); //取最小值 minDist = std::min(minDist, squaredSideLength); } //確保距離不要太短 if (minDist < m_minContourLengthAllowed) continue; //儲存相似的標記 Marker m; for (int i = 0; i < 4; i ++) { m.points.push_back(cv::Point2f(approxCurve[i].x,approxCurve[i].y)); } //逆時針排列這些點 //第一個點和第二個點之間連一條線 //如果第三個點在右邊,那麼這些點就是逆時針-??? cv::Point v1 = m.points[1] - m.points[0]; cv::Point v2 = m.points[2] - m.points[0]; double o = (v1.x * v2.y) - (v1.y * v2.x); if (o < 0.0)//如果第三個點在左邊,那麼需要把點排列成逆時針 { //http://blog.csdn.net/dizuo/article/details/6435847 //交換兩點的位置 std::swap(m.points[1], m.points[3]); } possibleMarkers.push_back(m); } //移除角落太接近的元素 //第一次檢測相似性 std::vector<std::pair<int, int>> tooNearCandidates; for (size_t i = 0; i < possibleMarkers.size(); i ++) { const Marker& m1 = possibleMarkers[i]; //計算每個邊角到其他可能標記的最近邊角的平均距離 for (size_t j = i + 1; j < possibleMarkers.size(); j ++) { const Marker& m2 = possibleMarkers[j]; float distSquared = 0.0; for (int c = 0; c < 4; c ++) { cv::Point v = m1.points[c] - m2.points[c]; //向量的點乘-》兩點的距離 distSquared += v.dot(v); } distSquared /= 4; if (distSquared < 100) { tooNearCandidates.push_back(std::pair<int, int>(i,j)); } } } std::vector<bool> removalMask (possibleMarkers.size(),false); for (size_t i = 0; i < tooNearCandidates.size(); i ++) { //周長 float p1 = perimeter(possibleMarkers[tooNearCandidates[i].first].points); float p2 = perimeter(possibleMarkers[tooNearCandidates[i].second].points); size_t removalIndex; if (p1>p2) { removalIndex = tooNearCandidates[i].second; }else { removalIndex = tooNearCandidates[i].first; } removalMask[removalIndex] = true; } //返回可能的對象 detectedMarkers.clear(); for (size_t i = 0; i < possibleMarkers.size(); i++) { if (!removalMask[i]) { detectedMarkers.push_back(possibleMarkers[i]); } }}
上面方法我們已經擷取到了一系列的可疑標記,為了進一步確認它們是不是我們想要的標記,還需要以下三步:
1.去掉透視投影,得到平面/正面的矩形。
2.使用Otsu演算法進行映像的閥值運算。
3.最後是標記的識別編碼。
為了得到這些矩形的標記映像,我們不得不使用透視變換去恢複(unwarp)輸入的映像。這個矩陣應該使用cv::getPerspectiveTransform函數,它首先根據四個對應的點找到透視變換,第一個參數是標記的座標,第二個是正方形標記映像的座標。估算的變換將會把標記轉換成方形,從而方便我們分析。
//分析每一個捕獲到的標記 Marker& marker = detectedMarkers[i]; // 找到透視投影,並把標記轉換成矩形 //輸入映像四邊形頂點座標 //輸出映像的相應的四邊形頂點座標 cv::Mat markerTransform = cv::getPerspectiveTransform(marker.points, m_markerCorners2d); // Transform image to get a canonical marker image //輸入的映像 //輸出的映像 //3x3變換矩陣 cv::warpPerspective(grayscale, canonicalMarkerImage, markerTransform, markerSize);
映像轉換成正視圖:
現在測試我們的標記是否是有效。
使用Otsu演算法移除灰色的像素,只留下黑色和白色像素。
//這是固定閥值方法 //輸入映像image必須為一個2值單通道映像 //檢測的輪廓數組,每一個輪廓用一個point類型的vector表示 //閥值 //max_value 使用 CV_THRESH_BINARY 和 CV_THRESH_BINARY_INV 的最大值 //type cv::threshold(grey, grey, 125, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
標記編碼識別
我們(作者)所使用的標記都有一個內部的5x5編碼,採用的是簡單修改的漢明碼。簡單的說,就是5bits中只有2bits被使用,其他三位都是錯誤的識別碼,也就是說我們至多有1024種不同的標識。
我們的漢明碼最大的不同是,漢明碼的第一位(同位位元的3和5)是反向的。所有ID 0(在漢明碼是00000),在這裡是10000,目的是減少環境造成的影響(?)。
然後我們根據標記計算每個5x5地區的黑色和白色像素的個數。
識別標記編碼
//儲存判斷結構的矩陣 cv::Mat bitMatrix = cv::Mat::zeros(5,5,CV_8UC1); //判斷每個5x5地區,是白色像素還是黑色像素 for (int y=0;y<5;y++) { for (int x=0;x<5;x++) { int cellX = (x+1)*cellSize; int cellY = (y+1)*cellSize; //建立grey不同尺寸的映像 /*與以下等價 cv::Rect rect(cellX,cellY,cellSize,cellSize); cv::Mat cell = grey(rect); */ cv::Mat cell = grey(cv::Rect(cellX,cellY,cellSize,cellSize)); //計算非0像素的個數 int nZ = cv::countNonZero(cell); if (nZ> (cellSize*cellSize) /2) bitMatrix.at<uchar>(y,x) = 1; } }
根據相機角度,有下列四種標記可能的形狀:
我們已經有了四種可能的標記圖片,我們不得不找出正確形態的標記。前面我們已經知道了每2bits的3個同位位元(每5bits中只有2bits有效,其餘3個只是為了校正存在)。正確的標記應該有0漢明距離誤差!
//檢查所有可能的位置 cv::Mat rotations[4]; int distances[4]; rotations[0] = bitMatrix; //漢明距離 distances[0] = hammDistMarker(rotations[0]); std::pair<int,int> minDist(distances[0],0); for (int i=1; i<4; i++) { //找到最小的漢明距離 rotations[i] = rotate(rotations[i-1]); distances[i] = hammDistMarker(rotations[i]); if (distances[i] < minDist.first) { minDist.first = distances[i]; minDist.second = i; } }
以上代碼找到的最小漢明距離應該是0,如果不是,則是一個錯誤的標記模式-遇到損壞的標記或者假陽性標記檢測(false-positive marker detection)。
-漢明距離:
在資訊理論中,兩個等長字串之間的漢明距離是兩個字串對應位置的字元不同的個數。換句話說,它就是將 一個字串變換成另外一個字串所需要替換的字元個數。
變換位置:
cv::Mat Marker::rotate(cv::Mat in){ cv::Mat out; in.copyTo(out); for (int i=0;i<in.rows;i++) { for (int j=0;j<in.cols;j++) { out.at<uchar>(i,j)=in.at<uchar>(in.cols-j-1,i); } } return out;}