基於標記的AR的opencv實現(一)
最近學習AR,買了本Mastering OpenCV,這書上有兩個AR的例子,這裡先分析的是第二章基於標識的AR,書中是使用Xcode給iphone或者ipad寫的,本文是在linux系統上vim實現的,終端模式。 先推薦兩個前輩的部落格,本文參考了二者和書進行理解原始碼。 http://blog.csdn.net/jinshengtao/article/details/48604435 taotao1233寫的,大部分是基於他的部落格,稱轉載也可以。 http://blog.csdn.net/acorld/article/details/8747813 missjuan寫的,就是分析在xcode上進行實現的。
我的程式原始碼已經上傳到http://download.csdn.net/detail/chuhang_zhqr/9298975,有需要的請下載。
以下開始進行分析:
程式大體框架:
1:對輸入影象幀進行標記檢測,這裡包括,灰度化,找到影象中輪廓,搜尋可能的標記;檢測和解碼標記,
2:估計標記的三維姿態,這裡包括提前對攝像機進行相機標定,獲取相機內參數和失真係數,根據這個計算出標記的旋轉矩陣和平移矩陣。
3:由相機內參數和標記的旋轉矩陣和平移矩陣,用OpenGL進行渲染三維物體;
以上是實現AR的必經之路,我認為OpenCV實現就已經很底層了,再底層那就太麻煩了,在科研的道路上,工程化實現未嘗不可。
因為是剛把原始碼實現了,沒來及完善工程,決定趁熱打鐵先記錄一下心路歷程。
現在開始分析原始碼,從第一步開始,輸入影象:輸入影象幀無非三種源,圖片,視訊,攝像頭。和書上一樣先使用圖片吧。就是這個圖片
書上的不是640x480的,我修了一下。src = imread("1.jpg");
1):灰度化處理:
必須將輸入影象轉換為灰度圖,因為標識僅包含黑白塊,這使得更容易在灰度影象上操作這些塊。
//彩色轉換成灰色影象
cvtColor(src,grayscale,CV_BGRA2GRAY);
}
這個沒什麼好說的,
2):執行二值化閾值操作:
將影象的每個畫素變成黑色或白色,為檢測輪廓做準備,使用合理的自適應閾值法,減小光照條件和軟強度變化影響。以需要二值化的畫素為中心,將給定半徑內的所有畫素的平均強度作為該畫素的強度,使輪廓檢測更具有魯棒性。
adaptiveThreshold(grayscale,//Input Image
thresholdImg,//Result binary image
255,
ADAPTIVE_THRESH_GAUSSIAN_C,
THRESH_BINARY_INV,
7,
7
);
/*輸入影象
//輸出影象
//使用 CV_THRESH_BINARY 和 CV_THRESH_BINARY_INV 的最大值
//自適應閾值演算法使用:CV_ADAPTIVE_THRESH_MEAN_C 或 CV_ADAPTIVE_THRESH_GAUSSIAN_C
//取閾值型別:必須是下者之一
//CV_THRESH_BINARY,
//CV_THRESH_BINARY_INV
//用來計算閾值的象素鄰域大小: 3, 5, 7, ...
*/
3):輪廓檢測:
使用findContours檢測輸入影象的輪廓。
//檢測所輸入的二值影象的輪廓,返回一個多邊形列表,其每個多邊形標識一個輪廓,小輪廓不關注,不包括標記...
void MarkerDetector::findContour(cv::Mat& thresholdImg, ContoursVector& contours, int minContourPointsAllowed) const
{
ContoursVector allContours;
/*輸入影象image必須為一個2值單通道影象
//檢測的輪廓陣列,每一個輪廓用一個point型別的vector表示
//輪廓的檢索模式
CV_RETR_EXTERNAL表示只檢測外輪廓
CV_RETR_LIST檢測的輪廓不建立等級關係
CV_RETR_CCOMP建立兩個等級的輪廓,上面的一層為外邊界,裡面的一層為內孔的邊界資訊。如果內孔內還有一個連通物體,這個物體的邊界也在頂層。
CV_RETR_TREE建立一個等級樹結構的輪廓。具體參考contours.c這個demo
//輪廓的近似辦法
CV_CHAIN_APPROX_NONE儲存所有的輪廓點,相鄰的兩個點的畫素位置差不超過1,即max(abs(x1-x2),abs(y2-y1))==1
CV_CHAIN_APPROX_SIMPLE壓縮水平方向,垂直方向,對角線方向的元素,只保留該方向的終點座標,例如一個矩形輪廓只需4個點來儲存輪廓資訊
CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似演算法
offset表示代表輪廓點的偏移量,可以設定為任意值。對ROI影象中找出的輪廓,並要在整個影象中進行分析時,這個引數還是很有用的。
*/
findContours(thresholdImg, allContours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);
contours.clear();
for(size_t i=0;i<allContours.size();i++)
{
int contourSize = allContours[i].size();
if(contourSize > minContourPointsAllowed)
{
contours.push_back(allContours[i]);
}
}
//Mat result(src.size(),CV_8U,Scalar(0));
//drawContours(result,detectedMarkers,-1,Scalar(255),2);
//imshow("AR based marker...",result);
}
其函式返回值為一個多邊形列表,其每個多邊形都表示一個輪廓。若輪廓包含的畫素數比minContourPointsAllowed還小,則是一個小輪廓,這裡不感興趣,直接去除,這些小輪廓可能並沒有包含標記。
4):搜尋候選標記:
在找到輪廓後,將開始進行多邊形逼近,這樣做為了減少輪廓的畫素。因為標記總是被四個頂點的多邊形包含,如果不是四個,就不是我們想要的標記。篩選出非標記區域。
用Opencv內建API檢測多邊形,通過判斷多邊形定點數量是否為4,四邊形各頂點之間相互距離是否滿足要求(四邊形是否足夠大),過濾非候選區域。然後再根據候選區域之間距離進一步篩選,得到最終的候選區域,並使得候選區域的頂點座標逆時針排列。
a. 四邊形頂點之間距離
對每個四邊形S,計算其相鄰頂點之間的距離:
上式中i,j為相鄰的兩個頂點,若頂點之間的最小值仍大於閾值,則保留該四邊形S進行下一步判斷。
b. 四邊形之間距離
求四邊形S和S’之間的距離,即計算四個頂點之間的平均距離:
若dist小於閾值,則四邊形S和S’距離較近,記錄到tooNearCandidates向量裡。接來下perimeter函式分別求四邊形S和S’四個頂點之間的距離和,保留距離較大的,將距離較小的放入removalMask陣列中,下式中i,j為四邊形內相鄰頂點
c. 行列式的幾何意義—逆時針排序
行列式是由一些資料排列成的方陣經過規定的計算方法而得到的一個數。那它的幾何意義是什麼呢?有兩種解釋:
一個是行列式就是行列式中的行或列向量所構成的超平行多面體的有向面積或有向體積;
另一個是矩陣A的行列式detA就是線性變化A下的圖形面積或體積的伸縮因子;
接下來的程式碼中,由於在approxPolyDP尋找多邊形時,頂點擺放次序有逆時針和順時針兩種,我們希望這些頂點按照逆時針擺放。因此,對於四邊形而言,我們只討論2*2行列式對應的有向面積。
一個2×2矩陣A的行列式,是A的行向量(或列向量)決定的平行四邊形的有向面積。用幾何觀點來看,二階行列式D是XOY平面上以行向量a=(a1,a2),b=(b1,b2)為鄰邊的平行
四邊形的有向面積。若這個平行四邊形是由向量a沿逆時針方向轉到b而得到的,面積取正值;若這個平行四邊形是由向量a沿順時針方向轉到而得到的,面積取負值。本例中,對於順時針擺放的頂點0,1,2,3,咱可以通過計算有0-1,0-2構成的向量計算其有向面積。如果是順時針擺放,那麼該有向面積一定是負數,只要交換1,3位置即可。
//由於我們的標記是四邊形,當找到影象所有輪廓細節後,本文用Opencv內建API檢測多邊形,通過判斷多邊形定點數量是否為4,四邊形各頂點之間相互距離是否滿足要求(四邊形是否足夠大),過濾非候選區域。然後再根據候選區域之間距離進一步篩選,得到最終的候選區域,並使得候選區域的頂點座標逆時針排列。
void MarkerDetector::findCandidates(const ContoursVector& contours,vector<Marker>& detectedMarkers)
{
vector<Point> approxCurve;//返回結果為多邊形,用點集表示//相似形狀
vector<Marker> possibleMarkers;//可能的標記
//For each contour,分析它是不是像標識,找到候選者//分析每個標記,如果是一個類似標記的平行六面體...
for(size_t i=0;i<contours.size();i++)
{
/*近似一個多邊形逼近,為了減少輪廓的畫素。這樣比較好,可篩選出非標記區域,因為標記總能被四個頂點的多邊形表示。如果多邊形的頂點多於或少於四個,就絕對不是本專案想要的標記。通過點集近似多邊形,第三個引數為epsilon代表近似程度,即原始輪廓及近似多邊形之間的距離,第四個引數表示多邊形是閉合的。*/
double eps = contours[i].size()*0.05;
//輸入影象的2維點集,輸出結果,估計精度,是否閉合。輸出多邊形的頂點組成的點集//使多邊形邊緣平滑,得到近似的多邊形
approxPolyDP(contours[i],approxCurve,eps,true);
//我們感興趣的多邊形只有四個頂點
if(approxCurve.size() != 4)
continue;
//檢查輪廓是否是凸邊形
if(!isContourConvex(approxCurve))
continue;
//確保連續點之間的距離是足夠大的。//確保相鄰的兩點間的距離“足夠大”-大到是一條邊而不是短線段就是了
//float minDist = numeric_limits<float>::max();//代表float可以表示的最大值,numeric_limits就是模板類,這裡表示max(float);3.4e038
float minDist = 1e10;//這個值就很大了
//求當前四邊形各頂點之間的最短距離
for(int i=0;i<4;i++)
{
Point side = approxCurve[i] - approxCurve[(i+1)%4];//這裡應該是2維的相減
float squaredSideLength = side.dot(side);//求2維向量的點積,就是XxY
minDist = min(minDist,squaredSideLength);//找出最小的距離
}
//檢查距離是不是特別小,小的話就退出本次迴圈,開始下一次迴圈
if(minDist<m_minContourLengthAllowed)
continue;
//所有的測試通過了,儲存標識候選,當四邊形大小合適,則將該四邊形maker放入possibleMarkers容器內 //儲存相似的標記
Marker m;
for(int i=0;i<4;i++)
m.points.push_back(Point2f(approxCurve[i].x,approxCurve[i].y));//vector標頭檔案裡面就有這個push_back函式,在vector類中作用為在vector尾部加入一個數據。
/*逆時針儲存這些點
//從程式碼推測,marker中的點集本來就兩種序列:順時針和逆時針,這裡要把順時針的序列改成逆時針,在多邊形逼近時,多邊形是閉合的,則不是順時針就是逆時針
//在第一個和第二個點之間跟蹤出一條線,如果第三個點在右邊,則點是逆時針儲存的//逆時針排列這些點,第一個點和第二個點之間連一條線,如果第三個點在邊,那麼這些點就是逆時針*/
Point v1 = m.points[1] - m.points[0];
Point v2 = m.points[2] - m.points[0];
/*行列式的幾何意義是什麼呢?有兩個解釋:一個解釋是行列式就是行列式中的行或列向量所構成的超平行多面體的有向面積或有向體積;另一個解釋是矩陣A的行列式detA就是線性變換A下的圖形面積或體積的伸縮因子。
//以行向量a=(a1,a2),b=(b1,b2)為鄰邊的平行四邊形的有向面積:若這個平行四邊形是由向量沿逆時針方向轉到b而得到的,面積取正值;若這個平行四邊形是由向量a沿順時針方向轉到而得到的,面積取負值; */
double o = (v1.x * v2.y) - (v1.y * v2.x);
if(o<0.0) //如果第三個點在左邊,那麼交換第一個點和第三個點,逆時針儲存
swap(m.points[1],m.points[3]);
possibleMarkers.push_back(m);//把這個標識放入候選標識向量中
}
//移除那些角點互相離的太近的四邊形//移除角點太接近的元素
vector< pair<int,int> > tooNearCandidates;
for(size_t i=0;i<possibleMarkers.size();i++)
{
const Marker& m1 = possibleMarkers[i];
//計算兩個maker四邊形之間的距離,四組點之間距離和的平均值,若平均值較小,則認為兩個maker很相近,把這一對四邊形放入移除佇列。//計算每個邊角到其他可能標記的最近邊角的平均距離
for(size_t j=i+1;j<possibleMarkers.size();j++)
{
const Marker& m2 = possibleMarkers[j];
float distSquared = 0;
for(int c=0;c<4;c++)
{
Point v = m1.points[c] - m2.points[c];
//向量的點乘-》兩點的距離
distSquared += v.dot(v);
}
distSquared /= 4;
if(distSquared < 100)
{
tooNearCandidates.push_back(pair<int,int>(i,j));
}
}
}
//移除了相鄰的元素對的標識
//計算距離相近的兩個marker內部,四個點的距離和,將距離和較小的,在removlaMask內做標記,即不作為最終的detectedMarkers
vector<bool> removalMask(possibleMarkers.size(),false);//建立Vector物件,並設定容量。第一個引數是容量,第二個是元素。
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]);
}
}
在這裡有個周長的函式:
float perimeter(const vector<Point2f> &a)//求多邊形周長。
{
float sum=0,dx,dy;
for(size_t i=0;i<a.size();i++)
{
size_t i2=(i+1) % a.size();
dx = a[i].x - a[i2].x;
dy = a[i].y - a[i2].y;
sum += sqrt(dx*dx + dy*dy);
}
return sum;
}
現在我們得到了一系列四邊形,並且四個點按逆時針排序,它們可能是標記。下面開始驗證是否為標記。
5:首先為獲得四邊形區域的正面檢視,應該刪除透視投影。
為了得到四邊形變換後的矩形標記影象,必須通過透視變換來變換影象,變換矩陣通過getPerspectiveTransform計算得到。該函式通過四邊形頂點來得到透視變換矩陣。函式的第一個引數為標記在影象空間的座標;第二個引數為方形標記影象四個頂點的座標。
void MarkerDetector::recognizeMarkers(const Mat& grayscale,vector<Marker>& detectedMarkers)
{
Mat canonicalMarkerImage;
char name[20] = "";
vector<Marker> goodMarkers;
/*Identify the markers識別標識 //分析每一個捕獲到的標記,去掉透視投影,得到平面/正面的矩形。
//為了得到這些矩形的標記影象,我們不得不使用透視變換去恢復(unwarp)輸入的影象。這個矩陣應該使用cv::getPerspectiveTransform函式,它首先根據四個對應的點找到透視變換,第一個引數是標記的座標,第二個是正方形標記影象的座標。估算的變換將會把標記轉換成方形,從而方便我們分析。 */
for(size_t i=0;i<detectedMarkers.size();i++)
{
Marker& marker = detectedMarkers[i];
//找到透視轉換矩陣,獲得矩形區域的正面檢視// 找到透視投影,並把標記轉換成矩形,輸入影象四邊形頂點座標,輸出影象的相應的四邊形頂點座標
// Find the perspective transformation that brings current marker to rectangular form
Mat markerTransform = getPerspectiveTransform(marker.points,m_markerCorners2d);//輸入原始影象和變換之後的影象的對應4個點,便可以得到變換矩陣
/* Transform image to get a canonical marker image
// Transform image to get a canonical marker image
//輸入的影象
//輸出的影象
//3x3變換矩陣 */
warpPerspective(grayscale,canonicalMarkerImage,markerTransform,markerSize);//對影象進行透視變換,這就得到和標識影象一致正面的影象,方向可能不同,看四個點如何排列的了。感覺這個變換後,就得到只有標識圖的正面圖
// sprintf(name,"warp_%d.jpg",i);
// imwrite(name,canonicalMarkerImage);
#ifdef SHOW_DEBUG_IMAGES
{
Mat markerImage = grayscale.clone();
marker.drawContour(markerImage);
Mat markerSubImage = markerImage(boundingRect(marker.points));
imshow("Source marker" + ToString(i),markerSubImage);
imwrite("Source marker" + ToString(i) + ".png",markerSubImage);
imshow("Marker " + ToString(i),canonicalMarkerImage);
imwrite("Marker " + ToString(i) + ".png",canonicalMarkerImage);
}
#endif
然後檢查所得的方形影象是否為一個有效的標記影象。為了讓標記只包含黑白兩種顏色,對候選標記區域的灰度圖使用大律OSTU演算法,求取二值化圖,去除灰色畫素,只留下黑白畫素。(之前不用OSTU是大範圍圖片,會影響效能)。Otsu演算法假定影象直方圖呈雙峰分佈,然後搜尋一個閾值,該閾值使得類間(extra-calss)的方差儘可能大,而使類內(inter-class)的方差儘可能小。
int Marker::getMarkerId(Mat& markerImage,int &nRotations)
{
assert(markerImage.rows == markerImage.cols);//如果它的條件返回錯誤,則終止程式執行
assert(markerImage.type() == CV_8UC1);
Mat grey = markerImage;
//Threshold image使用Otsu演算法移除灰色的畫素,只留下黑色和白色畫素。
//這是固定閥值方法
//輸入影象image必須為一個2值單通道影象
//檢測的輪廓陣列,每一個輪廓用一個point型別的vector表示
//閥值
//max_value 使用 CV_THRESH_BINARY 和 CV_THRESH_BINARY_INV 的最大值
//type
threshold(grey,grey,125,255,THRESH_BINARY | THRESH_OTSU);//對候選標記區域的灰度圖使用大律OSTU演算法,求取二值化圖,大範圍圖片用這個演算法會影響效能。
#ifdef SHOW_DEBUG_IMAGES
imshow("Binary marker",grey);
imwrite("Binary marker" + ".png",grey);
#endif
最後進行標記編碼識別。每個標記都有一個內部編碼。標記被分成7x7的網格,其中內部5x5的網格包含ID資訊。其餘部分是黑色邊界。因此首先需要檢查外部黑色邊界是否存在,然後讀取5x5的網格是否存在有效的標記編碼(因為檢測出來的標記可能是旋轉的,要旋轉標記編碼來得到有效的標記編碼)。
每個標記可以劃分成7*7個方格,黑格子表示0,白格子表示1。這樣標記內部將有5個數字,而每個數字由5個bit表示。具體編碼方式類似於海明碼,3個bit用於校驗,2個bit用於存放資料,因此每5個bit可以表達4種資料,而5行這樣的編碼可以表達4^5=1024個數據。如下圖所示:
接下來,咱有必要複習下《計算機組成原理》的海明碼,在唐碩飛老師課本的P100頁儲存器的校驗一節有提到。注意:海明碼只有一位糾錯能力!!
在計算機執行過程中,由於種種原因致使資料在儲存過程中可能出現差錯。為了能及時發現錯誤並糾正錯誤,通常可將原資料配成海明編碼。設欲檢測的二進位制程式碼為n位,為使其具有糾錯能力,需增添k位檢測位,組成n+k位的程式碼。為了能準確對錯誤定位以及指出程式碼沒錯,新增添的檢測位數k應滿足:
稍稍解釋一下,不等式左邊代表該類編碼允許的出錯數量共2k種;不等式右邊,若資料位中有一位出錯,那就有n種可能,若校驗位自身有一位錯誤,那就有k種可能,若完全沒錯,那也是一種可能,因此n+k+1。
設n+k位程式碼自左至右依次編碼為第1,2,3,…,n+k位,而將第k位檢測位記作Ci,分別安插在n+k位程式碼編號的第1,2,4,8,…,2k-1位上。這些檢測位的位置設定是為了保證它們能分別承擔n+k位資訊中不同資料位所組成的“小組“的奇偶檢測任務,使檢測位和它所負責檢測的小組中1的個數為奇數或偶數。以下是根據檢測特性P101規定死的:
C1 檢測的g1小組包含1,3,5,7,9,11,…位
C2 檢測的g2小組包含2,3,6,7,10,11,14,15…位
C4 檢測的g3小組包含4,5,6,7,12,13,14,15…位
海明校驗就是在編碼後,通過故障字的值確定碼子中哪一位發生了錯誤,並將其取反糾正錯誤。
例1:想傳遞資料位0101,則要配備3位校驗位c1c2b4c4b3b2b1,按照配偶原則:
故最終的海明碼即為0100101
例2:已知接收到的海明碼為0110101按照配偶原則,試問想要傳送的資訊是啥?
接收到的7位編碼,包含了3位校驗碼分別在第1,2,4位,首先判斷收到的資訊是否出錯,糾錯過程如下:
所以,P4P2P1=011,第3位出錯,可糾正為0100101,故欲傳遞的資訊為0101.
本書中採用3位校驗碼2位資料碼,則1,2,4位是校驗位,3,5是資料位。同時為了防止將全黑色的四邊形識別成合法的marker,增強演算法魯棒性,修改了3,5位資料校驗的奇偶性。即對於傳遞資料為00的情形C1C2B2C4B1,要避免00000,本來是這樣的:
現在是這樣的,10000
在溫故海明碼後,我們可以識別候選四邊形區域內的資料資訊,確定該四邊形是否為最初定義的Marker。
程式分析:
//在資訊理論中,兩個等長字串之間的漢明距離是兩個字串對應位置的字元不同的個數。換句話說,它就是將一個字串變換成另外一個字串所需要替換的字元個數。
int Marker::hammDistMarker(Mat bits)//對每個可能的標記方向找到海明距離,和參考標識一致的為0,其他旋轉形式的標記不為0,因為經過透射變換後,只能得到四個方向的標記,則旋轉四次,找到和參考標識一致的方向。
{
int ids[4][5]=
{
{1,0,0,0,0},
{1,0,1,1,1},
{0,1,0,0,1},
{0,1,1,1,0}
};
int dist = 0;
for(int y=0;y<5;y++)
{
int minSum = 1e5;//每個元素的海明距離
for(int p=0;p<4;p++)
{
int sum=0;
//now,count
for(int x=0;x<5;x++)
{
sum += bits.at<uchar>(y,x) == ids[p][x]?0:1;
}
if(minSum>sum)
minSum=sum;
}
dist += minSum;
}
return dist;
}
int Marker::mat2id(const Mat& bits)//移位,求或,再移位,得到最終的ID
{
int val=0;
for(int y=0;y<5;y++)
{
val<<=1;//移位操作
if(bits.at<uchar>(y,1)) val |= 1;
val<<=1;
if(bits.at<uchar>(y,3)) val |= 1;
}
return val;
}
首先檢查四邊形輪廓是否完整,即通過統計方塊內非零畫素值個數,若大於方塊內畫素個數的一半,則認為該方塊是白色的。按行遍歷所有輪廓方格,方格大小為100/7,只要有一個輪廓方格被判定為白色,那麼整個輪廓就是不完整的,捨棄該Marker
然後,同理識別5*5編碼區域,將0-1編碼寫入bitMatrix矩陣。由於IPAD拍攝照片存在旋轉變化,因此每個矩形方格具有四種旋轉狀態,即直接從當前矩形區域解碼可能是旋轉過的圖片,不能代表真正的資料。
本文為所有旋轉狀態下的Marker計算海明距離,選擇海明距離最小的作為最終的編碼矩陣。海明距離的計算:
hammDistMarker函式中,ids陣列的由來。咱採用3位校驗2位資料,因此每個stripe的2位資料將產生4種海明編碼。也就是說ids陣列列舉了Marker中每行資料的所有可能取值。
Marker中的一行表示一個數據,我們把bitMatrix的每一行同ids中的一行資料依次比較,總能尋找到ids中最貼近bitMatrix第x行的一行ids。再把bitMatrix對應的ids值求和,即可得到海明距離。
最後,在確定了Marker的旋轉狀態後,mat2id函式對該Marker進行解碼,即遍歷各行,或運算、移位運算得到最終的ID。
//所使用的標記都有一個內部的5x5編碼,採用的是簡單修改的漢明碼。簡單的說,就是5bits中只有2bits被使用,其他三位都是錯誤的識別碼,也就是說我們至多有1024種不同的標識。我們的漢明碼最大的不同是,漢明碼的第一位(奇偶校驗位的3和5)是反向的。所有ID 0(在漢明碼是00000),在這裡是10000,目的是減少環境造成的影響.
//標識被劃分為7x7的網格,內部的5x5表示標識內容,額外的是黑色邊界,接下來是逐個檢查四條邊的畫素是否都是黑色的,若有不是黑色,那麼就不是標識。
int cellSize = markerImage.rows/7;
for(int y=0;y<7;y++)
{
int inc = 6;
if(y == 0 || y == 6) inc=1;//對第一行和最後一行,檢查整個邊界
for(int x=0;x<7;x+=inc)
{
int cellX = x*cellSize;
int cellY = y*cellSize;
Mat cell = grey(Rect(cellX,cellY,cellSize,cellSize));
int nZ = countNonZero(cell);//統計區域內非0的個數。
if(nZ > (cellSize*cellSize)/2)
{
return -1;//如果邊界資訊不是黑色的,就不是一個標識。
}
}
}
Mat bitMatrix = Mat::zeros(5,5,CV_8UC1);
//得到資訊(對於內部的網格,決定是否是黑色或白色的)就是判斷內部5x5的網格都是什麼顏色的,得到一個包含資訊的矩陣bitMatrix。
for(int y=0;y<5;y++)
{
for(int x=0;x<5;x++)
{
int cellX = (x+1)*cellSize;
int cellY = (y+1)*cellSize;
Mat cell = grey(Rect(cellX,cellY,cellSize,cellSize));
int nZ = countNonZero(cell);
if(nZ > (cellSize*cellSize)/2)
bitMatrix.at<uchar>(y,x) = 1;
}
}
//檢查所有的旋轉
Mat rotations[4];
int distances[4];
rotations[0] = bitMatrix;
distances[0] = hammDistMarker(rotations[0]);//求沒有旋轉的矩陣的海明距離。
pair<int,int> minDist(distances[0],0);//把求得的海明距離和旋轉角度作為最小初始值對,每個pair都有兩個屬性值first和second
for(int i=1;i<4;i++)//就是判斷這個矩陣與參考矩陣旋轉多少度。
{
//計算最近的可能元素的海明距離
rotations[i] = rotate(rotations[i-1]);//每次旋轉90度
distances[i] = hammDistMarker(rotations[i]);
if(distances[i] < minDist.first)
{
minDist.first = distances[i];
minDist.second = i;//這個pair的第二個值是代表旋轉幾次,每次90度。
}
}
nRotations = minDist.second;//這個是將返回的旋轉角度值
if(minDist.first == 0)//若海明距離為0,則根據這個旋轉後的矩陣計算標識ID
{
return mat2id(rotations[minDist.second]);
}
return -1;
}
確定了Marker的旋轉狀態後,對四邊形頂點按照旋轉狀態排序,無論相機如何拍攝都使四邊形中間的頂點排在第一個。 而後,通過亞畫素技術cornerSubPix函式對頂點位置進一步細。所謂亞畫素,兩個畫素之間,還存在畫素,它完全由計算得到。
int nRotations;
int id = Marker::getMarkerId(canonicalMarkerImage,nRotations);
cout << "ID: " << id << endl;
if(id!=-1)
{
marker.id = id;
//sort the points so that they are always in the same order no matter the camera orientation
//Rotates the order of the elements in the range [first,last), in such a way that the element pointed by middle becomes the new first element.
//根據相機的旋轉,調整標記的姿態
rotate(marker.points.begin(),marker.points.begin() + 4 - nRotations,marker.points.end());//就是一個迴圈移位
goodMarkers.push_back(marker);
}
}
//refine using subpixel accuracy the corners 是把所有標識的四個頂點都放在一個大的向量中。
if(goodMarkers.size() > 0)
{
//找到所有標記的角點
vector<Point2f> preciseCorners(4*goodMarkers.size());//每個marker四個點
for(size_t i=0;i<goodMarkers.size();i++)
{
Marker& marker = goodMarkers[i];
for(int c=0;c<4;c++)
{
preciseCorners[i*4+c] = marker.points[c];//i表示第幾個marker,c表示某個marker的第幾個點
}
}
//Refines the corner locations.The function iterates to find the sub-pixel accurate location of corners or radial saddle points
//型別
/*
CV_TERMCRIT_ITER 用最大迭代次數作為終止條件
CV_TERMCRIT_EPS 用精度作為迭代條件
CV_TERMCRIT_ITER+CV_TERMCRIT_EPS 用最大迭代次數或者精度作為迭代條件,決定於哪個條件先滿足
//迭代的最大次數
//特定的閥值 */
TermCriteria termCriteria = TermCriteria(TermCriteria::MAX_ITER | TermCriteria::EPS,30,0.01);//這個是迭代終止條件,這裡是達到30次迭代或者達到0.01精度終止。角點精準化迭代過程的終止條件
/*輸入影象
//輸入的角點,也作為輸出更精確的角點
//接近的大小(Neighborhood size)
//Aperture parameter for the Sobel() operator
//畫素迭代(擴張)的方法 */
cornerSubPix(grayscale,preciseCorners,cvSize(5,5),cvSize(-1,-1),termCriteria);//發現亞畫素精度的角點位置,第二個引數代表輸入的角點的初始位置並輸出精準化的座標。在標記檢測的早期的階段沒有使用cornerSubPix函式是因為它的複雜性-呼叫這個函式處理大量頂點時會耗費大量的處理時間,因此我們只在處理有效標記時使用。
//copy back,再把精準化的座標傳給每一個標識。// 儲存最新的頂點
for(size_t i=0;i<goodMarkers.size();i++)
{
Marker& marker = goodMarkers[i];
for(int c=0;c<4;c++)
{
marker.points[c] = preciseCorners[i*4+c];
//cout<<"X:"<<marker.points[c].x<<"Y:"<<marker.points[c].y<<endl;
}
}
}
//畫出細化後的矩形圖片
Mat markerCornersMat(grayscale.size(),grayscale.type());
markerCornersMat = Scalar(0);
for(size_t i=0;i<goodMarkers.size();i++)
{
goodMarkers[i].drawContour(markerCornersMat,Scalar(255));
}
//imshow("Markers refined edges",grayscale*0.5 + markerCornersMat);
//imwrite("Markers refined edges" + ".png",grayscale*0.5 + markerCornersMat);
imwrite("refine.jpg",grayscale*0.5 + markerCornersMat);
detectedMarkers = goodMarkers;
}
在檢測到標記並對標記ID解碼後,需要細化它的角點,此操作最下一步在三維空間估計標記位置很有用。
下一節將分析標記姿態估計。