test_charucodetection.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. // This file is part of OpenCV project.
  2. // It is subject to the license terms in the LICENSE file found in the top-level directory
  3. // of this distribution and at http://opencv.org/license.html.
  4. #include "test_precomp.hpp"
  5. #include "test_aruco_utils.hpp"
  6. namespace opencv_test { namespace {
  7. /**
  8. * @brief Get a synthetic image of Chessboard in perspective
  9. */
  10. static Mat projectChessboard(int squaresX, int squaresY, float squareSize, Size imageSize,
  11. Mat cameraMatrix, Mat rvec, Mat tvec, bool legacyPattern) {
  12. Mat img(imageSize, CV_8UC1, Scalar::all(255));
  13. Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
  14. for(int y = 0; y < squaresY; y++) {
  15. float startY = float(y) * squareSize;
  16. for(int x = 0; x < squaresX; x++) {
  17. if(legacyPattern && (squaresY % 2 == 0)) {
  18. if((y + 1) % 2 != x % 2) continue;
  19. } else {
  20. if(y % 2 != x % 2) continue;
  21. }
  22. float startX = float(x) * squareSize;
  23. vector< Point3f > squareCorners;
  24. squareCorners.push_back(Point3f(startX, startY, 0) - Point3f(squaresX*squareSize/2.f, squaresY*squareSize/2.f, 0.f));
  25. squareCorners.push_back(squareCorners[0] + Point3f(squareSize, 0, 0));
  26. squareCorners.push_back(squareCorners[0] + Point3f(squareSize, squareSize, 0));
  27. squareCorners.push_back(squareCorners[0] + Point3f(0, squareSize, 0));
  28. vector< vector< Point2f > > projectedCorners;
  29. projectedCorners.push_back(vector< Point2f >());
  30. projectPoints(squareCorners, rvec, tvec, cameraMatrix, distCoeffs, projectedCorners[0]);
  31. vector< vector< Point > > projectedCornersInt;
  32. projectedCornersInt.push_back(vector< Point >());
  33. for(int k = 0; k < 4; k++)
  34. projectedCornersInt[0]
  35. .push_back(Point((int)projectedCorners[0][k].x, (int)projectedCorners[0][k].y));
  36. fillPoly(img, projectedCornersInt, Scalar::all(0));
  37. }
  38. }
  39. return img;
  40. }
  41. /**
  42. * @brief Check pose estimation of charuco board
  43. */
  44. static Mat projectCharucoBoard(aruco::CharucoBoard& board, Mat cameraMatrix, double yaw,
  45. double pitch, double distance, Size imageSize, int markerBorder,
  46. Mat &rvec, Mat &tvec) {
  47. getSyntheticRT(yaw, pitch, distance, rvec, tvec);
  48. // project markers
  49. Mat img = Mat(imageSize, CV_8UC1, Scalar::all(255));
  50. for(unsigned int indexMarker = 0; indexMarker < board.getIds().size(); indexMarker++) {
  51. projectMarker(img, board, indexMarker, cameraMatrix, rvec, tvec, markerBorder);
  52. }
  53. // project chessboard
  54. Mat chessboard =
  55. projectChessboard(board.getChessboardSize().width, board.getChessboardSize().height,
  56. board.getSquareLength(), imageSize, cameraMatrix, rvec, tvec, board.getLegacyPattern());
  57. for(unsigned int i = 0; i < chessboard.total(); i++) {
  58. if(chessboard.ptr< unsigned char >()[i] == 0) {
  59. img.ptr< unsigned char >()[i] = 0;
  60. }
  61. }
  62. return img;
  63. }
  64. static bool borderPixelsHaveSameColor(const Mat& image, uint8_t color) {
  65. for (int j = 0; j < image.cols; j++) {
  66. if (image.at<uint8_t>(0, j) != color || image.at<uint8_t>(image.rows-1, j) != color)
  67. return false;
  68. }
  69. for (int i = 0; i < image.rows; i++) {
  70. if (image.at<uint8_t>(i, 0) != color || image.at<uint8_t>(i, image.cols-1) != color)
  71. return false;
  72. }
  73. return true;
  74. }
  75. /**
  76. * @brief Check Charuco detection
  77. */
  78. class CV_CharucoDetection : public cvtest::BaseTest {
  79. public:
  80. CV_CharucoDetection(bool _legacyPattern) : legacyPattern(_legacyPattern) {}
  81. protected:
  82. void run(int);
  83. bool legacyPattern;
  84. };
  85. void CV_CharucoDetection::run(int) {
  86. int iter = 0;
  87. Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
  88. Size imgSize(500, 500);
  89. aruco::DetectorParameters params;
  90. params.minDistanceToBorder = 3;
  91. aruco::CharucoBoard board(Size(4, 4), 0.03f, 0.015f, aruco::getPredefinedDictionary(aruco::DICT_6X6_250));
  92. board.setLegacyPattern(legacyPattern);
  93. aruco::CharucoDetector detector(board, aruco::CharucoParameters(), params);
  94. cameraMatrix.at<double>(0, 0) = cameraMatrix.at<double>(1, 1) = 600;
  95. cameraMatrix.at<double>(0, 2) = imgSize.width / 2;
  96. cameraMatrix.at<double>(1, 2) = imgSize.height / 2;
  97. Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
  98. // for different perspectives
  99. for(double distance : {0.2, 0.4}) {
  100. for(int yaw = -55; yaw <= 50; yaw += 25) {
  101. for(int pitch = -55; pitch <= 50; pitch += 25) {
  102. int markerBorder = iter % 2 + 1;
  103. iter++;
  104. // create synthetic image
  105. Mat rvec, tvec;
  106. Mat img = projectCharucoBoard(board, cameraMatrix, deg2rad(yaw), deg2rad(pitch),
  107. distance, imgSize, markerBorder, rvec, tvec);
  108. // detect markers and interpolate charuco corners
  109. vector<vector<Point2f> > corners;
  110. vector<Point2f> charucoCorners;
  111. vector<int> ids, charucoIds;
  112. params.markerBorderBits = markerBorder;
  113. detector.setDetectorParameters(params);
  114. //detector.detectMarkers(img, corners, ids);
  115. if(iter % 2 == 0) {
  116. detector.detectBoard(img, charucoCorners, charucoIds, corners, ids);
  117. } else {
  118. aruco::CharucoParameters charucoParameters;
  119. charucoParameters.cameraMatrix = cameraMatrix;
  120. charucoParameters.distCoeffs = distCoeffs;
  121. detector.setCharucoParameters(charucoParameters);
  122. detector.detectBoard(img, charucoCorners, charucoIds, corners, ids);
  123. }
  124. ASSERT_GT(ids.size(), std::vector< int >::size_type(0)) << "Marker detection failed";
  125. // check results
  126. vector< Point2f > projectedCharucoCorners;
  127. // copy chessboardCorners
  128. vector<Point3f> copyChessboardCorners = board.getChessboardCorners();
  129. // move copyChessboardCorners points
  130. for (size_t i = 0; i < copyChessboardCorners.size(); i++)
  131. copyChessboardCorners[i] -= board.getRightBottomCorner() / 2.f;
  132. projectPoints(copyChessboardCorners, rvec, tvec, cameraMatrix, distCoeffs,
  133. projectedCharucoCorners);
  134. for(unsigned int i = 0; i < charucoIds.size(); i++) {
  135. int currentId = charucoIds[i];
  136. ASSERT_LT(currentId, (int)board.getChessboardCorners().size()) << "Invalid Charuco corner id";
  137. double repError = cv::norm(charucoCorners[i] - projectedCharucoCorners[currentId]); // TODO cvtest
  138. ASSERT_LE(repError, 5.) << "Charuco corner reprojection error too high";
  139. }
  140. }
  141. }
  142. }
  143. }
  144. /**
  145. * @brief Check charuco pose estimation
  146. */
  147. class CV_CharucoPoseEstimation : public cvtest::BaseTest {
  148. public:
  149. CV_CharucoPoseEstimation(bool _legacyPattern) : legacyPattern(_legacyPattern) {}
  150. protected:
  151. void run(int);
  152. bool legacyPattern;
  153. };
  154. void CV_CharucoPoseEstimation::run(int) {
  155. int iter = 0;
  156. Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
  157. Size imgSize(750, 750);
  158. aruco::DetectorParameters params;
  159. params.minDistanceToBorder = 3;
  160. aruco::CharucoBoard board(Size(4, 4), 0.03f, 0.015f, aruco::getPredefinedDictionary(aruco::DICT_6X6_250));
  161. board.setLegacyPattern(legacyPattern);
  162. aruco::CharucoDetector detector(board, aruco::CharucoParameters(), params);
  163. cameraMatrix.at<double>(0, 0) = cameraMatrix.at< double >(1, 1) = 1000;
  164. cameraMatrix.at<double>(0, 2) = imgSize.width / 2;
  165. cameraMatrix.at<double>(1, 2) = imgSize.height / 2;
  166. Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
  167. // for different perspectives
  168. for(double distance : {0.2, 0.25}) {
  169. for(int yaw = -55; yaw <= 50; yaw += 25) {
  170. for(int pitch = -55; pitch <= 50; pitch += 25) {
  171. int markerBorder = iter % 2 + 1;
  172. iter++;
  173. // get synthetic image
  174. Mat rvec, tvec;
  175. Mat img = projectCharucoBoard(board, cameraMatrix, deg2rad(yaw), deg2rad(pitch),
  176. distance, imgSize, markerBorder, rvec, tvec);
  177. // detect markers
  178. vector<vector<Point2f> > corners;
  179. vector<int> ids;
  180. params.markerBorderBits = markerBorder;
  181. detector.setDetectorParameters(params);
  182. // detect markers and interpolate charuco corners
  183. vector<Point2f> charucoCorners;
  184. vector<int> charucoIds;
  185. if(iter % 2 == 0) {
  186. detector.detectBoard(img, charucoCorners, charucoIds, corners, ids);
  187. } else {
  188. aruco::CharucoParameters charucoParameters;
  189. charucoParameters.cameraMatrix = cameraMatrix;
  190. charucoParameters.distCoeffs = distCoeffs;
  191. detector.setCharucoParameters(charucoParameters);
  192. detector.detectBoard(img, charucoCorners, charucoIds, corners, ids);
  193. }
  194. ASSERT_EQ(ids.size(), board.getIds().size());
  195. if(charucoIds.size() == 0) continue;
  196. // estimate charuco pose
  197. getCharucoBoardPose(charucoCorners, charucoIds, board, cameraMatrix, distCoeffs, rvec, tvec);
  198. // check axes
  199. const float aruco_offset = (board.getSquareLength() - board.getMarkerLength()) / 2.f;
  200. Point2f offset;
  201. vector<Point2f> topLeft, bottomLeft;
  202. if(legacyPattern) { // white box in upper left corner for even row count chessboard patterns
  203. offset = Point2f(aruco_offset + board.getSquareLength(), aruco_offset);
  204. topLeft = getMarkerById(board.getIds()[1], corners, ids);
  205. bottomLeft = getMarkerById(board.getIds()[2], corners, ids);
  206. } else { // always a black box in the upper left corner
  207. offset = Point2f(aruco_offset, aruco_offset);
  208. topLeft = getMarkerById(board.getIds()[0], corners, ids);
  209. bottomLeft = getMarkerById(board.getIds()[2], corners, ids);
  210. }
  211. vector<Point2f> axes = getAxis(cameraMatrix, distCoeffs, rvec, tvec, board.getSquareLength(), offset);
  212. ASSERT_NEAR(topLeft[0].x, axes[1].x, 3.f);
  213. ASSERT_NEAR(topLeft[0].y, axes[1].y, 3.f);
  214. ASSERT_NEAR(bottomLeft[0].x, axes[2].x, 3.f);
  215. ASSERT_NEAR(bottomLeft[0].y, axes[2].y, 3.f);
  216. // check estimate result
  217. vector< Point2f > projectedCharucoCorners;
  218. projectPoints(board.getChessboardCorners(), rvec, tvec, cameraMatrix, distCoeffs,
  219. projectedCharucoCorners);
  220. for(unsigned int i = 0; i < charucoIds.size(); i++) {
  221. int currentId = charucoIds[i];
  222. ASSERT_LT(currentId, (int)board.getChessboardCorners().size()) << "Invalid Charuco corner id";
  223. double repError = cv::norm(charucoCorners[i] - projectedCharucoCorners[currentId]); // TODO cvtest
  224. ASSERT_LE(repError, 5.) << "Charuco corner reprojection error too high";
  225. }
  226. }
  227. }
  228. }
  229. }
  230. /**
  231. * @brief Check diamond detection
  232. */
  233. class CV_CharucoDiamondDetection : public cvtest::BaseTest {
  234. public:
  235. CV_CharucoDiamondDetection();
  236. protected:
  237. void run(int);
  238. };
  239. CV_CharucoDiamondDetection::CV_CharucoDiamondDetection() {}
  240. void CV_CharucoDiamondDetection::run(int) {
  241. int iter = 0;
  242. Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
  243. Size imgSize(500, 500);
  244. aruco::DetectorParameters params;
  245. params.minDistanceToBorder = 0;
  246. float squareLength = 0.03f;
  247. float markerLength = 0.015f;
  248. aruco::CharucoBoard board(Size(3, 3), squareLength, markerLength,
  249. aruco::getPredefinedDictionary(aruco::DICT_6X6_250));
  250. aruco::CharucoDetector detector(board);
  251. cameraMatrix.at<double>(0, 0) = cameraMatrix.at< double >(1, 1) = 650;
  252. cameraMatrix.at<double>(0, 2) = imgSize.width / 2;
  253. cameraMatrix.at<double>(1, 2) = imgSize.height / 2;
  254. Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
  255. aruco::CharucoParameters charucoParameters;
  256. charucoParameters.cameraMatrix = cameraMatrix;
  257. charucoParameters.distCoeffs = distCoeffs;
  258. detector.setCharucoParameters(charucoParameters);
  259. // for different perspectives
  260. for(double distance : {0.2, 0.22}) {
  261. for(int yaw = -50; yaw <= 50; yaw += 25) {
  262. for(int pitch = -50; pitch <= 50; pitch += 25) {
  263. int markerBorder = iter % 2 + 1;
  264. vector<int> idsTmp;
  265. for(int i = 0; i < 4; i++)
  266. idsTmp.push_back(4 * iter + i);
  267. board = aruco::CharucoBoard(Size(3, 3), squareLength, markerLength,
  268. aruco::getPredefinedDictionary(aruco::DICT_6X6_250), idsTmp);
  269. detector.setBoard(board);
  270. iter++;
  271. // get synthetic image
  272. Mat rvec, tvec;
  273. Mat img = projectCharucoBoard(board, cameraMatrix, deg2rad(yaw), deg2rad(pitch),
  274. distance, imgSize, markerBorder, rvec, tvec);
  275. // detect markers
  276. vector<vector<Point2f>> corners;
  277. vector<int> ids;
  278. params.markerBorderBits = markerBorder;
  279. detector.setDetectorParameters(params);
  280. //detector.detectMarkers(img, corners, ids);
  281. // detect diamonds
  282. vector<vector<Point2f>> diamondCorners;
  283. vector<Vec4i> diamondIds;
  284. detector.detectDiamonds(img, diamondCorners, diamondIds, corners, ids);
  285. // check detect
  286. if(ids.size() != 4) {
  287. ts->printf(cvtest::TS::LOG, "Not enough markers for diamond detection");
  288. ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
  289. return;
  290. }
  291. // check results
  292. if(diamondIds.size() != 1) {
  293. ts->printf(cvtest::TS::LOG, "Diamond not detected correctly");
  294. ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
  295. return;
  296. }
  297. for(int i = 0; i < 4; i++) {
  298. if(diamondIds[0][i] != board.getIds()[i]) {
  299. ts->printf(cvtest::TS::LOG, "Incorrect diamond ids");
  300. ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
  301. return;
  302. }
  303. }
  304. vector< Point2f > projectedDiamondCorners;
  305. // copy chessboardCorners
  306. vector<Point3f> copyChessboardCorners = board.getChessboardCorners();
  307. // move copyChessboardCorners points
  308. for (size_t i = 0; i < copyChessboardCorners.size(); i++)
  309. copyChessboardCorners[i] -= board.getRightBottomCorner() / 2.f;
  310. projectPoints(copyChessboardCorners, rvec, tvec, cameraMatrix, distCoeffs,
  311. projectedDiamondCorners);
  312. vector< Point2f > projectedDiamondCornersReorder(4);
  313. projectedDiamondCornersReorder[0] = projectedDiamondCorners[0];
  314. projectedDiamondCornersReorder[1] = projectedDiamondCorners[1];
  315. projectedDiamondCornersReorder[2] = projectedDiamondCorners[3];
  316. projectedDiamondCornersReorder[3] = projectedDiamondCorners[2];
  317. for(unsigned int i = 0; i < 4; i++) {
  318. double repError = cv::norm(diamondCorners[0][i] - projectedDiamondCornersReorder[i]); // TODO cvtest
  319. if(repError > 5.) {
  320. ts->printf(cvtest::TS::LOG, "Diamond corner reprojection error too high");
  321. ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
  322. return;
  323. }
  324. }
  325. // estimate diamond pose
  326. vector< Vec3d > estimatedRvec, estimatedTvec;
  327. getMarkersPoses(diamondCorners, squareLength, cameraMatrix, distCoeffs, estimatedRvec,
  328. estimatedTvec, noArray(), false);
  329. // check result
  330. vector< Point2f > projectedDiamondCornersPose;
  331. vector< Vec3f > diamondObjPoints(4);
  332. diamondObjPoints[0] = Vec3f(0.f, 0.f, 0);
  333. diamondObjPoints[1] = Vec3f(squareLength, 0.f, 0);
  334. diamondObjPoints[2] = Vec3f(squareLength, squareLength, 0);
  335. diamondObjPoints[3] = Vec3f(0.f, squareLength, 0);
  336. projectPoints(diamondObjPoints, estimatedRvec[0], estimatedTvec[0], cameraMatrix,
  337. distCoeffs, projectedDiamondCornersPose);
  338. for(unsigned int i = 0; i < 4; i++) {
  339. double repError = cv::norm(projectedDiamondCornersReorder[i] - projectedDiamondCornersPose[i]); // TODO cvtest
  340. if(repError > 5.) {
  341. ts->printf(cvtest::TS::LOG, "Charuco pose error too high");
  342. ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
  343. return;
  344. }
  345. }
  346. }
  347. }
  348. }
  349. }
  350. /**
  351. * @brief Check charuco board creation
  352. */
  353. class CV_CharucoBoardCreation : public cvtest::BaseTest {
  354. public:
  355. CV_CharucoBoardCreation();
  356. protected:
  357. void run(int);
  358. };
  359. CV_CharucoBoardCreation::CV_CharucoBoardCreation() {}
  360. void CV_CharucoBoardCreation::run(int)
  361. {
  362. aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::DICT_5X5_250);
  363. int n = 6;
  364. float markerSizeFactor = 0.5f;
  365. for (float squareSize_mm = 5.0f; squareSize_mm < 35.0f; squareSize_mm += 0.1f)
  366. {
  367. aruco::CharucoBoard board_meters(Size(n, n), squareSize_mm*1e-3f,
  368. squareSize_mm * markerSizeFactor * 1e-3f, dictionary);
  369. aruco::CharucoBoard board_millimeters(Size(n, n), squareSize_mm,
  370. squareSize_mm * markerSizeFactor, dictionary);
  371. for (size_t i = 0; i < board_meters.getNearestMarkerIdx().size(); i++)
  372. {
  373. if (board_meters.getNearestMarkerIdx()[i].size() != board_millimeters.getNearestMarkerIdx()[i].size() ||
  374. board_meters.getNearestMarkerIdx()[i][0] != board_millimeters.getNearestMarkerIdx()[i][0])
  375. {
  376. ts->printf(cvtest::TS::LOG,
  377. cv::format("Charuco board topology is sensitive to scale with squareSize=%.1f\n",
  378. squareSize_mm).c_str());
  379. ts->set_failed_test_info(cvtest::TS::FAIL_INVALID_OUTPUT);
  380. break;
  381. }
  382. }
  383. }
  384. }
  385. TEST(CV_CharucoDetection, accuracy) {
  386. const bool legacyPattern = false;
  387. CV_CharucoDetection test(legacyPattern);
  388. test.safe_run();
  389. }
  390. TEST(CV_CharucoDetection, accuracy_legacyPattern) {
  391. const bool legacyPattern = true;
  392. CV_CharucoDetection test(legacyPattern);
  393. test.safe_run();
  394. }
  395. TEST(CV_CharucoPoseEstimation, accuracy) {
  396. const bool legacyPattern = false;
  397. CV_CharucoPoseEstimation test(legacyPattern);
  398. test.safe_run();
  399. }
  400. TEST(CV_CharucoPoseEstimation, accuracy_legacyPattern) {
  401. const bool legacyPattern = true;
  402. CV_CharucoPoseEstimation test(legacyPattern);
  403. test.safe_run();
  404. }
  405. TEST(CV_CharucoDiamondDetection, accuracy) {
  406. CV_CharucoDiamondDetection test;
  407. test.safe_run();
  408. }
  409. TEST(CV_CharucoBoardCreation, accuracy) {
  410. CV_CharucoBoardCreation test;
  411. test.safe_run();
  412. }
  413. TEST(Charuco, testCharucoCornersCollinear_true)
  414. {
  415. int squaresX = 13;
  416. int squaresY = 28;
  417. float squareLength = 300;
  418. float markerLength = 150;
  419. int dictionaryId = 11;
  420. aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::PredefinedDictionaryType(dictionaryId));
  421. aruco::CharucoBoard charucoBoard(Size(squaresX, squaresY), squareLength, markerLength, dictionary);
  422. // consistency with C++98
  423. const int arrLine[9] = {192, 204, 216, 228, 240, 252, 264, 276, 288};
  424. vector<int> charucoIdsAxisLine(9, 0);
  425. for (int i = 0; i < 9; i++){
  426. charucoIdsAxisLine[i] = arrLine[i];
  427. }
  428. const int arrDiag[7] = {198, 209, 220, 231, 242, 253, 264};
  429. vector<int> charucoIdsDiagonalLine(7, 0);
  430. for (int i = 0; i < 7; i++){
  431. charucoIdsDiagonalLine[i] = arrDiag[i];
  432. }
  433. bool resultAxisLine = charucoBoard.checkCharucoCornersCollinear(charucoIdsAxisLine);
  434. EXPECT_TRUE(resultAxisLine);
  435. bool resultDiagonalLine = charucoBoard.checkCharucoCornersCollinear(charucoIdsDiagonalLine);
  436. EXPECT_TRUE(resultDiagonalLine);
  437. }
  438. TEST(Charuco, testCharucoCornersCollinear_false)
  439. {
  440. int squaresX = 13;
  441. int squaresY = 28;
  442. float squareLength = 300;
  443. float markerLength = 150;
  444. int dictionaryId = 11;
  445. aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::PredefinedDictionaryType(dictionaryId));
  446. aruco::CharucoBoard charucoBoard(Size(squaresX, squaresY), squareLength, markerLength, dictionary);
  447. // consistency with C++98
  448. const int arr[63] = {192, 193, 194, 195, 196, 197, 198, 204, 205, 206, 207, 208,
  449. 209, 210, 216, 217, 218, 219, 220, 221, 222, 228, 229, 230,
  450. 231, 232, 233, 234, 240, 241, 242, 243, 244, 245, 246, 252,
  451. 253, 254, 255, 256, 257, 258, 264, 265, 266, 267, 268, 269,
  452. 270, 276, 277, 278, 279, 280, 281, 282, 288, 289, 290, 291,
  453. 292, 293, 294};
  454. vector<int> charucoIds(63, 0);
  455. for (int i = 0; i < 63; i++){
  456. charucoIds[i] = arr[i];
  457. }
  458. bool result = charucoBoard.checkCharucoCornersCollinear(charucoIds);
  459. EXPECT_FALSE(result);
  460. }
  461. // test that ChArUco board detection is subpixel accurate
  462. TEST(Charuco, testBoardSubpixelCoords)
  463. {
  464. cv::Size res{500, 500};
  465. cv::Mat K = (cv::Mat_<double>(3,3) <<
  466. 0.5*res.width, 0, 0.5*res.width,
  467. 0, 0.5*res.height, 0.5*res.height,
  468. 0, 0, 1);
  469. // set expected_corners values
  470. cv::Mat expected_corners = (cv::Mat_<float>(9,2) <<
  471. 200, 200,
  472. 250, 200,
  473. 300, 200,
  474. 200, 250,
  475. 250, 250,
  476. 300, 250,
  477. 200, 300,
  478. 250, 300,
  479. 300, 300
  480. );
  481. cv::Mat gray;
  482. aruco::Dictionary dict = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_APRILTAG_36h11);
  483. aruco::CharucoBoard board(Size(4, 4), 1.f, .8f, dict);
  484. // generate ChArUco board
  485. board.generateImage(Size(res.width, res.height), gray, 150);
  486. cv::GaussianBlur(gray, gray, Size(5, 5), 1.0);
  487. aruco::DetectorParameters params;
  488. params.cornerRefinementMethod = (int)cv::aruco::CORNER_REFINE_APRILTAG;
  489. aruco::CharucoParameters charucoParameters;
  490. charucoParameters.cameraMatrix = K;
  491. aruco::CharucoDetector detector(board, charucoParameters);
  492. detector.setDetectorParameters(params);
  493. std::vector<int> ids;
  494. std::vector<std::vector<cv::Point2f>> corners;
  495. cv::Mat c_ids, c_corners;
  496. detector.detectBoard(gray, c_corners, c_ids, corners, ids);
  497. ASSERT_EQ(ids.size(), size_t(8));
  498. ASSERT_EQ(c_corners.rows, expected_corners.rows);
  499. EXPECT_NEAR(0, cvtest::norm(expected_corners, c_corners.reshape(1), NORM_INF), 1e-1);
  500. }
  501. TEST(Charuco, issue_14014)
  502. {
  503. string imgPath = cvtest::findDataFile("aruco/recover.png");
  504. Mat img = imread(imgPath);
  505. aruco::DetectorParameters detectorParams;
  506. detectorParams.cornerRefinementMethod = (int)aruco::CORNER_REFINE_SUBPIX;
  507. detectorParams.cornerRefinementMinAccuracy = 0.01;
  508. aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_7X7_250), detectorParams);
  509. aruco::CharucoBoard board(Size(8, 5), 0.03455f, 0.02164f, detector.getDictionary());
  510. vector<Mat> corners, rejectedPoints;
  511. vector<int> ids;
  512. detector.detectMarkers(img, corners, ids, rejectedPoints);
  513. ASSERT_EQ(corners.size(), 19ull);
  514. EXPECT_EQ(Size(4, 1), corners[0].size()); // check dimension of detected corners
  515. size_t numRejPoints = rejectedPoints.size();
  516. ASSERT_EQ(rejectedPoints.size(), 24ull); // optional check to track regressions
  517. EXPECT_EQ(Size(4, 1), rejectedPoints[0].size()); // check dimension of detected corners
  518. detector.refineDetectedMarkers(img, board, corners, ids, rejectedPoints);
  519. ASSERT_EQ(corners.size(), 20ull);
  520. EXPECT_EQ(Size(4, 1), corners[0].size()); // check dimension of rejected corners after successfully refine
  521. ASSERT_EQ(rejectedPoints.size() + 1, numRejPoints);
  522. EXPECT_EQ(Size(4, 1), rejectedPoints[0].size()); // check dimension of rejected corners after successfully refine
  523. }
  524. TEST(Charuco, testmatchImagePoints)
  525. {
  526. aruco::CharucoBoard board(Size(2, 3), 1.f, 0.5f, aruco::getPredefinedDictionary(aruco::DICT_4X4_50));
  527. auto chessboardPoints = board.getChessboardCorners();
  528. vector<int> detectedIds;
  529. vector<Point2f> detectedCharucoCorners;
  530. for (const Point3f& point : chessboardPoints) {
  531. detectedIds.push_back((int)detectedCharucoCorners.size());
  532. detectedCharucoCorners.push_back({2.f*point.x, 2.f*point.y});
  533. }
  534. vector<Point3f> objPoints;
  535. vector<Point2f> imagePoints;
  536. board.matchImagePoints(detectedCharucoCorners, detectedIds, objPoints, imagePoints);
  537. ASSERT_EQ(detectedCharucoCorners.size(), objPoints.size());
  538. ASSERT_EQ(detectedCharucoCorners.size(), imagePoints.size());
  539. for (size_t i = 0ull; i < detectedCharucoCorners.size(); i++) {
  540. EXPECT_EQ(detectedCharucoCorners[i], imagePoints[i]);
  541. EXPECT_EQ(chessboardPoints[i].x, objPoints[i].x);
  542. EXPECT_EQ(chessboardPoints[i].y, objPoints[i].y);
  543. }
  544. }
  545. typedef testing::TestWithParam<int> CharucoDraw;
  546. INSTANTIATE_TEST_CASE_P(/**/, CharucoDraw, testing::Values(CV_8UC2, CV_8SC2, CV_16UC2, CV_16SC2, CV_32SC2, CV_32FC2, CV_64FC2));
  547. TEST_P(CharucoDraw, testDrawDetected) {
  548. vector<vector<Point>> detected_golds = {{Point(20, 20), Point(80, 20), Point(80, 80), Point2f(20, 80)}};
  549. Point center_gold = (detected_golds[0][0] + detected_golds[0][1] + detected_golds[0][2] + detected_golds[0][3]) / 4;
  550. int type = GetParam();
  551. vector<Mat> detected(detected_golds[0].size(), Mat(4, 1, type));
  552. // copy detected_golds to detected with any 2 channels type
  553. for (size_t i = 0ull; i < detected_golds[0].size(); i++) {
  554. detected[0].row((int)i) = Scalar(detected_golds[0][i].x, detected_golds[0][i].y);
  555. }
  556. vector<vector<Point>> contours;
  557. Point detectedCenter;
  558. Moments m;
  559. Mat img;
  560. // check drawDetectedMarkers
  561. img = Mat::zeros(100, 100, CV_8UC1);
  562. ASSERT_NO_THROW(aruco::drawDetectedMarkers(img, detected, noArray(), Scalar(255, 255, 255)));
  563. // check that the marker borders are painted
  564. findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
  565. ASSERT_EQ(contours.size(), 1ull);
  566. m = moments(contours[0]);
  567. detectedCenter = Point(cvRound(m.m10/m.m00), cvRound(m.m01/m.m00));
  568. ASSERT_EQ(detectedCenter, center_gold);
  569. // check drawDetectedCornersCharuco
  570. img = Mat::zeros(100, 100, CV_8UC1);
  571. ASSERT_NO_THROW(aruco::drawDetectedCornersCharuco(img, detected[0], noArray(), Scalar(255, 255, 255)));
  572. // check that the 4 charuco corners are painted
  573. findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
  574. ASSERT_EQ(contours.size(), 4ull);
  575. for (size_t i = 0ull; i < 4ull; i++) {
  576. m = moments(contours[i]);
  577. detectedCenter = Point(cvRound(m.m10/m.m00), cvRound(m.m01/m.m00));
  578. // detectedCenter must be in detected_golds
  579. ASSERT_TRUE(find(detected_golds[0].begin(), detected_golds[0].end(), detectedCenter) != detected_golds[0].end());
  580. }
  581. // check drawDetectedDiamonds
  582. img = Mat::zeros(100, 100, CV_8UC1);
  583. ASSERT_NO_THROW(aruco::drawDetectedDiamonds(img, detected, noArray(), Scalar(255, 255, 255)));
  584. // check that the diamonds borders are painted
  585. findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
  586. ASSERT_EQ(contours.size(), 1ull);
  587. m = moments(contours[0]);
  588. detectedCenter = Point(cvRound(m.m10/m.m00), cvRound(m.m01/m.m00));
  589. ASSERT_EQ(detectedCenter, center_gold);
  590. }
  591. typedef testing::TestWithParam<cv::Size> CharucoBoard;
  592. INSTANTIATE_TEST_CASE_P(/**/, CharucoBoard, testing::Values(Size(3, 2), Size(3, 2), Size(6, 2), Size(2, 6),
  593. Size(3, 4), Size(4, 3), Size(7, 3), Size(3, 7)));
  594. TEST_P(CharucoBoard, testWrongSizeDetection)
  595. {
  596. cv::Size boardSize = GetParam();
  597. ASSERT_FALSE(boardSize.width == boardSize.height);
  598. aruco::CharucoBoard board(boardSize, 1.f, 0.5f, aruco::getPredefinedDictionary(aruco::DICT_4X4_50));
  599. Mat boardImage;
  600. board.generateImage(boardSize*40, boardImage);
  601. swap(boardSize.width, boardSize.height);
  602. aruco::CharucoDetector detector(aruco::CharucoBoard(boardSize, 1.f, 0.5f, aruco::getPredefinedDictionary(aruco::DICT_4X4_50)));
  603. // try detect board with wrong size
  604. for(int i: {0, 1}) {
  605. vector<int> detectedCharucoIds, detectedArucoIds;
  606. vector<Point2f> detectedCharucoCorners;
  607. vector<vector<Point2f>> detectedArucoCorners;
  608. if (i == 0) {
  609. detector.detectBoard(boardImage, detectedCharucoCorners, detectedCharucoIds, detectedArucoCorners, detectedArucoIds);
  610. // aruco markers must be found
  611. ASSERT_EQ(detectedArucoIds.size(), board.getIds().size());
  612. ASSERT_EQ(detectedArucoCorners.size(), board.getIds().size());
  613. } else {
  614. detector.detectBoard(boardImage, detectedCharucoCorners, detectedCharucoIds);
  615. }
  616. // charuco corners should not be found in board with wrong size
  617. ASSERT_TRUE(detectedCharucoCorners.empty());
  618. ASSERT_TRUE(detectedCharucoIds.empty());
  619. }
  620. }
  621. typedef testing::TestWithParam<std::tuple<cv::Size, float, cv::Size, int>> CharucoBoardGenerate;
  622. INSTANTIATE_TEST_CASE_P(/**/, CharucoBoardGenerate, testing::Values(make_tuple(Size(7, 4), 13.f, Size(400, 300), 24),
  623. make_tuple(Size(12, 2), 13.f, Size(200, 150), 1),
  624. make_tuple(Size(12, 2), 13.1f, Size(400, 300), 1)));
  625. TEST_P(CharucoBoardGenerate, issue_24806)
  626. {
  627. aruco::Dictionary dict = aruco::getPredefinedDictionary(aruco::DICT_4X4_1000);
  628. auto params = GetParam();
  629. const Size boardSize = std::get<0>(params);
  630. const float squareLength = std::get<1>(params), markerLength = 10.f;
  631. Size imgSize = std::get<2>(params);
  632. const aruco::CharucoBoard board(boardSize, squareLength, markerLength, dict);
  633. const int marginSize = std::get<3>(params);
  634. Mat boardImg;
  635. // generate chessboard image
  636. board.generateImage(imgSize, boardImg, marginSize);
  637. // This condition checks that the width of the image determines the dimensions of the chessboard in this test
  638. CV_Assert((float)(boardImg.cols) / (float)boardSize.width <=
  639. (float)(boardImg.rows) / (float)boardSize.height);
  640. // prepare data for chessboard image test
  641. Mat noMarginsImg = boardImg(Range(marginSize, boardImg.rows - marginSize),
  642. Range(marginSize, boardImg.cols - marginSize));
  643. const float pixInSquare = (float)(noMarginsImg.cols) / (float)boardSize.width;
  644. Size pixInChessboard(cvRound(pixInSquare*boardSize.width), cvRound(pixInSquare*boardSize.height));
  645. const Point startChessboard((noMarginsImg.cols - pixInChessboard.width) / 2,
  646. (noMarginsImg.rows - pixInChessboard.height) / 2);
  647. Mat chessboardZoneImg = noMarginsImg(Rect(startChessboard, pixInChessboard));
  648. // B - black pixel, W - white pixel
  649. // chessboard corner 1:
  650. // B W
  651. // W B
  652. Mat goldCorner1 = (Mat_<uint8_t>(2, 2) <<
  653. 0, 255,
  654. 255, 0);
  655. // B - black pixel, W - white pixel
  656. // chessboard corner 2:
  657. // W B
  658. // B W
  659. Mat goldCorner2 = (Mat_<uint8_t>(2, 2) <<
  660. 255, 0,
  661. 0, 255);
  662. // test chessboard corners in generated image
  663. for (const Point3f& p: board.getChessboardCorners()) {
  664. Point2f chessCorner(pixInSquare*(p.x/squareLength),
  665. pixInSquare*(p.y/squareLength));
  666. Mat winCorner = chessboardZoneImg(Rect(Point(cvRound(chessCorner.x) - 1, cvRound(chessCorner.y) - 1), Size(2, 2)));
  667. bool eq = (cv::countNonZero(goldCorner1 != winCorner) == 0) || (cv::countNonZero(goldCorner2 != winCorner) == 0);
  668. ASSERT_TRUE(eq);
  669. }
  670. // marker size in pixels
  671. const float pixInMarker = markerLength/squareLength*pixInSquare;
  672. // the size of the marker margin in pixels
  673. const float pixInMarginMarker = 0.5f*(pixInSquare - pixInMarker);
  674. // determine the zone where the aruco markers are located
  675. int endArucoX = cvRound(pixInSquare*(boardSize.width-1)+pixInMarginMarker+pixInMarker);
  676. int endArucoY = cvRound(pixInSquare*(boardSize.height-1)+pixInMarginMarker+pixInMarker);
  677. Mat arucoZone = chessboardZoneImg(Range(cvRound(pixInMarginMarker), endArucoY), Range(cvRound(pixInMarginMarker), endArucoX));
  678. const auto& markerCorners = board.getObjPoints();
  679. float minX, maxX, minY, maxY;
  680. minX = maxX = markerCorners[0][0].x;
  681. minY = maxY = markerCorners[0][0].y;
  682. for (const auto& marker : markerCorners) {
  683. for (const Point3f& objCorner : marker) {
  684. minX = min(minX, objCorner.x);
  685. maxX = max(maxX, objCorner.x);
  686. minY = min(minY, objCorner.y);
  687. maxY = max(maxY, objCorner.y);
  688. }
  689. }
  690. Point2f outCorners[3];
  691. for (const auto& marker : markerCorners) {
  692. for (int i = 0; i < 3; i++) {
  693. outCorners[i] = Point2f(marker[i].x, marker[i].y) - Point2f(minX, minY);
  694. outCorners[i].x = outCorners[i].x / (maxX - minX) * float(arucoZone.cols);
  695. outCorners[i].y = outCorners[i].y / (maxY - minY) * float(arucoZone.rows);
  696. }
  697. Size dst_sz(outCorners[2] - outCorners[0]); // assuming CCW order
  698. dst_sz.width = dst_sz.height = std::min(dst_sz.width, dst_sz.height);
  699. Rect borderRect = Rect(outCorners[0], dst_sz);
  700. //The test checks the inner and outer borders of the Aruco markers.
  701. //In the inner border of Aruco marker, all pixels should be black.
  702. //In the outer border of Aruco marker, all pixels should be white.
  703. Mat markerImg = arucoZone(borderRect);
  704. bool markerBorderIsBlack = borderPixelsHaveSameColor(markerImg, 0);
  705. ASSERT_EQ(markerBorderIsBlack, true);
  706. Mat markerOuterBorder = markerImg;
  707. markerOuterBorder.adjustROI(1, 1, 1, 1);
  708. bool markerOuterBorderIsWhite = borderPixelsHaveSameColor(markerOuterBorder, 255);
  709. ASSERT_EQ(markerOuterBorderIsWhite, true);
  710. }
  711. }
  712. TEST(Charuco, testSeveralBoardsWithCustomIds)
  713. {
  714. Size res{500, 500};
  715. Mat K = (Mat_<double>(3,3) <<
  716. 0.5*res.width, 0, 0.5*res.width,
  717. 0, 0.5*res.height, 0.5*res.height,
  718. 0, 0, 1);
  719. Mat expected_corners = (Mat_<float>(9,2) <<
  720. 200, 200,
  721. 250, 200,
  722. 300, 200,
  723. 200, 250,
  724. 250, 250,
  725. 300, 250,
  726. 200, 300,
  727. 250, 300,
  728. 300, 300
  729. );
  730. aruco::Dictionary dict = cv::aruco::getPredefinedDictionary(aruco::DICT_4X4_50);
  731. vector<int> ids1 = {0, 1, 33, 3, 4, 5, 6, 8}, ids2 = {7, 9, 44, 11, 12, 13, 14, 15};
  732. aruco::CharucoBoard board1(Size(4, 4), 1.f, .8f, dict, ids1), board2(Size(4, 4), 1.f, .8f, dict, ids2);
  733. // generate ChArUco board
  734. Mat gray;
  735. {
  736. Mat gray1, gray2;
  737. board1.generateImage(Size(res.width, res.height), gray1, 150);
  738. board2.generateImage(Size(res.width, res.height), gray2, 150);
  739. hconcat(gray1, gray2, gray);
  740. }
  741. aruco::CharucoParameters charucoParameters;
  742. charucoParameters.cameraMatrix = K;
  743. aruco::CharucoDetector detector1(board1, charucoParameters), detector2(board2, charucoParameters);
  744. vector<int> ids;
  745. vector<Mat> corners;
  746. Mat c_ids1, c_ids2, c_corners1, c_corners2;
  747. detector1.detectBoard(gray, c_corners1, c_ids1, corners, ids);
  748. detector2.detectBoard(gray, c_corners2, c_ids2, corners, ids);
  749. ASSERT_EQ(ids.size(), size_t(16));
  750. // In 4.x detectBoard() returns the charuco corners in a 2D Mat with shape (N_corners, 1)
  751. // In 5.x, after PR #23473, detectBoard() returns the charuco corners in a 1D Mat with shape (1, N_corners)
  752. ASSERT_EQ(expected_corners.total(), c_corners1.total()*c_corners1.channels());
  753. EXPECT_NEAR(0., cvtest::norm(expected_corners.reshape(1, 1), c_corners1.reshape(1, 1), NORM_INF), 3e-1);
  754. ASSERT_EQ(expected_corners.total(), c_corners2.total()*c_corners2.channels());
  755. expected_corners.col(0) += 500;
  756. EXPECT_NEAR(0., cvtest::norm(expected_corners.reshape(1, 1), c_corners2.reshape(1, 1), NORM_INF), 3e-1);
  757. }
  758. }} // namespace