文章目錄
- ArUco 標記檢測
- 標記與字典
- 標記物創建
- 標記檢測
- 姿態估計
- 選擇字典
- 預定義字典
- 自動生成字典
- 手動定義字典
- 檢測器參數
- 閾值處理
- adaptiveThreshConstant
- 輪廓過濾
- minMarkerPerimeterRate 與 maxMarkerPerimeterRate
- polygonalApproxAccuracyRate
- minCornerDistanceRate
- minMarkerDistanceRate
- minDistanceToBorder
- 比特提取
- markerBorderBits
- minOtsuStdDev
- perspectiveRemovePixelPerCell
- perspectiveRemoveIgnoredMarginPerCell
- 標記識別
- maxErroneousBitsInBorderRate
- errorCorrectionRate
- 角點優化
- cornerRefinementMethod
- cornerRefinementWinSize
- relativeCornerRefinmentWinSize
- cornerRefinementMaxIterations 和 cornerRefinementMinAccuracy
- ArUco 標定板檢測
- 棋盤檢測
- 網格標定板
- 優化標記檢測
- ChArUco 板的檢測
- 目標
- 源代碼
- ChArUco 標定板創建
- ChArUco 棋盤檢測
- ChArUco 姿態估計
- 菱形標記檢測
- ChArUco 菱形標記生成
- ChArUco 菱形標記檢測
- ChArUco 菱形姿態估計
- 使用ArUco和ChArUco進行標定
- 使用 ChArUco 標定板進行校準
- 使用ArUco標定板進行校準
- ArUco 模塊常見問題解答
ArUco 標記檢測
https://docs.opencv.org/4.x/d5/dae/tutorial_aruco_detection.html
下一篇教程: ArUco 標定板檢測
原作者 | Sergio Garrido, Alexander Panov |
兼容性 | OpenCV >= 4.7.0 |
姿態估計在許多計算機視覺應用中至關重要:機器人導航、增強現實等等。該過程基于尋找真實環境中的點與其二維圖像投影之間的對應關系。這通常是一個困難的步驟,因此通常會使用合成標記或基準標記來簡化這一過程。
最流行的方法之一是使用二進制方形基準標記。這些標記的主要優點是單個標記就能提供足夠的對應點(其四個角點)來獲取相機姿態。此外,內部的二進制編碼使其特別魯棒,可以應用錯誤檢測和糾正技術。
aruco 模塊基于 ArUco 庫,這是一個由 Rafael Mu?oz 和 Sergio Garrido 開發的用于檢測方形基準標記的流行庫 [98]`。
aruco 功能包含在:
#include <opencv2/objdetect/aruco_detector.hpp>
標記與字典
ArUco標記是一種由寬黑色邊框和內部二進制矩陣組成的合成方形標記,矩陣決定了其標識符(id)。黑色邊框便于在圖像中快速檢測,二進制編碼則支持標識識別及錯誤檢測與糾正技術的應用。標記尺寸決定了內部矩陣的大小,例如4x4的標記由16位組成。
ArUco標記示例:
標記圖像示例
需注意標記在環境中可能以旋轉狀態出現,但檢測過程需能確定其原始旋轉方向,從而明確識別每個角點。這一過程同樣基于二進制編碼實現。
標記字典是指特定應用中考慮使用的標記集合,本質上是所有標記二進制編碼的列表。字典的主要屬性包括字典大小和標記尺寸:
- 字典大小:組成字典的標記數量
- 標記尺寸:標記的位/模塊數量
aruco模塊預置了多種字典,涵蓋不同字典大小和標記尺寸的組合。
可能有人會認為標記id是二進制編碼轉換為十進制數所得,但實際上對于大尺寸標記,位數過多會導致處理超大數字不切實際。因此,標記id僅表示該標記在其所屬字典中的索引位置。例如字典中前5個標記的id分別為:0、1、2、3和4。
更多關于字典的詳細信息請參閱"選擇字典"章節。
標記物創建
在檢測之前,需要先打印標記物以便放置在環境中。可以使用generateImageMarker()
函數生成標記物圖像。
例如,我們分析以下調用:
cv::Mat markerImage;
cv::aruco::Dictionary dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
cv::aruco::generateImageMarker(dictionary, 23, 200, markerImage, 1);
cv::imwrite("marker23.png", markerImage);
首先,通過選擇 aruco 模塊中的預定義字典之一創建 cv::aruco::Dictionary
對象。具體來說,該字典由 250 個標記組成,每個標記大小為 6x6 位(cv::aruco::DICT_6X6_250
)。
cv::aruco::generateImageMarker()
的參數如下:
-
第一個參數是之前創建的
cv::aruco::Dictionary
對象。 -
第二個參數是標記 ID,此處為字典
cv::aruco::DICT_6X6_250
中的第 23 號標記。注意,每個字典包含的標記數量不同。本例中,有效 ID 范圍為 0 至 249,超出此范圍的 ID 將引發異常。 -
第三個參數 200 表示輸出標記圖像的尺寸。此處輸出圖像大小為 200x200 像素。注意,該參數需足夠大以容納特定字典的位數。例如,無法為 6x6 位的標記生成 5x5 像素的圖像(且未考慮標記邊框)。此外,為避免變形,該參數應與位數加邊框大小成比例,或至少遠大于標記尺寸(如示例中的 200),使變形可忽略不計。
-
第四個參數為輸出圖像。
-
最后一個可選參數用于指定標記黑色邊框的寬度,其值相對于位數比例設定。例如,值 2 表示邊框寬度等于兩個內部位的尺寸,默認值為 1。
生成的圖像如下:
生成的標記
完整示例代碼位于 samples/cpp/tutorial_code/objectDetection/
目錄下的 create_marker.cpp
文件中。
示例現使用 cv::CommandLineParser
從命令行獲取輸入。對于此文件,示例參數如下:
"marker23.png" -d=10 -id=23
create_marker.cpp
的參數:
const char* keys ="{@outfile |res.png| Output image }""{d | 0 | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,""DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, ""DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,""DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}""{cd | | Input file with custom dictionary }""{id | 0 | Marker id in the dictionary }""{ms | 200 | Marker size in pixels }""{bb | 1 | Number of bits in marker borders }""{si | false | show generated image }";
}
標記檢測
給定一張包含ArUco標記的圖像,檢測過程需要返回檢測到的標記列表。每個檢測到的標記包含以下信息:
- 標記四個角點在圖像中的位置(保持原始順序)
- 標記的ID
標記檢測過程主要分為兩個步驟:
-
候選標記檢測:分析圖像以尋找可能成為標記的方形區域。首先通過自適應閾值分割標記,然后從閾值圖像中提取輪廓,丟棄非凸輪廓或不符合方形近似的輪廓。還會進行額外過濾(移除過小或過大的輪廓,消除彼此過于接近的輪廓等)。
-
標記識別:通過分析內部編碼確認候選標記的有效性。首先對每個候選標記應用透視變換獲取標準形式,然后使用Otsu算法對標準圖像進行閾值處理以分離黑白位。根據標記尺寸和邊框大小將圖像劃分為不同單元格,統計每個單元格的黑白像素數量來確定位值。最后分析這些位值,判斷標記是否屬于特定字典。必要時會采用糾錯技術。
參考以下示例圖像:
包含多種標記的圖像
該圖像的打印照片效果:
帶有標記的原始圖像
綠色區域表示檢測到的標記(注意部分標記存在旋轉),紅色小方塊指示標記左上角:
標記檢測結果圖像
粉色區域表示識別階段被拒絕的候選標記:
被拒絕的候選標記圖像
在aruco模塊中,檢測功能通過cv::aruco::ArucoDetector::detectMarkers()
函數實現。這是該模塊的核心功能,其他所有功能都基于此函數返回的檢測結果。
標記檢測示例:
cv::Mat inputImage;
// ... read inputImage ...
std::vector<int> markerIds;
std::vector<std::vector<cv::Point2f>> markerCorners, rejectedCandidates;
cv::aruco::DetectorParameters detectorParams = cv::aruco::DetectorParameters();
cv::aruco::Dictionary dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
cv::aruco::ArucoDetector detector(dictionary, detectorParams);
detector.detectMarkers(inputImage, markerCorners, markerIds, rejectedCandidates);
創建 cv::aruco::ArucoDetector
對象時,需要向構造函數傳遞以下參數:
-
字典對象,此處使用預定義的字典之一 (
cv::aruco::DICT_6X6_250
)。 -
cv::aruco::DetectorParameters
類型對象。該對象包含檢測過程中可自定義的所有參數,這些參數將在下一節詳細說明。
cv::aruco::ArucoDetector::detectMarkers()
的參數包括:
-
第一個參數是包含待檢測標記的圖像。
-
檢測到的標記存儲在
markerCorners
和markerIds
結構中:
-
markerCorners
是檢測到的標記角點列表。每個標記的四個角點按原始順序返回(從左上角開始順時針方向)。因此,第一個角點是左上角,依次是右上角、右下角和左下角。 -
markerIds
是markerCorners
中每個檢測到標記的ID列表。注意返回的markerCorners
和markerIds
向量大小相同。
- 最后一個可選參數
rejectedCandidates
是返回的候選標記列表,即被找到但未通過有效性檢測的形狀。每個候選標記同樣由其四個角點定義,格式與markerCorners
參數相同。該參數可省略,僅用于調試和“重檢測”策略(參見cv::aruco::ArucoDetector::refineDetectedMarkers()
`)。
完成 cv::aruco::ArucoDetector::detectMarkers()
檢測后,通常需要驗證標記是否正確識別。aruco模塊提供了 drawDetectedMarkers()
函數,可在輸入圖像上繪制檢測到的標記。例如:
cv::Mat outputImage = inputImage.clone();
cv::aruco::drawDetectedMarkers(outputImage, markerCorners, markerIds);
-
outputImage
是用于繪制標記的輸入/輸出圖像(通常與檢測到標記的圖像相同)。 -
markerCorners
和markerIds
是由cv::aruco::ArucoDetector::detectMarkers()
函數返回的檢測到的標記結構。
注意:此函數僅用于可視化,可以省略使用。
通過這兩個函數,我們可以創建一個基本的標記檢測循環來從攝像頭檢測標記:
cv::aruco::ArucoDetector detector(dictionary, detectorParams);cv::VideoCapture inputVideo;int waitTime;if(!video.empty()) {inputVideo.open(video);waitTime = 0;} else {inputVideo.open(camId);waitTime = 10;}double totalTime = 0;int totalIterations = 0;// set coordinate systemcv::Mat objPoints(4, 1, CV_32FC3);objPoints.ptr<Vec3f>(0)[0] = Vec3f(-markerLength/2.f, markerLength/2.f, 0);objPoints.ptr<Vec3f>(0)[1] = Vec3f(markerLength/2.f, markerLength/2.f, 0);objPoints.ptr<Vec3f>(0)[2] = Vec3f(markerLength/2.f, -markerLength/2.f, 0);objPoints.ptr<Vec3f>(0)[3] = Vec3f(-markerLength/2.f, -markerLength/2.f, 0);while(inputVideo.grab()) {cv::Mat image, imageCopy;inputVideo.retrieve(image);double tick = (double)getTickCount();vector<int> ids;vector<vector<Point2f> > corners, rejected;// detect markers and estimate posedetector.detectMarkers(image, corners, ids, rejected);size_t nMarkers = corners.size();vector<Vec3d> rvecs(nMarkers), tvecs(nMarkers);if(estimatePose && !ids.empty()) {// Calculate pose for each markerfor (size_t i = 0; i < nMarkers; i++) {solvePnP(objPoints, corners.at(i), camMatrix, distCoeffs, rvecs.at(i), tvecs.at(i));}}double currentTime = ((double)getTickCount() - tick) / getTickFrequency();totalTime += currentTime;totalIterations++;if(totalIterations % 30 == 0) {cout << "Detection Time = " << currentTime * 1000 << " ms "<< "(Mean = " << 1000 * totalTime / double(totalIterations) << " ms)" << endl;}// draw resultsimage.copyTo(imageCopy);if(!ids.empty()) {cv::aruco::drawDetectedMarkers(imageCopy, corners, ids);if(estimatePose) {for(unsigned int i = 0; i < ids.size(); i++)cv::drawFrameAxes(imageCopy, camMatrix, distCoeffs, rvecs[i], tvecs[i], markerLength * 1.5f, 2);}}if(showRejected && !rejected.empty())cv::aruco::drawDetectedMarkers(imageCopy, rejected, noArray(), Scalar(100, 0, 255));imshow("out", imageCopy);char key = (char)waitKey(waitTime);if(key == 27) break;}
注意:部分可選參數已被省略,例如檢測參數對象和被拒絕候選者的輸出向量。
完整可運行示例位于samples/cpp/tutorial_code/objectDetection/
目錄下的detect_markers.cpp
文件中。
現在這些示例使用cv::CommandLineParser
從命令行獲取輸入。對于該文件,示例參數如下所示:
-v=/path_to_opencv/opencv/doc/tutorials/objdetect/aruco_detection/images/singlemarkersoriginal.jpg -d=10
detect_markers.cpp
的參數:
const char* keys ="{d | 0 | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,""DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, ""DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,""DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16,""DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20}""{cd | | Input file with custom dictionary }""{v | | Input from video or image file, if ommited, input comes from camera }""{ci | 0 | Camera id if input doesnt come from video (-v) }""{c | | Camera intrinsic parameters. Needed for camera pose }""{l | 0.1 | Marker side length (in meters). Needed for correct scale in camera pose }""{dp | | File of marker detector parameters }""{r | | show rejected candidates too }""{refine | | Corner refinement: CORNER_REFINE_NONE=0, CORNER_REFINE_SUBPIX=1,""CORNER_REFINE_CONTOUR=2, CORNER_REFINE_APRILTAG=3}";
姿態估計
在檢測到標記后,接下來你可能想利用它們來獲取相機姿態。
要進行相機姿態估計,你需要知道相機的標定參數。這些參數包括相機矩陣和畸變系數。如果你不清楚如何標定相機,可以參考OpenCV的calibrateCamera()
函數和相機標定教程。你也可以按照使用ArUco和ChArUco進行標定教程中的說明,使用aruco模塊來標定相機。注意,除非相機光學組件發生改變(例如調整焦距),否則只需進行一次標定即可。
標定完成后,你將獲得一個相機矩陣:這是一個3x3的矩陣,包含焦距和相機中心坐標(即內參),以及畸變系數:這是一個由5個或更多元素組成的向量,用于模擬相機產生的畸變。
使用ArUco標記進行姿態估計時,可以單獨計算每個標記的姿態。如果你想基于一組標記來估計一個統一姿態,請使用ArUco標定板(參見ArUco標定板檢測教程)。與單標記相比,使用ArUco標定板的優勢在于允許部分標記被遮擋。
相機相對于標記的姿態是一個從標記坐標系到相機坐標系的3D變換,由旋轉向量和平移向量表示。OpenCV提供了cv::solvePnP()
函數來實現這一功能。
Mat camMatrix, distCoeffs;if(estimatePose) {// You can read camera parameters from tutorial_camera_params.ymlreadCameraParamsFromCommandLine(parser, camMatrix, distCoeffs);}
// set coordinate systemcv::Mat objPoints(4, 1, CV_32FC3);objPoints.ptr<Vec3f>(0)[0] = Vec3f(-markerLength/2.f, markerLength/2.f, 0);objPoints.ptr<Vec3f>(0)[1] = Vec3f(markerLength/2.f, markerLength/2.f, 0);objPoints.ptr<Vec3f>(0)[2] = Vec3f(markerLength/2.f, -markerLength/2.f, 0);objPoints.ptr<Vec3f>(0)[3] = Vec3f(-markerLength/2.f, -markerLength/2.f, 0);
vector<int> ids;vector<vector<Point2f> > corners, rejected;// detect markers and estimate posedetector.detectMarkers(image, corners, ids, rejected);size_t nMarkers = corners.size();vector<Vec3d> rvecs(nMarkers), tvecs(nMarkers);if(estimatePose && !ids.empty()) {// Calculate pose for each markerfor (size_t i = 0; i < nMarkers; i++) {solvePnP(objPoints, corners.at(i), camMatrix, distCoeffs, rvecs.at(i), tvecs.at(i));}}
-
corners
參數是由cv::aruco::ArucoDetector::detectMarkers()
函數返回的標記角點向量。 -
第二個參數是標記邊長的實際尺寸(以米或其他單位表示)。注意估計位姿的平移向量將使用相同單位。
-
camMatrix
和distCoeffs
是相機校準過程中生成的相機標定參數。 -
輸出參數
rvecs
和tvecs
分別表示corners
中每個檢測到標記的旋轉向量和平移向量。
該函數假設的標記坐標系默認位于標記中心(或左上角),Z軸向外延伸,如下圖所示。坐標軸顏色對應關系為:X軸紅色,Y軸綠色,Z軸藍色。注意圖中旋轉標記的軸向。
帶坐標軸顯示的圖像
OpenCV 提供了繪制上圖所示坐標軸的功能,可用于驗證位姿估計結果:
// draw resultsimage.copyTo(imageCopy);if(!ids.empty()) {cv::aruco::drawDetectedMarkers(imageCopy, corners, ids);if(estimatePose) {for(unsigned int i = 0; i < ids.size(); i++)cv::drawFrameAxes(imageCopy, camMatrix, distCoeffs, rvecs[i], tvecs[i], markerLength * 1.5f, 2);}}
-
imageCopy
是輸入/輸出圖像,檢測到的標記將在此顯示。 -
camMatrix
和distCoeffs
是相機標定參數。 -
對于每個檢測到的標記,
rvecs[i]
和tvecs[i]
分別表示旋轉向量和平移向量。 -
最后一個參數是軸的長度,單位與 tvec 相同(通常為米)。
示例視頻:
完整示例代碼位于 samples/cpp/tutorial_code/objectDetection/
目錄下的 detect_markers.cpp
文件中。
現在這些示例使用 cv::CommandLineParser
從命令行獲取輸入。對于此文件,示例參數如下:
-v=/path_to_opencv/opencv/doc/tutorials/objdetect/aruco_detection/images/singlemarkersoriginal.jpg -d=10
-c=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_params.yml
detect_markers.cpp
的參數:
const char* keys ="{d | 0 | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,""DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, ""DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,""DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16,""DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20}""{cd | | Input file with custom dictionary }""{v | | Input from video or image file, if ommited, input comes from camera }""{ci | 0 | Camera id if input doesnt come from video (-v) }""{c | | Camera intrinsic parameters. Needed for camera pose }""{l | 0.1 | Marker side length (in meters). Needed for correct scale in camera pose }""{dp | | File of marker detector parameters }""{r | | show rejected candidates too }""{refine | | Corner refinement: CORNER_REFINE_NONE=0, CORNER_REFINE_SUBPIX=1,""CORNER_REFINE_CONTOUR=2, CORNER_REFINE_APRILTAG=3}";
選擇字典
aruco模塊提供了Dictionary
類來表示標記字典。
除了字典中標記的大小和數量外,字典還有另一個重要參數——標記間距離。標記間距離是字典標記之間的最小漢明距離,它決定了字典檢測和糾正錯誤的能力。
一般來說,較小的字典尺寸和較大的標記尺寸會增加標記間距離,反之亦然。然而,由于需要從圖像中提取更多比特位,較大尺寸標記的檢測會更加困難。
例如,如果你的應用只需要10個標記,那么使用僅包含這10個標記的字典比使用包含1000個標記的字典更好。原因是包含10個標記的字典將具有更高的標記間距離,因此對錯誤的魯棒性更強。
因此,aruco模塊提供了多種選擇標記字典的方法,以提高系統的魯棒性:
預定義字典
這是選擇字典最簡單的方式。aruco模塊包含了一系列預定義的字典,涵蓋不同標記尺寸和標記數量。例如:
cv::aruco::Dictionary dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
cv::aruco::DICT_6X6_250
是一個預定義的標記字典示例,采用6x6位編碼,共包含250個標記。
在提供的所有字典中,建議選擇能滿足應用需求的最小字典。例如,若需要200個6x6位標記,使用cv::aruco::DICT_6X6_250
比cv::aruco::DICT_6X6_1000
更合適。字典越小,標記間的區分距離越大。
預定義字典的完整列表可查閱PredefinedDictionaryType
枚舉類型的文檔。
自動生成字典
可以自動生成字典來調整所需的標記數量和位數,以優化標記間的距離:
cv::aruco::Dictionary dictionary = cv::aruco::extendDictionary(36, 5);
這將生成一個由36個5x5位標記組成的自定義字典。生成過程可能需要幾秒鐘,具體時間取決于參數設置(字典越大、位數越多,速度越慢)。
此外,您可以使用opencv/samples/cpp
目錄中的aruco_dict_utils.cpp
示例程序。該示例會計算生成字典的最小漢明距離,并允許您創建具有反射抗性的標記。
手動定義字典
最后,字典可以手動配置,以便使用任何編碼方式。為此,需要手動設置 cv::aruco::Dictionary
對象的參數。需要注意的是,除非有特殊理由需要手動操作,否則建議優先使用之前提到的其他方法。
cv::aruco::Dictionary
的參數包括:
class Dictionary {
public:cv::Mat bytesList; // marker code informationint markerSize; // number of bits per dimensionint maxCorrectionBits; // maximum number of bits that can be corrected...}
bytesList
是包含所有標記碼信息的數組。markerSize
表示每個標記的尺寸(例如,5 代表 5x5 位的標記)。最后,maxCorrectionBits
是在標記檢測過程中可糾正的最大錯誤位數。如果該值設置過高,可能導致大量誤報。
bytesList
中的每一行代表字典中的一個標記。不過,這些標記并非以二進制形式存儲,而是采用特殊格式存儲以簡化檢測過程。
幸運的是,通過靜態方法 Dictionary::getByteListFromBits()
可以輕松將標記轉換為此格式。
例如:
cv::aruco::Dictionary dictionary;// Markers of 6x6 bits
dictionary.markerSize = 6;// Maximum number of bit corrections
dictionary.maxCorrectionBits = 3;// Let's create a dictionary of 100 markers
for(int i = 0; i < 100; i++)
{// Assume generateMarkerBits() generates a new marker in binary format, so that// markerBits is a 6x6 matrix of CV_8UC1 type, only containing 0s and 1scv::Mat markerBits = generateMarkerBits();cv::Mat markerCompressed = cv::aruco::Dictionary::getByteListFromBits(markerBits);// Add the marker as a new rowdictionary.bytesList.push_back(markerCompressed);
}
檢測器參數
cv::aruco::ArucoDetector
的一個參數是cv::aruco::DetectorParameters
對象。該對象包含了標記檢測過程中可自定義的所有選項。
本節將詳細說明每個檢測器參數。這些參數可根據其涉及的處理流程進行分類:
閾值處理
標記檢測流程的第一步是對輸入圖像進行自適應閾值處理。
例如,前文示例圖像經過閾值處理后的效果如下:
可通過以下參數自定義閾值處理過程:
adaptiveThreshWinSizeMin、adaptiveThreshWinSizeMax 和 adaptiveThreshWinSizeStep
參數 adaptiveThreshWinSizeMin
和 adaptiveThreshWinSizeMax
表示自適應閾值處理中窗口尺寸(以像素為單位)的選擇范圍(詳見 OpenCV 的 threshold()
和 adaptiveThreshold()
函數說明)。
參數 adaptiveThreshWinSizeStep
表示窗口尺寸從 adaptiveThreshWinSizeMin
到 adaptiveThreshWinSizeMax
的遞增步長。
例如,當設置 adaptiveThreshWinSizeMin
= 5、adaptiveThreshWinSizeMax
= 21 且 adaptiveThreshWinSizeStep
= 4 時,系統會以 5、9、13、17 和 21 像素的窗口尺寸分 5 步進行閾值處理。每步處理后都會提取候選標記。
若標記尺寸過大而窗口尺寸過小,可能導致標記邊框斷裂而無法檢測,如下圖所示:
反之,若標記過小而窗口尺寸過大,同樣會導致檢測失敗,還會降低處理性能。此外,過大的窗口會使處理過程趨近于全局閾值化,喪失自適應優勢。
最簡單的情況是將 adaptiveThreshWinSizeMin
和 adaptiveThreshWinSizeMax
設為相同值,此時僅執行單次閾值處理。但通常建議設置窗口尺寸范圍,不過過多的閾值處理步驟也會顯著影響性能。
另請參閱:
cv::aruco::DetectorParameters::adaptiveThreshWinSizeMin
cv::aruco::DetectorParameters::adaptiveThreshWinSizeMax
cv::aruco::DetectorParameters::adaptiveThreshWinSizeStep
adaptiveThreshConstant
adaptiveThreshConstant
參數表示在閾值化操作中添加的常數值(更多細節請參考 OpenCV 的 threshold()
和 adaptiveThreshold()
函數)。在大多數情況下,其默認值是一個不錯的選擇。
另請參閱 cv::aruco::DetectorParameters::adaptiveThreshConstant
輪廓過濾
經過閾值處理后,系統會檢測出輪廓。但并非所有輪廓都被視為標記候選對象。它們會通過多個步驟進行過濾,以剔除極不可能是標記的輪廓。本節參數用于自定義這一過濾過程。
需要注意的是,在大多數情況下,這實際上是檢測能力與性能之間的平衡問題。所有被保留的輪廓都會進入后續處理階段,而這些階段通常具有更高的計算成本。因此,相比后期階段,更建議在當前階段就剔除無效候選對象。
另一方面,如果過濾條件過于嚴格,真實的標記輪廓可能會被錯誤剔除,從而導致檢測失敗。
minMarkerPerimeterRate 與 maxMarkerPerimeterRate
這兩個參數用于確定標記的最小和最大尺寸,具體指標記輪廓的最小和最大周長。它們不以絕對像素值指定,而是相對于輸入圖像的最大維度進行設置。
例如,對于640x480尺寸的圖像,若最小相對標記周長為0.05,則最小標記周長計算為640×0.05=32像素(因為640是圖像的最大維度)。maxMarkerPerimeterRate
參數同理適用。
若minMarkerPerimeterRate
設置過低,檢測性能會顯著下降,因為后續處理階段需要評估的輪廓數量將大幅增加。這種負面影響在maxMarkerPerimeterRate
參數上不太明顯,因為圖像中通常存在更多小輪廓而非大輪廓。當minMarkerPerimeterRate
設為0且maxMarkerPerimeterRate
設為4(或更大)時,相當于處理圖像中的所有輪廓,但出于性能考慮不建議這樣做。
另請參閱:cv::aruco::DetectorParameters::minMarkerPerimeterRate
、cv::aruco::DetectorParameters::maxMarkerPerimeterRate
polygonalApproxAccuracyRate
對每個候選區域進行多邊形近似處理,僅接受近似為正方形形狀的候選。該參數決定了多邊形近似所能產生的最大誤差(更多信息請參閱 approxPolyDP()
函數)。
此參數相對于候選區域的長度(以像素為單位)。例如,若候選區域的周長為100像素且 polygonalApproxAccuracyRate
值為0.04,則最大誤差為100×0.04=4像素。
在大多數情況下,默認值效果良好,但對于高度畸變的圖像可能需要更大的誤差值。
另請參閱 cv::aruco::DetectorParameters::polygonalApproxAccuracyRate
minCornerDistanceRate
同一標記內任意兩個角點之間的最小距離。該值相對于標記周長表示,實際最小像素距離為周長乘以 minCornerDistanceRate。
另請參閱 cv::aruco::DetectorParameters::minCornerDistanceRate
minMarkerDistanceRate
兩個不同標記中任意角點對之間的最小距離。該值以兩個標記中最小周長的相對比例表示。如果兩個候選標記距離過近,較小的那個將被忽略。
另請參閱 cv::aruco::DetectorParameters::minMarkerDistanceRate
minDistanceToBorder
標記任意角點到圖像邊框的最小距離(以像素為單位)。即使標記部分被圖像邊框遮擋,只要遮擋范圍較小,仍能正確檢測。然而,若某個角點被遮擋,返回的角點坐標通常會錯誤地定位在圖像邊框附近。
如果標記角點位置至關重要(例如進行姿態估計時),建議直接舍棄那些角點過于接近圖像邊框的標記。其他應用場景下則無需此操作。
另請參閱 cv::aruco::DetectorParameters::minDistanceToBorder
比特提取
在完成候選檢測后,系統會分析每個候選對象的比特位以判斷它們是否為有效標記。
在分析二進制代碼本身之前,需要先提取比特位。這一過程包括校正透視畸變,并使用大津閾值法對處理后的圖像進行二值化,以區分黑白像素。
下圖展示了一個標記在消除透視畸變后獲得的圖像示例:
透視校正
接著,將圖像劃分為與標記比特數相同的網格單元。每個單元中統計黑白像素的數量,根據多數原則確定該單元對應的比特值:
標記單元
該過程支持通過以下參數進行自定義調整:
markerBorderBits
該參數表示標記邊框的寬度,其數值相對于每個比特的大小。例如,值為2表示邊框寬度等于兩個內部比特的寬度。
使用時,此參數需與所采用標記的實際邊框尺寸保持一致。邊框尺寸可通過標記繪制函數(如generateImageMarker()
)進行配置。
另請參閱 cv::aruco::DetectorParameters::markerBorderBits
minOtsuStdDev
該值決定了執行Otsu閾值分割時像素值的最小標準差。如果標準差較低,很可能意味著整個區域都是黑色(或白色),此時應用Otsu算法沒有意義。在這種情況下,所有比特位將根據平均值是否高于或低于128被設為0(或1)。
另請參閱 cv::aruco::DetectorParameters::minOtsuStdDev
perspectiveRemovePixelPerCell
該參數決定了在矯正透視畸變后(包括邊框),所獲得圖像中每個單元格對應的像素數量。這對應于上圖中紅色方塊的大小。
舉例來說,假設我們處理的是5x5比特的標記,且邊框大小為1比特(參見markerBorderBits
)。那么,每個維度上的總單元格/比特數為5 + 2*1 = 7(邊框需要計算兩次)。總單元格數為7x7。
如果perspectiveRemovePixelPerCell
的值設為10,那么最終獲得的圖像大小將是10*7 = 70,即70x70像素。
提高此參數的值可以在一定程度上改善比特提取過程,但也可能影響性能表現。
另請參閱cv::aruco::DetectorParameters::perspectiveRemovePixelPerCell
perspectiveRemoveIgnoredMarginPerCell
在提取每個單元格的比特位時,會統計黑色和白色像素的數量。通常不建議考慮所有單元格像素,更好的做法是忽略單元格邊緣的部分像素。
這樣做的原因是,在消除透視畸變后,單元格的顏色通常無法完美分離,白色單元格可能會侵入黑色單元格的部分像素(反之亦然)。因此,忽略部分像素可以避免統計錯誤的像素。
例如,在下圖中:
標記單元格邊緣
只有綠色方框內的像素會被計入。從右圖可以看出,處理后的像素減少了相鄰單元格帶來的噪聲。perspectiveRemoveIgnoredMarginPerCell
參數表示紅色方框與綠色方框之間的差異范圍。
該參數相對于單元格的總大小。例如,如果單元格大小為40像素且此參數值為0.1,則每個單元格會忽略40*0.1=4像素的邊緣。這意味著實際分析的每個單元格像素總數將是32x32,而非40x40。
另請參閱 cv::aruco::DetectorParameters::perspectiveRemoveIgnoredMarginPerCell
標記識別
在提取比特位后,下一步是檢查提取的代碼是否屬于標記字典。如有必要,可執行糾錯操作。
maxErroneousBitsInBorderRate
標記邊框的比特位應為黑色。此參數指定了邊框允許的錯誤比特數上限,即邊框內白色比特的最大數量。該數值以相對于標記總比特數的比率形式表示。
另請參閱 cv::aruco::DetectorParameters::maxErroneousBitsInBorderRate
errorCorrectionRate
每個標記字典都有一個理論上的最大可糾正比特數(Dictionary.maxCorrectionBits
)。但該值可通過 errorCorrectionRate
參數進行調整。
例如,若所用字典允許的最大可糾正比特數為6,且 errorCorrectionRate
值為0.5,則實際最大可糾正比特數為6*0.5=3比特。
此參數可用于降低糾錯能力,從而避免誤判。
另請參閱 cv::aruco::DetectorParameters::errorCorrectionRate
角點優化
在檢測并識別出標記后,最后一步是對角點位置進行亞像素級優化(參見OpenCV的cornerSubPix()
函數和cv::aruco::CornerRefineMethod
)。
注意:此步驟是可選的,僅當需要精確獲取標記角點位置(例如用于姿態估計)時才適用。該步驟通常較為耗時,因此默認處于禁用狀態。
cornerRefinementMethod
該參數決定是否執行角點亞像素優化處理,以及執行時采用的具體方法。如果不需要精確的角點檢測,可以禁用此功能。可選值包括 CORNER_REFINE_NONE
、CORNER_REFINE_SUBPIX
、CORNER_REFINE_CONTOUR
和 CORNER_REFINE_APRILTAG
。
另請參閱 cv::aruco::DetectorParameters::cornerRefinementMethod
cornerRefinementWinSize
該參數用于確定角點細化處理過程中的最大窗口尺寸。
設置過高的值可能導致圖像中相鄰的角點被包含在同一窗口區域內,從而使得標記的角點在此過程中移動到錯誤的位置。此外,這還可能影響處理性能。若ArUco標記尺寸過小,窗口尺寸會自動減小,具體可參考cv::aruco::DetectorParameters::relativeCornerRefinmentWinSize
。最終窗口尺寸按以下公式計算:min(cornerRefinementWinSize, averageArucoModuleSize*relativeCornerRefinmentWinSize),其中averageArucoModuleSize表示ArUco標記模塊的平均像素尺寸。
另請參閱cv::aruco::DetectorParameters::cornerRefinementWinSize
relativeCornerRefinmentWinSize
用于角點優化的動態窗口大小,相對于ArUco模塊尺寸(默認值為0.3)。
最終窗口大小的計算公式為:min(cornerRefinementWinSize, averageArucoModuleSize*relativeCornerRefinmentWinSize),其中averageArucoModuleSize表示ArUco標記的平均模塊尺寸(以像素為單位)。當標記之間距離較遠時,建議將該參數值增大至0.4-0.5;當標記密集分布時,建議減小至0.1-0.2。
另請參閱 cv::aruco::DetectorParameters::relativeCornerRefinmentWinSize
cornerRefinementMaxIterations 和 cornerRefinementMinAccuracy
這兩個參數決定了亞像素細化過程的停止條件。cornerRefinementMaxIterations
表示最大迭代次數,cornerRefinementMinAccuracy
表示停止過程前的最小誤差值。
如果迭代次數過高,可能會影響性能。反之,如果過低,則可能導致亞像素細化效果不佳。
另請參閱 cv::aruco::DetectorParameters::cornerRefinementMaxIterations
和 cv::aruco::DetectorParameters::cornerRefinementMinAccuracy
生成于 2025 年 4 月 30 日星期三 23:08:42,由 doxygen 1.12.0 為 OpenCV 生成
ArUco 標定板檢測
https://docs.opencv.org/4.x/db/da9/tutorial_aruco_board_detection.html
上一教程: ArUco 標記檢測
下一教程: ChArUco 標定板檢測
原作者 | Sergio Garrido, Alexander Panov |
兼容性 | OpenCV >= 4.7.0 |
ArUco 標定板是由多個標記組成的集合,其功能類似于單個標記,能夠為相機提供單一的位姿信息。
最常見的標定板類型是所有標記都位于同一平面的布局,這種設計便于直接打印:
但標定板的排列方式并不局限于此,它可以呈現任何二維或三維的布局結構。
標定板與獨立標記組的本質區別在于:標定板中標記之間的相對位置是預先已知的。這一特性使得所有標記的角點都能用于計算相機相對于整個標定板的位姿。
當使用獨立標記組時,由于無法獲知標記在環境中的相對位置,只能單獨計算每個標記的位姿。
使用標定板的主要優勢包括:
- 位姿估計更具靈活性。只需檢測到部分標記即可完成位姿計算,因此在存在遮擋或局部視野的情況下仍能工作
- 通常能獲得更高精度的位姿結果,因為采用了更多點對應關系(標記角點)進行計算
棋盤檢測
棋盤檢測與標準標記檢測類似,唯一區別在于姿態估計步驟。實際上,使用標記棋盤時,需要先完成標準標記檢測,才能估計棋盤姿態。
執行棋盤姿態估計時,應使用solvePnP()
函數,如下方samples/cpp/tutorial_code/objectDetection/detect_board.cpp
所示。
int markersX = parser.get<int>("w");int markersY = parser.get<int>("h");float markerLength = parser.get<float>("l");float markerSeparation = parser.get<float>("s");bool showRejected = parser.has("r");bool refindStrategy = parser.has("rs");int camId = parser.get<int>("ci");Mat camMatrix, distCoeffs;readCameraParamsFromCommandLine(parser, camMatrix, distCoeffs);aruco::Dictionary dictionary = readDictionatyFromCommandLine(parser);aruco::DetectorParameters detectorParams = readDetectorParamsFromCommandLine(parser);String video;if(parser.has("v")) {video = parser.get<String>("v");}if(!parser.check()) {parser.printErrors();return 0;}aruco::ArucoDetector detector(dictionary, detectorParams);VideoCapture inputVideo;int waitTime;if(!video.empty()) {inputVideo.open(video);waitTime = 0;} else {inputVideo.open(camId);waitTime = 10;}float axisLength = 0.5f * ((float)min(markersX, markersY) * (markerLength + markerSeparation) +markerSeparation);// Create GridBoard objectaruco::GridBoard board(Size(markersX, markersY), markerLength, markerSeparation, dictionary);// Also you could create Board object//vector<vector<Point3f> > objPoints; // array of object points of all the marker corners in the board//vector<int> ids; // vector of the identifiers of the markers in the board//aruco::Board board(objPoints, dictionary, ids);double totalTime = 0;int totalIterations = 0;while(inputVideo.grab()) {Mat image, imageCopy;inputVideo.retrieve(image);double tick = (double)getTickCount();vector<int> ids;vector<vector<Point2f>> corners, rejected;Vec3d rvec, tvec;// Detect markersdetector.detectMarkers(image, corners, ids, rejected);// Refind strategy to detect more markersif(refindStrategy)detector.refineDetectedMarkers(image, board, corners, ids, rejected, camMatrix,distCoeffs);// Estimate board poseint markersOfBoardDetected = 0;if(!ids.empty()) {// Get object and image points for the solvePnP functioncv::Mat objPoints, imgPoints;board.matchImagePoints(corners, ids, objPoints, imgPoints);// Find posecv::solvePnP(objPoints, imgPoints, camMatrix, distCoeffs, rvec, tvec);markersOfBoardDetected = (int)objPoints.total() / 4;}double currentTime = ((double)getTickCount() - tick) / getTickFrequency();totalTime += currentTime;totalIterations++;if(totalIterations % 30 == 0) {cout << "Detection Time = " << currentTime * 1000 << " ms "<< "(Mean = " << 1000 * totalTime / double(totalIterations) << " ms)" << endl;}// Draw resultsimage.copyTo(imageCopy);if(!ids.empty())aruco::drawDetectedMarkers(imageCopy, corners, ids);if(showRejected && !rejected.empty())aruco::drawDetectedMarkers(imageCopy, rejected, noArray(), Scalar(100, 0, 255));if(markersOfBoardDetected > 0)cv::drawFrameAxes(imageCopy, camMatrix, distCoeffs, rvec, tvec, axisLength);imshow("out", imageCopy);char key = (char)waitKey(waitTime);if(key == 27) break;
參數說明:
-
objPoints
和imgPoints
:對象點和圖像點,需與cv::aruco::GridBoard::matchImagePoints()
匹配。該函數需要輸入從cv::aruco::ArucoDetector::detectMarkers()
函數檢測到的標記角點markerCorners
和標記IDmarkerIds
結構。 -
board
:定義標定板布局及其ID的cv::aruco::Board
對象 -
cameraMatrix
和distCoeffs
:姿態估計所需的相機標定參數 -
rvec
和tvec
:標定板的估計姿態。若非空則作為初始猜測值 -
函數返回值:用于估計標定板姿態的標記總數
可通過 drawFrameAxes()
函數驗證獲得的姿態,例如:
帶坐標軸的標定板
另一個部分遮擋的標定板示例:
存在遮擋的標定板
如圖所示,即使部分標記未被檢測到,仍可通過其余標記估計標定板姿態。
示例視頻:
完整示例代碼位于 samples/cpp/tutorial_code/objectDetection/detect_board.cpp
中。
示例程序現通過 cv::CommandLineParser
接收命令行參數,該文件的示例參數如下:
-w=5 -h=7 -l=100 -s=10
-v=/path_to_opencv/opencv/doc/tutorials/objdetect/aruco_board_detection/gboriginal.jpg
-c=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_params.yml
-cd=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_dict.ymlParameters for `detect_board.cpp`: const char* keys ="{w | | Number of squares in X direction }""{h | | Number of squares in Y direction }""{l | | Marker side length (in pixels) }""{s | | Separation between two consecutive markers in the grid (in pixels)}""{d | | dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,""DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, ""DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,""DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}""{cd | | Input file with custom dictionary }""{c | | Output file with calibrated camera parameters }""{v | | Input from video or image file, if omitted, input comes from camera }""{ci | 0 | Camera id if input doesnt come from video (-v) }""{dp | | File of marker detector parameters }""{rs | | Apply refind strategy }""{r | | show rejected candidates too }";
}
網格標定板
創建 cv::aruco::Board
對象需要為環境中的每個標記指定角點位置。但在許多情況下,標定板只是同一平面上按網格布局排列的一組標記,因此可以輕松打印和使用。
幸運的是,aruco模塊提供了便捷創建和打印這類標記的基礎功能。
cv::aruco::GridBoard
是一個繼承自 cv::aruco::Board
的特化類,它表示所有標記位于同一平面且呈網格布局的標定板,如下圖所示:
ArUco標定板圖像
具體而言,網格標定板的坐標系位于標定板平面內,以板面左下角為中心,Z軸向外延伸,如下圖所示(X軸:紅色,Y軸:綠色,Z軸:藍色):
帶坐標軸的標定板
可以通過以下參數定義 cv::aruco::GridBoard
對象:
- X方向的標記數量
- Y方向的標記數量
- 標記邊長
- 標記間隔距離
- 標記使用的字典
- 所有標記的ID(共X*Y個標記)
使用 cv::aruco::GridBoard
構造函數可以輕松根據這些參數創建對象:
aruco::GridBoard board(Size(markersX, markersY), markerLength, markerSeparation, dictionary);
- 前兩個參數分別表示X和Y方向的標記數量
- 第三和第四個參數分別表示標記長度和間隔距離。可使用任意單位,但需注意該標定板的估計位姿將使用相同單位(通常以米為單位)
- 最后提供標記使用的字典
因此,該標定板將包含5x7=35個標記。默認情況下,每個標記的ID按升序從0開始分配,即0, 1, 2, …, 34。
創建網格標定板后,通常需要打印并使用。有兩種實現方式:
- 使用腳本
doc/patter_tools/gen_pattern.py
,參見創建校準圖案 - 使用函數
cv::aruco::GridBoard::generateImage()
cv::aruco::GridBoard
類提供的生成函數可通過以下代碼調用:
Mat boardImage;
board.generateImage(imageSize, boardImage, margins, borderBits);
- 第一個參數是輸出圖像的像素尺寸(本例為600x500像素)。如果尺寸與標定板長寬比不符,圖像將居中顯示
boardImage
:輸出帶標定板的圖像- 第三個參數是(可選)邊距像素值,確保標記不接觸圖像邊界(本例邊距為10)
- 最后是標記邊框大小,與
generateImageMarker()
函數類似,默認值為1
完整的工作示例見 samples/cpp/tutorial_code/objectDetection/create_board.cpp
輸出圖像效果如下:
示例現在通過 cv::CommandLineParser
接收命令行參數。該文件的示例參數如下:
"_output_path_/aboard.png" -w=5 -h=7 -l=100 -s=10 -d=10
優化標記檢測
ArUco標定板還可用于提升標記檢測效果。當我們已檢測到屬于標定板的部分標記時,可以利用這些標記和標定板的布局信息,嘗試找出之前未被檢測到的標記。
這一功能可通過調用cv::aruco::refineDetectedMarkers()
函數實現,該函數應在cv::aruco::ArucoDetector::detectMarkers()
之后調用。
該函數的主要參數包括:檢測到標記的原始圖像、標定板對象、已檢測到的標記角點、已檢測到的標記ID,以及被拒絕的標記角點。
被拒絕的角點可從cv::aruco::ArucoDetector::detectMarkers()
函數獲取,這些角點也被稱為候選標記。這些候選標記是在原始圖像中找到的方形區域,但由于未能通過識別步驟(例如其內部編碼存在過多錯誤)而未被確認為有效標記。
然而,這些候選標記有時可能是實際存在的標記,只是由于圖像噪聲過大、分辨率過低或其他影響二進制編碼提取的問題而未被正確識別。cv::aruco::ArucoDetector::refineDetectedMarkers()
函數會在這些候選標記與標定板缺失的標記之間建立對應關系。這一搜索過程基于兩個參數:
-
候選標記與缺失標記投影之間的距離。要獲取這些投影,必須至少檢測到標定板上的一個標記。如果提供了相機參數(相機矩陣和畸變系數),則使用這些參數計算投影。否則,將通過局部單應性變換計算投影,此時僅允許使用平面標定板(即所有標記角點的Z坐標應相同)。
refineDetectedMarkers()
中的minRepDistance
參數決定了候選角點與投影標記角點之間的最小歐氏距離(默認值為10)。 -
二進制編碼。如果候選標記滿足最小距離條件,將再次分析其內部比特以確定是否為實際投影標記。不過在這種情況下,條件相對寬松,允許的錯誤比特數可以更多。這由
errorCorrectionRate
參數控制(默認值為3.0)。如果該參數為負值,則完全不分析內部比特,僅評估角點距離。
以下是使用cv::aruco::ArucoDetector::refineDetectedMarkers()
函數的示例:
// Detect markersdetector.detectMarkers(image, corners, ids, rejected);// Refind strategy to detect more markersif(refindStrategy)detector.refineDetectedMarkers(image, board, corners, ids, rejected, camMatrix, distCoeffs);
還需注意的是,在某些情況下,如果初始檢測到的標記點數量過少(例如僅1或2個標記點),缺失標記點的投影質量可能較差,從而導致錯誤的對應關系。
具體實現細節請參考模塊示例。
生成于 2025年4月30日 星期三 23:08:42,由 doxygen 1.12.0 為 OpenCV 生成
ChArUco 板的檢測
https://docs.opencv.org/4.x/df/d4a/tutorial_charuco_detection.html
上一篇教程: ArUco板的檢測
下一篇教程: 菱形標記的檢測
ArUco標記和板因其快速檢測和多功能性而非常實用。然而,ArUco標記的一個問題是,即使在應用亞像素細化后,其角點位置的精度也不夠高。
相反,棋盤圖案的角點可以更精確地細化,因為每個角點都被兩個黑色方塊包圍。但尋找棋盤圖案不如尋找ArUco板靈活:它必須完全可見且不允許遮擋。
ChArUco板試圖結合這兩種方法的優點:
ChArUco定義
ArUco部分用于插值棋盤角點的位置,因此它具有標記板的靈活性,允許遮擋或部分可見。此外,由于插值的角點屬于棋盤,它們在亞像素精度方面非常準確。
當需要高精度時(例如相機校準),ChArUco板比標準ArUco板是更好的選擇。
目標
在本教程中,你將學習:
- 如何創建一個 charuco 棋盤?
- 如何在不進行相機校準的情況下檢測 charuco 角點?
- 如何通過相機校準和姿態估計來檢測 charuco 角點?
源代碼
你可以在 samples/cpp/tutorial_code/objectDetection/detect_board_charuco.cpp
中找到這段代碼。
以下是一個示例代碼,展示了如何實現目標列表中列出的所有功能。
int squaresX = parser.get<int>("w");int squaresY = parser.get<int>("h");float squareLength = parser.get<float>("sl");float markerLength = parser.get<float>("ml");bool refine = parser.has("rs");int camId = parser.get<int>("ci");string video;if(parser.has("v")) {video = parser.get<string>("v");}Mat camMatrix, distCoeffs;readCameraParamsFromCommandLine(parser, camMatrix, distCoeffs);aruco::DetectorParameters detectorParams = readDetectorParamsFromCommandLine(parser);aruco::Dictionary dictionary = readDictionatyFromCommandLine(parser);if(!parser.check()) {parser.printErrors();return 0;}VideoCapture inputVideo;int waitTime = 0;if(!video.empty()) {inputVideo.open(video);} else {inputVideo.open(camId);waitTime = 10;}float axisLength = 0.5f * ((float)min(squaresX, squaresY) * (squareLength));// create charuco board objectaruco::CharucoBoard charucoBoard(Size(squaresX, squaresY), squareLength, markerLength, dictionary);// create charuco detectoraruco::CharucoParameters charucoParams;charucoParams.tryRefineMarkers = refine; // if tryRefineMarkers, refineDetectedMarkers() will be used in detectBoard()charucoParams.cameraMatrix = camMatrix; // cameraMatrix can be used in detectBoard()charucoParams.distCoeffs = distCoeffs; // distCoeffs can be used in detectBoard()aruco::CharucoDetector charucoDetector(charucoBoard, charucoParams, detectorParams);double totalTime = 0;int totalIterations = 0;while(inputVideo.grab()) {Mat image, imageCopy;inputVideo.retrieve(image);double tick = (double)getTickCount();vector<int> markerIds, charucoIds;vector<vector<Point2f> > markerCorners;vector<Point2f> charucoCorners;Vec3d rvec, tvec;// detect markers and charuco cornerscharucoDetector.detectBoard(image, charucoCorners, charucoIds, markerCorners, markerIds);// estimate charuco board posebool validPose = false;if(camMatrix.total() != 0 && distCoeffs.total() != 0 && charucoIds.size() >= 4) {Mat objPoints, imgPoints;charucoBoard.matchImagePoints(charucoCorners, charucoIds, objPoints, imgPoints);validPose = solvePnP(objPoints, imgPoints, camMatrix, distCoeffs, rvec, tvec);}double currentTime = ((double)getTickCount() - tick) / getTickFrequency();totalTime += currentTime;totalIterations++;if(totalIterations % 30 == 0) {cout << "Detection Time = " << currentTime * 1000 << " ms "<< "(Mean = " << 1000 * totalTime / double(totalIterations) << " ms)" << endl;}// draw resultsimage.copyTo(imageCopy);if(markerIds.size() > 0) {aruco::drawDetectedMarkers(imageCopy, markerCorners);}if(charucoIds.size() > 0) {aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(255, 0, 0));}if(validPose)cv::drawFrameAxes(imageCopy, camMatrix, distCoeffs, rvec, tvec, axisLength);imshow("out", imageCopy);if(waitKey(waitTime) == 27) break;}
ChArUco 標定板創建
aruco 模塊提供了 cv::aruco::CharucoBoard
類來表示 ChArUco 標定板,該類繼承自 cv::aruco::Board
類。
與 ChArUco 的其他功能一樣,這個類定義在:
#include <opencv2/objdetect/charuco_detector.hpp>
定義 cv::aruco::CharucoBoard
需要以下參數:
- 棋盤在 X 和 Y 方向上的方格數量
- 方格的邊長
- 標記的邊長
- 標記的字典
- 所有標記的 ID
與 cv::aruco::GridBoard
類似,aruco 模塊提供了便捷的方式來創建 cv::aruco::CharucoBoard
。通過 cv::aruco::CharucoBoard
構造函數可以輕松根據這些參數創建對象:
aruco::Dictionary
dictionary = readDictionatyFromCommandLine(parser);
cv::aruco::CharucoBoard
board(Size
(squaresX, squaresY), (float)squareLength, (float)markerLength, dictionary);
- 第一個參數分別表示 X 和 Y 方向上的方格數量
- 第二和第三個參數分別是方格和標記的邊長。可以使用任何單位,但需注意該棋盤的估計位姿將使用相同單位(通常使用米)
- 最后提供標記的字典
默認情況下,每個標記的 ID 按升序從 0 開始分配,與 cv::aruco::GridBoard
構造函數相同。可以通過 board.ids
訪問 ID 向量來自定義,如父類 cv::aruco::Board
所示。
創建 cv::aruco::CharucoBoard
對象后,可以生成圖像進行打印。有兩種方法實現:
- 使用腳本
doc/patter_tools/gen_pattern.py
,參見Create calibration pattern
- 使用函數
cv::aruco::CharucoBoard::generateImage()
cv::aruco::CharucoBoard
類提供了 cv::aruco::CharucoBoard::generateImage()
函數,可通過以下代碼調用:
Mat boardImage;Size imageSize;imageSize.width = squaresX * squareLength + 2 * margins;imageSize.height = squaresY * squareLength + 2 * margins;board.generateImage(imageSize, boardImage, margins, borderBits);
-
第一個參數是輸出圖像的像素尺寸。如果該尺寸與棋盤尺寸不成比例,圖像將居中顯示。
-
第二個參數是包含Charuco棋盤的輸出圖像。
-
第三個參數是(可選的)邊距像素值,確保所有標記都不會接觸圖像邊框。
-
最后是標記邊框的尺寸,類似于
cv::aruco::generateImageMarker()
函數。默認值為1。
輸出圖像效果如下:
完整示例代碼位于samples/cpp/tutorial_code/objectDetection/
目錄下的create_board_charuco.cpp
文件中。
現在,示例程序create_board_charuco.cpp
通過cv::CommandLineParser
接收命令行輸入。該文件的示例參數形式如下:
"_output_path_/chboard.png" -w=5 -h=7 -sl=100 -ml=60 -d=10
ChArUco 棋盤檢測
當檢測 ChArUco 棋盤時,實際上是在檢測棋盤上的每個棋盤格角點。
ChArUco 棋盤上的每個角點都有一個唯一的標識符(id)。這些 id 從 0 開始編號,直到棋盤上的角點總數。ChArUco 棋盤檢測的步驟可以分解如下:
- 獲取輸入圖像
Mat image, imageCopy;inputVideo.retrieve(image);
需要檢測標記點的原始圖像。該圖像對于在ChArUco角點進行亞像素級優化是必需的。
- 讀取相機校準參數(僅適用于帶相機校準的檢測)
if(parser.has("c")) {bool readOk = readCameraParameters(parser.get<std::string>("c"), camMatrix, distCoeffs);if(!readOk) {throw std::runtime_error("Invalid camera file\n");}}
readCameraParameters
的參數說明如下:
-
第一個參數是相機內參矩陣和畸變系數的文件路徑。
-
第二和第三個參數分別是 cameraMatrix 和 distCoeffs。
該函數接收這些參數作為輸入,并返回一個布爾值,表示相機標定參數是否有效。若無需標定直接檢測 charuco 角點,則無需此步驟。
- 標記檢測與 charuco 角點插值
ChArUco 角點的檢測基于先前檢測到的標記。因此,首先檢測標記,然后從標記中插值計算出 ChArUco 角點。檢測 ChArUco 角點的方法是 cv::aruco::CharucoDetector::detectBoard()
。
// detect markers and charuco cornerscharucoDetector.detectBoard(image, charucoCorners, charucoIds, markerCorners, markerIds);
detectBoard
的參數包括:
image
- 輸入圖像。charucoCorners
- 輸出檢測到的角點的圖像位置列表。charucoIds
- 輸出charucoCorners
中每個檢測到的角點的 ID。markerCorners
- 輸入/輸出檢測到的標記角點向量。markerIds
- 輸入/輸出檢測到的標記標識符向量。
如果 markerCorners
和 markerIds
為空,該函數將檢測 ArUco 標記及其 ID。
如果提供了校準參數,ChArUco 角點將通過以下步驟進行插值:首先,根據 ArUco 標記估計粗略姿態,然后將 ChArUco 角點重新投影到圖像中。反之,如果未提供校準參數,則通過計算 ChArUco 平面與 ChArUco 圖像投影之間的對應單應性來插值 ChArUco 角點。
使用單應性的主要問題是插值對圖像畸變更敏感。實際上,單應性計算僅使用每個 ChArUco 角點最近的標記,以減少畸變的影響。
在檢測 ChArUco 板的標記時,尤其是使用單應性時,建議禁用標記的角點細化。原因是由于棋盤格方塊的鄰近性,亞像素處理可能導致角點位置出現顯著偏差,這些偏差會傳播到 ChArUco 角點插值中,導致結果不佳。
注意:為避免偏差,棋盤格方塊與 ArUco 標記之間的邊距應大于一個標記模塊的 70%。
此外,僅返回周圍兩個標記均被找到的角點。如果任一周圍標記未被檢測到,通常意味著該區域存在遮擋或圖像質量不佳。無論如何,最好不考慮該角點,因為我們希望確保插值的 ChArUco 角點非常精確。
在插值 ChArUco 角點后,會執行亞像素細化。
一旦完成 ChArUco 角點的插值,我們可能需要繪制它們以檢查檢測是否正確。這可以通過 cv::aruco::drawDetectedCornersCharuco()
函數輕松實現:
aruco::drawDetectedCornersCharuco(imageCopy, charucoCorners, charucoIds, cv::Scalar(255, 0, 0));
-
imageCopy
是用于繪制角點的圖像(通常與檢測到角點的圖像相同)。 -
outputImage
將是帶有繪制角點的inputImage
克隆圖像。 -
charucoCorners
和charucoIds
是通過cv::aruco::CharucoDetector::detectBoard()
函數檢測到的Charuco角點。 -
最后一個參數是(可選的)繪制角點所用的顏色,類型為
cv::Scalar
。
對于這張圖像:
帶有Charuco板的圖像
處理結果如下:
檢測到的Charuco板
當存在遮擋時,如下圖所示,雖然部分角點清晰可見,但由于遮擋導致其周圍標記未被全部檢測到,因此這些角點不會被插值計算:
存在遮擋的Charuco檢測
示例視頻:
完整示例代碼位于 samples/cpp/tutorial_code/objectDetection/
目錄下的 detect_board_charuco.cpp
文件中。
示例程序 detect_board_charuco.cpp
現在通過 cv::CommandLineParser
接收命令行輸入參數。該文件的示例參數格式如下:
-w=5 -h=7 -sl=0.04 -ml=0.02 -d=10 -v=/path_to_opencv/opencv/doc/tutorials/objdetect/charuco_detection/images/choriginal.jpg
ChArUco 姿態估計
ChArUco 標定板的最終目標是通過精確查找角點來實現高精度校準或姿態估計。
aruco 模塊提供了便捷的 ChArUco 姿態估計功能。與 cv::aruco::GridBoard
類似,cv::aruco::CharucoBoard
的坐標系被設置在標定板平面內,Z 軸指向平面內部,原點位于標定板左下角中心。
注意:OpenCV 4.6.0 版本后,標定板坐標系發生了不兼容變更。現在坐標系設置在標定板平面內且 Z 軸指向平面內部(此前 Z 軸指向平面外部)。順時針順序的 objPoints
對應 Z 軸指向平面內部的情況,逆時針順序的 objPoints
則對應 Z 軸指向平面外部的情況。詳見 PR https://github.com/opencv/opencv_contrib/pull/3174
執行 ChArUco 標定板姿態估計時,應使用 cv::aruco::CharucoBoard::matchImagePoints()
和 cv::solvePnP()
函數:
// estimate charuco board posebool validPose = false;if(camMatrix.total() != 0 && distCoeffs.total() != 0 && charucoIds.size() >= 4) {Mat objPoints, imgPoints;charucoBoard.matchImagePoints(charucoCorners, charucoIds, objPoints, imgPoints);validPose = solvePnP(objPoints, imgPoints, camMatrix, distCoeffs, rvec, tvec);}
-
charucoCorners
和charucoIds
參數來自cv::aruco::CharucoDetector::detectBoard()
函數檢測到的ChArUco角點。 -
cameraMatrix
和distCoeffs
是相機標定參數,為姿態估計所必需。 -
最后,
rvec
和tvec
參數表示ChArUco板的輸出姿態。 -
如果姿態估計正確,
cv::solvePnP()
返回 true,否則返回 false。失敗的主要原因是角點數量不足或它們位于同一直線上。
可以使用 cv::drawFrameAxes()
繪制坐標軸來驗證姿態估計是否正確。結果將顯示為:(X軸:紅色,Y軸:綠色,Z軸:藍色)
ChArUco板坐標軸
完整示例代碼位于 samples/cpp/tutorial_code/objectDetection/
目錄下的 detect_board_charuco.cpp
文件中。
示例程序 detect_board_charuco.cpp
現在通過 cv::CommandLineParser
接收命令行輸入參數。對于該文件,示例參數如下:
-w=5 -h=7 -sl=0.04 -ml=0.02 -d=10
-v=/path_to_opencv/opencv/doc/tutorials/objdetect/charuco_detection/images/choriginal.jpg
-c=/path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_charuco.yml
生成于 2025 年 4 月 30 日 星期三 23:08:42,由 doxygen 1.12.0 為 OpenCV 生成
菱形標記檢測
https://docs.opencv.org/4.x/d5/d07/tutorial_charuco_diamond_detection.html
上一教程: ChArUco標定板檢測
下一教程: 使用ArUco和ChArUco進行校準
ChArUco菱形標記(簡稱菱形標記)是由3x3方格和4個位于白色方格內的ArUco標記組成的棋盤。雖然外觀與ChArUco標定板相似,但兩者在概念上存在本質區別。
無論是ChArUco標定板還是菱形標記,它們的檢測都基于預先識別出的ArUco標記。對于ChArUco標定板,系統通過直接查看標記ID來篩選使用的標記。這意味著只要圖像中出現標定板包含的標記,就會自動認定其屬于該標定板。然而,如果同一標定板的多個標記出現在圖像中,會導致系統無法確定應該使用哪個標記,從而產生歧義。
相比之下,菱形標記的檢測不依賴于標記ID,而是基于標記之間的相對位置關系。因此,同一菱形或不同菱形中的標記ID可以重復,系統仍能無歧義地同時檢測它們。不過,由于基于相對位置關系尋找標記的復雜性,菱形標記的尺寸被限制為3x3方格和4個標記。
與單個ArUco標記類似,每個菱形標記由4個角點和1個標識符組成。四個角點對應標記中棋盤的四個角,而標識符實際上是由4個數字組成的數組,代表菱形內四個ArUco標記的ID。
菱形標記特別適用于需要允許重復標記的場景,例如:
- 通過使用菱形標記進行標注,可以大幅增加單個標記的標識符數量。理論上可實現N^4種不同ID(N為所用字典中的標記數量)
- 為四個標記賦予不同的語義含義。例如,可以用其中一個標記ID表示標記的尺度(即方格大小),這樣只需更改四個標記中的一個,就能讓同一菱形在環境中以不同尺寸被檢測到,用戶無需手動指定每個標記的尺度。該用例已包含在模塊示例文件夾的
detect_diamonds.cpp
文件中
此外,由于菱形標記的角點是棋盤角點,它們可用于高精度的位姿估計。
菱形標記相關功能定義在opencv2/objdetect/charuco_detector.hpp頭文件中。
ChArUco 菱形標記生成
使用 cv::aruco::CharucoBoard::generateImage()
函數可以輕松生成菱形標記圖像。例如:
vector<int> diamondIds = {ids[0], ids[1], ids[2], ids[3]};aruco::CharucoBoard charucoBoard(Size(3, 3), (float)squareLength, (float)markerLength, dictionary, diamondIds);Mat markerImg;charucoBoard.generateImage(Size(3*squareLength + 2*margins, 3*squareLength + 2*margins), markerImg, margins, borderBits);
這將創建一個邊長為200像素的正方形鉆石標記圖像,標記大小為120像素。標記ID通過第二個參數以cv::Vec4i
對象形式給出。鉆石布局中的標記ID順序與標準ChArUco標定板相同,即頂部、左側、右側和底部。
生成的圖像如下:
鉆石標記
完整可運行示例位于samples/cpp/tutorial_code/objectDetection/
目錄下的create_diamond.cpp
文件中。
示例程序create_diamond.cpp
現在通過cv::CommandLineParser
從命令行接收輸入參數。對于該文件,示例參數格式如下:
"_path_/mydiamond.png" -sl=200 -ml=120 -d=10 -ids=0,1,2,3
ChArUco 菱形標記檢測
與大多數情況類似,檢測菱形標記需要先完成ArUco標記的檢測。在檢測到標記后,使用 cv::aruco::CharucoDetector::detectDiamonds()
函數來檢測菱形標記:
vector<int> markerIds;vector<Vec4i> diamondIds;vector<vector<Point2f> > markerCorners, diamondCorners;vector<Vec3d> rvecs, tvecs;detector.detectDiamonds(image, diamondCorners, diamondIds, markerCorners, markerIds);
cv::aruco::CharucoDetector::detectDiamonds()
函數接收原始圖像以及先前檢測到的標記角點和ID。如果markerCorners和markerIds為空,該函數將自動檢測ArUco標記及其ID。輸入圖像對于在ChArUco角點執行亞像素級優化是必需的。該函數還接收正方形尺寸與標記尺寸的比例參數,該參數在以下兩個步驟中都需要使用:根據標記的相對位置檢測菱形,以及插值計算ChArUco角點。
函數通過兩個參數返回檢測到的菱形。第一個參數diamondCorners
是一個數組,包含每個檢測到菱形的四個角點。其格式與cv::aruco::ArucoDetector::detectMarkers()
函數檢測到的角點類似,且每個菱形的角點順序與ArUco標記一致——即從左上角開始順時針排列。第二個返回參數diamondIds
包含diamondCorners
中所有菱形角點對應的ID,每個ID實際上是由4個整數組成的數組,可用cv::Vec4i
類型表示。
檢測到的菱形可通過cv::aruco::drawDetectedDiamonds()
函數進行可視化,該函數只需接收圖像及菱形角點和ID作為參數:
if(diamondIds.size() > 0) {aruco::drawDetectedDiamonds(imageCopy, diamondCorners, diamondIds);
該結果與 cv::aruco::drawDetectedMarkers()
生成的相同,但會打印出菱形標記的四個ID:
檢測到的菱形標記
完整可運行示例位于 samples/cpp/tutorial_code/objectDetection/
目錄下的 detect_diamonds.cpp
文件中。
現在,示例 detect_diamonds.cpp
通過 cv::CommandLineParser
接收命令行輸入。對于該文件,示例參數如下所示:
-dp=path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/detector_params.yml -sl=0.4 -ml=0.25 -refine=3
-v=path_to_opencv/opencv/doc/tutorials/objdetect/charuco_diamond_detection/images/diamondmarkers.jpg
-cd=path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_dict.yml
ChArUco 菱形姿態估計
由于 ChArUco 菱形由其四個角點表示,其姿態估計方式與單個 ArUco 標記相同,即使用 cv::solvePnP()
函數。例如:
// estimate diamond posesize_t N = diamondIds.size();if(estimatePose && N > 0) {cv::Mat objPoints(4, 1, CV_32FC3);rvecs.resize(N);tvecs.resize(N);if(!autoScale) {// set coordinate systemobjPoints.ptr<Vec3f>(0)[0] = Vec3f(-squareLength/2.f, squareLength/2.f, 0);objPoints.ptr<Vec3f>(0)[1] = Vec3f(squareLength/2.f, squareLength/2.f, 0);objPoints.ptr<Vec3f>(0)[2] = Vec3f(squareLength/2.f, -squareLength/2.f, 0);objPoints.ptr<Vec3f>(0)[3] = Vec3f(-squareLength/2.f, -squareLength/2.f, 0);// Calculate pose for each markerfor (size_t i = 0ull; i < N; i++)solvePnP(objPoints, diamondCorners.at(i), camMatrix, distCoeffs, rvecs.at(i), tvecs.at(i));
該函數將獲取每個菱形標記的旋轉和平移向量,并將它們存儲在 rvecs
和 tvecs
中。需要注意的是,菱形角點實際上是棋盤格方塊的角點,因此進行姿態估計時需要提供方塊邊長而非標記長度。此外還需提供相機標定參數。
最后,可以通過調用 drawFrameAxes()
繪制坐標系來驗證估計的姿態是否正確:
檢測到的菱形坐標系
菱形姿態的坐標系將位于標記中心,Z軸向外延伸,這與簡單的ArUco標記姿態估計方式一致。
示例視頻:
此外,ChArUco菱形姿態也可以像ChArUco標定板一樣進行估計:
for (size_t i = 0ull; i < N; i++) { // estimate diamond pose as Charuco boardMat objPoints_b, imgPoints;// The coordinate system of the diamond is placed in the board plane centered in the bottom left cornervector<int> charucoIds = {0, 1, 3, 2}; // if CCW order, Z axis pointing in the plane// vector<int> charucoIds = {0, 2, 3, 1}; // if CW order, Z axis pointing out the planecharucoBoard.matchImagePoints(diamondCorners[i], charucoIds, objPoints_b, imgPoints);solvePnP(objPoints_b, imgPoints, camMatrix, distCoeffs, rvecs[i], tvecs[i]);}
完整可運行示例位于 samples/cpp/tutorial_code/objectDetection/
目錄下的 detect_diamonds.cpp
文件中。
示例程序 detect_diamonds.cpp
現在通過 cv::CommandLineParser
接收命令行輸入參數。對于該文件,示例參數格式如下:
-dp=path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/detector_params.yml -sl=0.4 -ml=0.25 -refine=3
-v=path_to_opencv/opencv/doc/tutorials/objdetect/charuco_diamond_detection/images/diamondmarkers.jpg
-cd=path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_dict.yml
-c=path_to_opencv/opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_params.yml
生成于 2025年4月30日 星期三 23:08:42,由 doxygen 1.12.0 為 OpenCV 創建
使用ArUco和ChArUco進行標定
https://docs.opencv.org/4.x/da/d13/tutorial_aruco_calibration.html
上一篇教程: 菱形標記檢測
下一篇教程: ArUco模塊常見問題
ArUco模塊也可用于相機標定。相機標定的目的是獲取相機內參和畸變系數。這些參數在相機光學組件未改動時保持不變,因此通常只需進行一次標定。
傳統相機標定通常使用OpenCV的cv::calibrateCamera()
函數。該函數需要多組三維空間點與其在相機圖像中投影點的對應關系,這些對應點通常來自棋盤格圖案的角點。更多細節可參考cv::calibrateCamera()
函數文檔或OpenCV標定教程。
通過ArUco模塊,可以利用ArUco標記角點或ChArUco角點進行標定。與傳統棋盤格相比,ArUco標定具有更強的適應性,能容忍標記遮擋或局部可見的情況。
雖然可以使用標記角點或ChArUco角點進行標定,但強烈推薦使用ChArUco角點方案,因為其提供的角點坐標精度遠高于普通標記角點。僅在無法使用ChArUco標定板的特殊場景下,才考慮使用標準ArUco板進行標定。
使用 ChArUco 標定板進行校準
要通過 ChArUco 標定板進行校準,需要從不同視角檢測標定板,這與傳統棋盤格圖案的標準校準方法相同。不過,由于使用 ChArUco 的優勢,允許存在遮擋和局部視角,并非所有角點都需要在所有視角中可見。
ChArUco 校準視角
使用 cv::calibrateCamera()
對 cv::aruco::CharucoBoard
進行校準的示例:
// Create charuco board object and CharucoDetectoraruco::CharucoBoard board(Size(squaresX, squaresY), squareLength, markerLength, dictionary);aruco::CharucoDetector detector(board, charucoParams, detectorParams);// Collect data from each framevector<Mat> allCharucoCorners, allCharucoIds;vector<vector<Point2f>> allImagePoints;vector<vector<Point3f>> allObjectPoints;vector<Mat> allImages;Size imageSize;while(inputVideo.grab()) {Mat image, imageCopy;inputVideo.retrieve(image);vector<int> markerIds;vector<vector<Point2f>> markerCorners;Mat currentCharucoCorners, currentCharucoIds;vector<Point3f> currentObjectPoints;vector<Point2f> currentImagePoints;// Detect ChArUco boarddetector.detectBoard(image, currentCharucoCorners, currentCharucoIds);
if(key == 'c' && currentCharucoCorners.total() > 3) {// Match image pointsboard.matchImagePoints(currentCharucoCorners, currentCharucoIds, currentObjectPoints, currentImagePoints);if(currentImagePoints.empty() || currentObjectPoints.empty()) {cout << "Point matching failed, try again." << endl;continue;}cout << "Frame captured" << endl;allCharucoCorners.push_back(currentCharucoCorners);allCharucoIds.push_back(currentCharucoIds);allImagePoints.push_back(currentImagePoints);allObjectPoints.push_back(currentObjectPoints);allImages.push_back(image);imageSize = image.size();}}
Mat cameraMatrix, distCoeffs;if(calibrationFlags & CALIB_FIX_ASPECT_RATIO) {cameraMatrix = Mat::eye(3, 3, CV_64F);cameraMatrix.at<double>(0, 0) = aspectRatio;}// Calibrate camera using ChArUcodouble repError = calibrateCamera(allObjectPoints, allImagePoints, imageSize, cameraMatrix, distCoeffs,noArray(), noArray(), noArray(), noArray(), noArray(), calibrationFlags);
在每個視角捕獲的ChArUco角點和ChArUco標識符分別存儲在向量allCharucoCorners
和allCharucoIds
中,每個視角對應一個元素。
calibrateCamera()
函數將用相機標定參數填充cameraMatrix
和distCoeffs
數組,并返回標定得到的重投影誤差。rvecs
和tvecs
中的元素將被填充為相機在每個視角下的估計姿態(相對于ChArUco標定板)。
最后,calibrationFlags
參數決定了標定過程中的一些選項。
完整可運行示例位于samples/cpp/tutorial_code/objectDetection
文件夾中的calibrate_camera_charuco.cpp
文件。
現在示例通過cv::CommandLineParser
接收命令行輸入。對于該文件,示例參數如下:
"camera_calib.txt" -w=5 -h=7 -sl=0.04 -ml=0.02 -d=10
-v=path/img_%02d.jpg
來自 opencv/samples/cpp/tutorial_code/objectDetection/tutorial_camera_charuco.yml
的相機標定參數是通過處理 該文件夾 中的 img_00.jpg-img_03.jpg
圖像獲取的。
使用ArUco標定板進行校準
如前所述,建議使用ChArUco標定板而非ArUco標定板進行相機校準,因為ChArUco角點比標記角點更精確。但在某些特殊情況下,可能需要使用基于ArUco標定板的校準方法。與之前的情況類似,該方法需要從不同視角檢測ArUco標定板。
ArUco校準視角
使用cv::calibrateCamera()
對cv::aruco::GridBoard
進行校準的示例:
// Create board object and ArucoDetectoraruco::GridBoard gridboard(Size(markersX, markersY), markerLength, markerSeparation, dictionary);aruco::ArucoDetector detector(dictionary, detectorParams);// Collected frames for calibrationvector<vector<vector<Point2f>>> allMarkerCorners;vector<vector<int>> allMarkerIds;Size imageSize;while(inputVideo.grab()) {Mat image, imageCopy;inputVideo.retrieve(image);vector<int> markerIds;vector<vector<Point2f>> markerCorners, rejectedMarkers;// Detect markersdetector.detectMarkers(image, markerCorners, markerIds, rejectedMarkers);// Refind strategy to detect more markersif(refindStrategy) {detector.refineDetectedMarkers(image, gridboard, markerCorners, markerIds, rejectedMarkers);}
if(key == 'c' && !markerIds.empty()) {cout << "Frame captured" << endl;allMarkerCorners.push_back(markerCorners);allMarkerIds.push_back(markerIds);imageSize = image.size();}}
Mat cameraMatrix, distCoeffs;if(calibrationFlags & CALIB_FIX_ASPECT_RATIO) {cameraMatrix = Mat::eye(3, 3, CV_64F);cameraMatrix.at<double>(0, 0) = aspectRatio;}// Prepare data for calibrationvector<Point3f> objectPoints;vector<Point2f> imagePoints;vector<Mat> processedObjectPoints, processedImagePoints;size_t nFrames = allMarkerCorners.size();for(size_t frame = 0; frame < nFrames; frame++) {Mat currentImgPoints, currentObjPoints;gridboard.matchImagePoints(allMarkerCorners[frame], allMarkerIds[frame], currentObjPoints, currentImgPoints);if(currentImgPoints.total() > 0 && currentObjPoints.total() > 0) {processedImagePoints.push_back(currentImgPoints);processedObjectPoints.push_back(currentObjPoints);}}// Calibrate cameradouble repError = calibrateCamera(processedObjectPoints, processedImagePoints, imageSize, cameraMatrix, distCoeffs,noArray(), noArray(), noArray(), noArray(), noArray(), calibrationFlags);
完整可運行示例位于 samples/cpp/tutorial_code/objectDetection
文件夾中的 calibrate_camera.cpp
文件。
現在這些示例通過 cv::CommandLineParser
接收命令行輸入。對于該文件,示例參數如下所示:
"camera_calib.txt" -w=5 -h=7 -l=100 -s=10 -d=10 -v=path/aruco_videos_or_images
生成于 2025年4月30日 星期三 23:08:42,由 doxygen 1.12.0 為 OpenCV 生成
ArUco 模塊常見問題解答
https://docs.opencv.org/4.x/d1/dcb/tutorial_aruco_faq.html
上一教程: 使用ArUco和ChArUco進行標定
本文整理了關于使用aruco模塊的常見問題。
- 我只想標記一些物體,應該使用什么?
這種情況下,只需使用單個ArUco標記即可。您可以在每個需要識別的物體上放置一個或多個不同ID的標記。
- 標記檢測使用什么算法?
aruco模塊基于原始ArUco庫。完整的檢測過程描述可參考:
?S. Garrido-Jurado, R. Mu?oz-Salinas, F. J. Madrid-Cuevas, and M. J. Marín-Jiménez. 2014、“Automatic generation and detection of highly reliable fiducial markers under occlusion”. Pattern Recogn. 47, 6 (June 2014), 2280-2292. DOI=10.1016/j.patcog.2014.01.005
- 我的標記無法正確檢測,該怎么辦?
可能有很多因素會影響標記的正確檢測。您可能需要調整cv::aruco::DetectorParameters
對象中的某些參數。首先可以檢查標記是否被cv::aruco::ArucoDetector::detectMarkers()
函數返回為被拒絕的候選。根據結果,您應該嘗試修改不同的參數。
如果使用ArUco板,還可以嘗試cv::aruco::ArucoDetector::refineDetectedMarkers()
函數。如果使用大尺寸標記(400x400像素及以上),嘗試增加cv::aruco::DetectorParameters::adaptiveThreshWinSizeMax
值。同時避免ArUco標記周圍過窄的邊框(標記周長的5%或更少,可通過cv::aruco::DetectorParameters::minMarkerDistanceRate
調整)。
- ArUco板有什么優點?有什么缺點?
使用標記板可以從一組標記中獲取相機姿態,而不僅依賴單個標記。這樣即使部分標記被遮擋,只要能看到一個標記就能獲取姿態。
此外,由于通常使用更多角點進行姿態估計,其精度會比使用單個標記更高。
主要缺點是標記板不如單個標記靈活。
- ChArUco板相比ArUco板有什么優點?有什么缺點?
ChArUco板結合了棋盤格和ArUco板。因此,ChArUco板提供的角點比ArUco板(或單個標記)更精確。
主要缺點是ChArUco板不如ArUco板靈活。例如,ChArUco板是平面板且具有特定的標記布局,而ArUco板可以有任何布局,甚至是3D的。此外,ChArUco板中的標記通常更小,更難檢測。
- 我不需要姿態估計,應該使用ChArUco板嗎?
不需要。ChArUco板的主要目的是為姿態估計或相機標定提供高精度角點。
- ArUco板中的所有標記必須在同一平面嗎?
不需要,ArUco板中的標記角點可以放置在其3D坐標系中的任何位置。
- ChArUco板中的所有標記必須在同一平面嗎?
是的,ChArUco板中的所有標記必須在同一平面,且其布局由棋盤格形狀固定。
cv::aruco::Board
對象和cv::aruco::GridBoard
對象有什么區別?
cv::aruco::GridBoard
類是繼承自cv::aruco::Board
類的特定類型板。一個cv::aruco::GridBoard
對象是其標記位于同一平面且呈網格布局的板。
- 什么是Diamond標記?
Diamond標記與3x3方格的ChArUco板非常相似。但與ChArUco板不同,Diamond的檢測基于標記的相對位置。當您想為Diamond中的任何(或所有)標記賦予概念意義時,它們很有用。例如使用其中一個標記來提供Diamond的比例。
- 在進行板檢測、ChArUco板檢測或Diamond檢測之前,是否需要先檢測標記?
是的,單個標記的檢測是aruco模塊的基本功能。使用cv::aruco::DetectorParameters::detectMarkers()
函數完成。其他功能都接收來自此函數的已檢測標記列表。
- 我想校準相機,可以使用這個模塊嗎?
可以,aruco模塊提供了使用ArUco板和ChArUco板進行相機標定的功能。
- 應該使用ChArUco板還是ArUco板進行標定?
強烈推薦使用ChArUco板進行標定,因為其精度更高。
- 應該使用預定義字典還是生成自己的字典?
通常使用預定義字典更簡單。但如果需要更大的字典(就標記數量或位數而言),則應生成自己的字典。當您想最大化標記間距離以在識別步驟中實現更好的糾錯時,字典生成也很有用。
- 我生成自己的字典耗時太長
字典生成只需在應用程序開始時執行一次,且只需幾秒鐘。如果在檢測循環的每次迭代中都生成字典,這是錯誤的做法。
此外,建議使用cv::aruco::Dictionary::writeDictionary()
將字典保存到文件,并在每次執行時用cv::aruco::Dictionary::readDictionary()
讀取,這樣就不需要每次都生成。
- 我想使用已經打印的原始ArUco庫中的一些標記,可以使用嗎?
可以,預定義字典之一cv::aruco::DICT_ARUCO_ORIGINAL
`可以檢測原始ArUco庫中具有相同標識符的標記。
- 可以在本模塊中使用原始ArUco庫的板配置文件嗎?
不能直接使用,您需要將ArUco文件的信息適配到aruco模塊的板格式。
- 可以使用本模塊檢測其他基于二進制基準標記的庫的標記嗎?
可能可以,但您需要將原始庫的字典移植到aruco模塊格式。
- 需要將字典信息存儲在文件中以便在不同執行中使用嗎?
如果使用預定義字典,則不需要。否則建議將其保存到文件。
- 需要將板信息存儲在文件中以便在不同執行中使用嗎?
如果使用cv::aruco::GridBoard
或cv::aruco::CharucoBoard
,只需存儲提供給cv::aruco::GridBoard::GridBoard()
構造函數或cv::aruco::CharucoBoard
構造函數的板尺寸。如果手動修改板的標記ID,或使用不同類型的板,應將板對象保存到文件。
- aruco模塊提供將字典或板保存到文件的功能嗎?
可以使用cv::aruco::Dictionary::writeDictionary()
和cv::aruco::Dictionary::readDictionary()
處理cv::aruco::Dictionary
。板類的數據成員是公開的,可以輕松存儲。
- 好的,但如何渲染3D模型以創建增強現實應用?
為此,您需要使用外部渲染引擎庫,如OpenGL。aruco模塊僅提供獲取相機姿態(即旋轉和平移向量)的功能,這是創建增強現實效果所必需的。但您需要將旋轉和平移向量從OpenCV格式適配到3D渲染庫接受的格式。原始ArUco庫包含如何為OpenGL和Ogre3D執行此操作的示例。
- 我在研究工作中使用了這個模塊,如何引用?
您可以引用原始ArUco庫:
?S. Garrido-Jurado, R. Mu?oz-Salinas, F. J. Madrid-Cuevas, and M. J. Marín-Jiménez. 2014、“Automatic generation and detection of highly reliable fiducial markers under occlusion”. Pattern Recogn. 47, 6 (June 2014), 2280-2292. DOI=10.1016/j.patcog.2014.01.005
- 姿態估計標記無法正確檢測,該怎么辦?
重要的是要注意,僅使用4個共面點進行姿態估計存在歧義。通常,如果相機靠近標記,可以解決歧義。但隨著標記變小,角點估計的誤差增大,歧義成為問題。嘗試增大標記尺寸,也可以嘗試使用非對稱標記(aruco_dict_utils.cpp)以避免沖突。使用多個標記(ArUco/ChArUco/Diamonds板)和帶有cv::SOLVEPNP_IPPE_SQUARE
`選項的solvePnP()進行姿態估計。更多信息見此問題。
生成于 2025年4月30日 星期三 23:08:42,由 doxygen 1.12.0 生成
2025-07-19(六)