test_arucodetection.cpp 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354
  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 "opencv2/objdetect/aruco_detector.hpp"
  6. #include "opencv2/calib3d.hpp"
  7. namespace cv {
  8. namespace aruco {
  9. bool operator==(const Dictionary& d1, const Dictionary& d2);
  10. bool operator==(const Dictionary& d1, const Dictionary& d2) {
  11. return d1.markerSize == d2.markerSize
  12. && std::equal(d1.bytesList.begin<Vec<uint8_t, 4>>(), d1.bytesList.end<Vec<uint8_t, 4>>(), d2.bytesList.begin<Vec<uint8_t, 4>>())
  13. && std::equal(d2.bytesList.begin<Vec<uint8_t, 4>>(), d2.bytesList.end<Vec<uint8_t, 4>>(), d1.bytesList.begin<Vec<uint8_t, 4>>())
  14. && d1.maxCorrectionBits == d2.maxCorrectionBits;
  15. };
  16. }
  17. }
  18. namespace opencv_test { namespace {
  19. /**
  20. * @brief Draw 2D synthetic markers and detect them
  21. */
  22. class CV_ArucoDetectionSimple : public cvtest::BaseTest {
  23. public:
  24. CV_ArucoDetectionSimple();
  25. protected:
  26. void run(int);
  27. };
  28. CV_ArucoDetectionSimple::CV_ArucoDetectionSimple() {}
  29. void CV_ArucoDetectionSimple::run(int) {
  30. aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250));
  31. // 20 images
  32. for(int i = 0; i < 20; i++) {
  33. const int markerSidePixels = 100;
  34. int imageSize = markerSidePixels * 2 + 3 * (markerSidePixels / 2);
  35. // draw synthetic image and store marker corners and ids
  36. vector<vector<Point2f> > groundTruthCorners;
  37. vector<int> groundTruthIds;
  38. Mat img = Mat(imageSize, imageSize, CV_8UC1, Scalar::all(255));
  39. for(int y = 0; y < 2; y++) {
  40. for(int x = 0; x < 2; x++) {
  41. Mat marker;
  42. int id = i * 4 + y * 2 + x;
  43. aruco::generateImageMarker(detector.getDictionary(), id, markerSidePixels, marker);
  44. Point2f firstCorner =
  45. Point2f(markerSidePixels / 2.f + x * (1.5f * markerSidePixels),
  46. markerSidePixels / 2.f + y * (1.5f * markerSidePixels));
  47. Mat aux = img.colRange((int)firstCorner.x, (int)firstCorner.x + markerSidePixels)
  48. .rowRange((int)firstCorner.y, (int)firstCorner.y + markerSidePixels);
  49. marker.copyTo(aux);
  50. groundTruthIds.push_back(id);
  51. groundTruthCorners.push_back(vector<Point2f>());
  52. groundTruthCorners.back().push_back(firstCorner);
  53. groundTruthCorners.back().push_back(firstCorner + Point2f(markerSidePixels - 1, 0));
  54. groundTruthCorners.back().push_back(
  55. firstCorner + Point2f(markerSidePixels - 1, markerSidePixels - 1));
  56. groundTruthCorners.back().push_back(firstCorner + Point2f(0, markerSidePixels - 1));
  57. }
  58. }
  59. if(i % 2 == 1) img.convertTo(img, CV_8UC3);
  60. // detect markers
  61. vector<vector<Point2f> > corners;
  62. vector<int> ids;
  63. detector.detectMarkers(img, corners, ids);
  64. // check detection results
  65. for(unsigned int m = 0; m < groundTruthIds.size(); m++) {
  66. int idx = -1;
  67. for(unsigned int k = 0; k < ids.size(); k++) {
  68. if(groundTruthIds[m] == ids[k]) {
  69. idx = (int)k;
  70. break;
  71. }
  72. }
  73. if(idx == -1) {
  74. ts->printf(cvtest::TS::LOG, "Marker not detected");
  75. ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
  76. return;
  77. }
  78. for(int c = 0; c < 4; c++) {
  79. double dist = cv::norm(groundTruthCorners[m][c] - corners[idx][c]); // TODO cvtest
  80. if(dist > 0.001) {
  81. ts->printf(cvtest::TS::LOG, "Incorrect marker corners position");
  82. ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
  83. return;
  84. }
  85. }
  86. }
  87. }
  88. }
  89. static double deg2rad(double deg) { return deg * CV_PI / 180.; }
  90. /**
  91. * @brief Get rvec and tvec from yaw, pitch and distance
  92. */
  93. static void getSyntheticRT(double yaw, double pitch, double distance, Mat &rvec, Mat &tvec) {
  94. rvec = Mat(3, 1, CV_64FC1);
  95. tvec = Mat(3, 1, CV_64FC1);
  96. // Rvec
  97. // first put the Z axis aiming to -X (like the camera axis system)
  98. Mat rotZ(3, 1, CV_64FC1);
  99. rotZ.ptr<double>(0)[0] = 0;
  100. rotZ.ptr<double>(0)[1] = 0;
  101. rotZ.ptr<double>(0)[2] = -0.5 * CV_PI;
  102. Mat rotX(3, 1, CV_64FC1);
  103. rotX.ptr<double>(0)[0] = 0.5 * CV_PI;
  104. rotX.ptr<double>(0)[1] = 0;
  105. rotX.ptr<double>(0)[2] = 0;
  106. Mat camRvec, camTvec;
  107. composeRT(rotZ, Mat(3, 1, CV_64FC1, Scalar::all(0)), rotX, Mat(3, 1, CV_64FC1, Scalar::all(0)),
  108. camRvec, camTvec);
  109. // now pitch and yaw angles
  110. Mat rotPitch(3, 1, CV_64FC1);
  111. rotPitch.ptr<double>(0)[0] = 0;
  112. rotPitch.ptr<double>(0)[1] = pitch;
  113. rotPitch.ptr<double>(0)[2] = 0;
  114. Mat rotYaw(3, 1, CV_64FC1);
  115. rotYaw.ptr<double>(0)[0] = yaw;
  116. rotYaw.ptr<double>(0)[1] = 0;
  117. rotYaw.ptr<double>(0)[2] = 0;
  118. composeRT(rotPitch, Mat(3, 1, CV_64FC1, Scalar::all(0)), rotYaw,
  119. Mat(3, 1, CV_64FC1, Scalar::all(0)), rvec, tvec);
  120. // compose both rotations
  121. composeRT(camRvec, Mat(3, 1, CV_64FC1, Scalar::all(0)), rvec,
  122. Mat(3, 1, CV_64FC1, Scalar::all(0)), rvec, tvec);
  123. // Tvec, just move in z (camera) direction the specific distance
  124. tvec.ptr<double>(0)[0] = 0.;
  125. tvec.ptr<double>(0)[1] = 0.;
  126. tvec.ptr<double>(0)[2] = distance;
  127. }
  128. /**
  129. * @brief Create a synthetic image of a marker with perspective
  130. */
  131. static Mat projectMarker(const aruco::Dictionary &dictionary, int id, Mat cameraMatrix, double yaw,
  132. double pitch, double distance, Size imageSize, int markerBorder,
  133. vector<Point2f> &corners, int encloseMarker=0) {
  134. // canonical image
  135. Mat marker, markerImg;
  136. const int markerSizePixels = 100;
  137. aruco::generateImageMarker(dictionary, id, markerSizePixels, marker, markerBorder);
  138. marker.copyTo(markerImg);
  139. if(encloseMarker){ //to enclose the marker
  140. int enclose = int(marker.rows/4);
  141. markerImg = Mat::zeros(marker.rows+(2*enclose), marker.cols+(enclose*2), CV_8UC1);
  142. Mat field= markerImg.rowRange(int(enclose), int(markerImg.rows-enclose))
  143. .colRange(int(0), int(markerImg.cols));
  144. field.setTo(255);
  145. field= markerImg.rowRange(int(0), int(markerImg.rows))
  146. .colRange(int(enclose), int(markerImg.cols-enclose));
  147. field.setTo(255);
  148. field = markerImg(Rect(enclose,enclose,marker.rows,marker.cols));
  149. marker.copyTo(field);
  150. }
  151. // get rvec and tvec for the perspective
  152. Mat rvec, tvec;
  153. getSyntheticRT(yaw, pitch, distance, rvec, tvec);
  154. const float markerLength = 0.05f;
  155. vector<Point3f> markerObjPoints;
  156. markerObjPoints.push_back(Point3f(-markerLength / 2.f, +markerLength / 2.f, 0));
  157. markerObjPoints.push_back(markerObjPoints[0] + Point3f(markerLength, 0, 0));
  158. markerObjPoints.push_back(markerObjPoints[0] + Point3f(markerLength, -markerLength, 0));
  159. markerObjPoints.push_back(markerObjPoints[0] + Point3f(0, -markerLength, 0));
  160. // project markers and draw them
  161. Mat distCoeffs(5, 1, CV_64FC1, Scalar::all(0));
  162. projectPoints(markerObjPoints, rvec, tvec, cameraMatrix, distCoeffs, corners);
  163. vector<Point2f> originalCorners;
  164. originalCorners.push_back(Point2f(0+float(encloseMarker*markerSizePixels/4), 0+float(encloseMarker*markerSizePixels/4)));
  165. originalCorners.push_back(originalCorners[0]+Point2f((float)markerSizePixels, 0));
  166. originalCorners.push_back(originalCorners[0]+Point2f((float)markerSizePixels, (float)markerSizePixels));
  167. originalCorners.push_back(originalCorners[0]+Point2f(0, (float)markerSizePixels));
  168. Mat transformation = getPerspectiveTransform(originalCorners, corners);
  169. Mat img(imageSize, CV_8UC1, Scalar::all(255));
  170. Mat aux;
  171. const char borderValue = 127;
  172. warpPerspective(markerImg, aux, transformation, imageSize, INTER_NEAREST, BORDER_CONSTANT,
  173. Scalar::all(borderValue));
  174. // copy only not-border pixels
  175. for(int y = 0; y < aux.rows; y++) {
  176. for(int x = 0; x < aux.cols; x++) {
  177. if(aux.at<unsigned char>(y, x) == borderValue) continue;
  178. img.at<unsigned char>(y, x) = aux.at<unsigned char>(y, x);
  179. }
  180. }
  181. return img;
  182. }
  183. enum class ArucoAlgParams
  184. {
  185. USE_DEFAULT = 0,
  186. USE_APRILTAG=1, /// Detect marker candidates :: using AprilTag
  187. DETECT_INVERTED_MARKER, /// Check if there is a white marker
  188. USE_ARUCO3 /// Check if aruco3 should be used
  189. };
  190. /**
  191. * @brief Draws markers in perspective and detect them
  192. */
  193. class CV_ArucoDetectionPerspective : public cvtest::BaseTest {
  194. public:
  195. CV_ArucoDetectionPerspective(ArucoAlgParams arucoAlgParam) : arucoAlgParams(arucoAlgParam) {}
  196. protected:
  197. void run(int);
  198. ArucoAlgParams arucoAlgParams;
  199. };
  200. void CV_ArucoDetectionPerspective::run(int) {
  201. int iter = 0;
  202. int szEnclosed = 0;
  203. Mat cameraMatrix = Mat::eye(3, 3, CV_64FC1);
  204. Size imgSize(500, 500);
  205. cameraMatrix.at<double>(0, 0) = cameraMatrix.at<double>(1, 1) = 650;
  206. cameraMatrix.at<double>(0, 2) = imgSize.width / 2;
  207. cameraMatrix.at<double>(1, 2) = imgSize.height / 2;
  208. aruco::DetectorParameters params;
  209. params.minDistanceToBorder = 1;
  210. aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params);
  211. // detect from different positions
  212. for(double distance : {0.1, 0.3, 0.5, 0.7}) {
  213. for(int pitch = 0; pitch < 360; pitch += (distance == 0.1? 60:180)) {
  214. for(int yaw = 70; yaw <= 120; yaw += 40){
  215. int currentId = iter % 250;
  216. int markerBorder = iter % 2 + 1;
  217. iter++;
  218. vector<Point2f> groundTruthCorners;
  219. aruco::DetectorParameters detectorParameters = params;
  220. detectorParameters.markerBorderBits = markerBorder;
  221. /// create synthetic image
  222. Mat img=
  223. projectMarker(detector.getDictionary(), currentId, cameraMatrix, deg2rad(yaw), deg2rad(pitch),
  224. distance, imgSize, markerBorder, groundTruthCorners, szEnclosed);
  225. // marker :: Inverted
  226. if(ArucoAlgParams::DETECT_INVERTED_MARKER == arucoAlgParams){
  227. img = ~img;
  228. detectorParameters.detectInvertedMarker = true;
  229. }
  230. if(ArucoAlgParams::USE_APRILTAG == arucoAlgParams){
  231. detectorParameters.cornerRefinementMethod = (int)aruco::CORNER_REFINE_APRILTAG;
  232. }
  233. if (ArucoAlgParams::USE_ARUCO3 == arucoAlgParams) {
  234. detectorParameters.useAruco3Detection = true;
  235. detectorParameters.cornerRefinementMethod = (int)aruco::CORNER_REFINE_SUBPIX;
  236. }
  237. detector.setDetectorParameters(detectorParameters);
  238. // detect markers
  239. vector<vector<Point2f> > corners;
  240. vector<int> ids;
  241. detector.detectMarkers(img, corners, ids);
  242. // check results
  243. if(ids.size() != 1 || (ids.size() == 1 && ids[0] != currentId)) {
  244. if(ids.size() != 1)
  245. ts->printf(cvtest::TS::LOG, "Incorrect number of detected markers");
  246. else
  247. ts->printf(cvtest::TS::LOG, "Incorrect marker id");
  248. ts->set_failed_test_info(cvtest::TS::FAIL_MISMATCH);
  249. return;
  250. }
  251. for(int c = 0; c < 4; c++) {
  252. double dist = cv::norm(groundTruthCorners[c] - corners[0][c]); // TODO cvtest
  253. if(dist > 5) {
  254. ts->printf(cvtest::TS::LOG, "Incorrect marker corners position");
  255. ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
  256. return;
  257. }
  258. }
  259. }
  260. }
  261. // change the state :: to detect an enclosed inverted marker
  262. if(ArucoAlgParams::DETECT_INVERTED_MARKER == arucoAlgParams && distance == 0.1){
  263. distance -= 0.1;
  264. szEnclosed++;
  265. }
  266. }
  267. }
  268. // Helper struct and functions for CV_ArucoDetectionConfidence
  269. // Inverts a square subregion inside selected cells of a marker to simulate a confidence drop
  270. enum class MarkerRegionToTemper {
  271. BORDER, // Only invert cells within the marker border bits
  272. INNER, // Only invert cells in the inner part of the marker (excluding borders)
  273. ALL // Invert any cells
  274. };
  275. // Define the characteristics of cell inversions
  276. struct MarkerTemperingConfig {
  277. float cellRatioToTemper; // [0,1] ratio of the cell to invert
  278. int numCellsToTemper; // Number of cells to invert
  279. MarkerRegionToTemper markerRegionToTemper; // Which cells to invert (BORDER, INNER, ALL)
  280. };
  281. // Test configs for CV_ArucoDetectionConfidence
  282. struct ArucoConfidenceTestConfig {
  283. MarkerTemperingConfig markerTemperingConfig; // Configuration of cells to invert (percentage, number and markerRegionToTemper)
  284. float perspectiveRemoveIgnoredMarginPerCell; // Width of the margin of pixels on each cell not considered for the marker identification
  285. int markerBorderBits; // Number of bits of the marker border
  286. float distortionRatio; // Percentage of offset used for perspective distortion, bigger means more distorted
  287. };
  288. enum class markerRot
  289. {
  290. NONE = 0,
  291. ROT_90,
  292. ROT_180,
  293. ROT_270
  294. };
  295. struct markerDetectionGT {
  296. int id; // Marker identification
  297. double confidence; // Pixel-based confidence defined as 1 - (inverted area / total area)
  298. bool expectDetection; // True if we expect to detect the marker
  299. };
  300. struct MarkerCreationConfig {
  301. int id; // Marker identification
  302. int markerSidePixels; // Marker size (in pixels)
  303. markerRot rotation; // Rotation of the marker in degrees (0, 90, 180, 270)
  304. };
  305. void rotateMarker(Mat &marker, const markerRot rotation)
  306. {
  307. if(rotation == markerRot::NONE)
  308. return;
  309. if (rotation == markerRot::ROT_90) {
  310. cv::transpose(marker, marker);
  311. cv::flip(marker, marker, 0);
  312. } else if (rotation == markerRot::ROT_180) {
  313. cv::flip(marker, marker, -1);
  314. } else if (rotation == markerRot::ROT_270) {
  315. cv::transpose(marker, marker);
  316. cv::flip(marker, marker, 1);
  317. }
  318. }
  319. void distortMarker(Mat &marker, const float distortionRatio)
  320. {
  321. if (distortionRatio < FLT_EPSILON)
  322. return;
  323. // apply a distortion (a perspective warp) to simulate a non-ideal capture
  324. vector<Point2f> src = { {0, 0},
  325. {static_cast<float>(marker.cols), 0},
  326. {static_cast<float>(marker.cols), static_cast<float>(marker.rows)},
  327. {0, static_cast<float>(marker.rows)} };
  328. float offset = marker.cols * distortionRatio; // distortionRatio % offset for distortion
  329. vector<Point2f> dst = { {offset, offset},
  330. {marker.cols - offset, 0},
  331. {marker.cols - offset, marker.rows - offset},
  332. {0, marker.rows - offset} };
  333. Mat M = getPerspectiveTransform(src, dst);
  334. warpPerspective(marker, marker, M, marker.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(255));
  335. }
  336. /**
  337. * @brief Inverts a square subregion inside selected cells of a marker image to simulate confidence degradation.
  338. *
  339. * The function computes the marker grid parameters and then applies a bitwise inversion
  340. * on a square markerRegionToTemper inside the chosen cells. The number of cells to be inverted is determined by
  341. * the parameter 'numCellsToTemper'. The candidate cells can be filtered to only include border cells,
  342. * inner cells, or all cells according to the parameter 'markerRegionToTemper'.
  343. *
  344. * @param marker The marker image
  345. * @param markerSidePixels The total size of the marker in pixels (inner and border).
  346. * @param markerId The id of the marker
  347. * @param params The Aruco detector configuration (provides border bits, margin ratios, etc.).
  348. * @param dictionary The Aruco marker dictionary (used to determine marker grid size).
  349. * @param cellTempConfig Cell tempering config as defined in MarkerTemperingConfig
  350. * @return Cell tempering ground truth as defined in markerDetectionGT
  351. */
  352. markerDetectionGT applyTemperingToMarkerCells(cv::Mat &marker,
  353. const int markerSidePixels,
  354. const int markerId,
  355. const aruco::DetectorParameters &params,
  356. const aruco::Dictionary &dictionary,
  357. const MarkerTemperingConfig &cellTempConfig)
  358. {
  359. // nothing to invert
  360. if(cellTempConfig.numCellsToTemper <= 0 || cellTempConfig.cellRatioToTemper <= FLT_EPSILON)
  361. return {markerId, 1.0, true};
  362. // compute the overall grid dimensions.
  363. const int markerSizeWithBorders = dictionary.markerSize + 2 * params.markerBorderBits;
  364. const int cellSidePixelsSize = markerSidePixels / markerSizeWithBorders;
  365. // compute the margin within each cell used for identification.
  366. const int cellMarginPixels = static_cast<int>(params.perspectiveRemoveIgnoredMarginPerCell * cellSidePixelsSize);
  367. const int innerCellSizePixels = cellSidePixelsSize - 2 * cellMarginPixels;
  368. // determine the size of the square that will be inverted in each cell.
  369. // (cellSidePixelsInvert / innerCellSizePixels)^2 should equal cellRatioToTemper.
  370. const int cellSidePixelsInvert = min(cellSidePixelsSize, static_cast<int>(innerCellSizePixels * std::sqrt(cellTempConfig.cellRatioToTemper)));
  371. const int inversionOffsetPixels = (cellSidePixelsSize - cellSidePixelsInvert) / 2;
  372. // nothing to invert
  373. if(cellSidePixelsInvert <= 0)
  374. return {markerId, 1.0, true};
  375. int cellsTempered = 0;
  376. int borderErrors = 0;
  377. int innerCellsErrors = 0;
  378. // iterate over each cell in the grid.
  379. for (int row = 0; row < markerSizeWithBorders; row++) {
  380. for (int col = 0; col < markerSizeWithBorders; col++) {
  381. // decide if this cell falls in the markerRegionToTemper to temper.
  382. const bool isBorder = (row < params.markerBorderBits ||
  383. col < params.markerBorderBits ||
  384. row >= markerSizeWithBorders - params.markerBorderBits ||
  385. col >= markerSizeWithBorders - params.markerBorderBits);
  386. const bool inRegion = (cellTempConfig.markerRegionToTemper == MarkerRegionToTemper::ALL ||
  387. (isBorder && cellTempConfig.markerRegionToTemper == MarkerRegionToTemper::BORDER) ||
  388. (!isBorder && cellTempConfig.markerRegionToTemper == MarkerRegionToTemper::INNER));
  389. // apply the inversion to simulate tempering.
  390. if (inRegion && (cellsTempered < cellTempConfig.numCellsToTemper)) {
  391. const int xStart = col * cellSidePixelsSize + inversionOffsetPixels;
  392. const int yStart = row * cellSidePixelsSize + inversionOffsetPixels;
  393. cv::Rect cellRect(xStart, yStart, cellSidePixelsInvert, cellSidePixelsInvert);
  394. cv::Mat cellROI = marker(cellRect);
  395. cv::bitwise_not(cellROI, cellROI);
  396. ++cellsTempered;
  397. // cell too tempered, no detection expected
  398. if(cellTempConfig.cellRatioToTemper > 0.5f) {
  399. if(isBorder){
  400. ++borderErrors;
  401. } else {
  402. ++innerCellsErrors;
  403. }
  404. }
  405. }
  406. if(cellsTempered >= cellTempConfig.numCellsToTemper)
  407. break;
  408. }
  409. if(cellsTempered >= cellTempConfig.numCellsToTemper)
  410. break;
  411. }
  412. // compute the ground-truth confidence
  413. const double invertedArea = cellsTempered * cellSidePixelsInvert * cellSidePixelsInvert;
  414. const double totalDetectionArea = markerSizeWithBorders * innerCellSizePixels * markerSizeWithBorders * innerCellSizePixels;
  415. const double groundTruthConfidence = std::max(0.0, 1.0 - invertedArea / totalDetectionArea);
  416. // check if marker is expected to be detected
  417. const int maximumErrorsInBorder = static_cast<int>(dictionary.markerSize * dictionary.markerSize * params.maxErroneousBitsInBorderRate);
  418. const int maxCorrectionRecalculed = static_cast<int>(dictionary.maxCorrectionBits * params.errorCorrectionRate);
  419. const bool expectDetection = static_cast<bool>(borderErrors <= maximumErrorsInBorder && innerCellsErrors <= maxCorrectionRecalculed);
  420. return {markerId, groundTruthConfidence, expectDetection};
  421. }
  422. /**
  423. * @brief Create an image of a marker with inverted (tempered) regions to simulate detection confidence
  424. *
  425. * Applies an optional rotation and an optional perspective warp to simulate a distorted marker.
  426. * Inverts a square subregion inside selected cells of a marker image to simulate a drop in confidence.
  427. * Computes the ground-truth confidence as one minus the ratio of inverted area to the total marker area used for identification.
  428. *
  429. */
  430. markerDetectionGT generateTemperedMarkerImage(Mat &marker, const MarkerCreationConfig &markerConfig, const MarkerTemperingConfig &markerTemperingConfig,
  431. const aruco::DetectorParameters &params, const aruco::Dictionary &dictionary, const float distortionRatio = 0.f)
  432. {
  433. // generate the synthetic marker image
  434. aruco::generateImageMarker(dictionary, markerConfig.id, markerConfig.markerSidePixels,
  435. marker, params.markerBorderBits);
  436. // rotate marker if necessary
  437. rotateMarker(marker, markerConfig.rotation);
  438. // temper with cells to simulate detection confidence drops
  439. markerDetectionGT groundTruth = applyTemperingToMarkerCells(marker, markerConfig.markerSidePixels, markerConfig.id, params, dictionary, markerTemperingConfig);
  440. // apply a distortion (a perspective warp) to simulate a non-ideal capture
  441. distortMarker(marker, distortionRatio);
  442. return groundTruth;
  443. }
  444. /**
  445. * @brief Copies a marker image into a larger image at the given top-left position.
  446. */
  447. void placeMarker(Mat &img, const Mat &marker, const Point2f &topLeft)
  448. {
  449. Rect roi(Point(static_cast<int>(topLeft.x), static_cast<int>(topLeft.y)), marker.size());
  450. marker.copyTo(img(roi));
  451. }
  452. /**
  453. * @brief Test the marker confidence computations
  454. *
  455. * Loops over a set of detector configurations (e.g. expected confidence, distortion, DetectorParameters)
  456. * For each configuration, it creates a synthetic image containing four markers arranged in a 2x2 grid.
  457. * Each marker is generated with its own configuration (id, size, rotation).
  458. * Finally, it runs the detector and checks that each marker is detected and
  459. * that its computed confidence is close to the ground truth value.
  460. *
  461. */
  462. static void runArucoDetectionConfidence(ArucoAlgParams arucoAlgParam) {
  463. aruco::DetectorParameters params;
  464. // make sure there are no bits have any detection errors
  465. params.maxErroneousBitsInBorderRate = 0.0;
  466. params.errorCorrectionRate = 0.0;
  467. params.perspectiveRemovePixelPerCell = 8; // esnsure that there is enough resolution to properly handle distortions
  468. aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params);
  469. const bool detectInvertedMarker = (arucoAlgParam == ArucoAlgParams::DETECT_INVERTED_MARKER);
  470. // define several detector configurations to test different settings
  471. // {{MarkerTemperingConfig}, perspectiveRemoveIgnoredMarginPerCell, markerBorderBits, distortionRatio}
  472. vector<ArucoConfidenceTestConfig> detectorConfigs = {
  473. // No margins, No distortion
  474. {{0.f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.f},
  475. {{0.01f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.f},
  476. {{0.05f, 100, MarkerRegionToTemper::ALL}, 0.0f, 2, 0.f},
  477. {{0.1f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.f},
  478. {{0.15f, 30, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.f},
  479. {{0.20f, 55, MarkerRegionToTemper::ALL}, 0.0f, 2, 0.f},
  480. // Margins, No distortion
  481. {{0.f, 26, MarkerRegionToTemper::BORDER}, 0.05f, 1, 0.f},
  482. {{0.01f, 56, MarkerRegionToTemper::BORDER}, 0.05f, 2, 0.f},
  483. {{0.05f, 144, MarkerRegionToTemper::ALL}, 0.1f, 3, 0.f},
  484. {{0.10f, 49, MarkerRegionToTemper::ALL}, 0.15f, 1, 0.f},
  485. // No margins, distortion
  486. {{0.f, 36, MarkerRegionToTemper::INNER}, 0.0f, 1, 0.01f},
  487. {{0.01f, 36, MarkerRegionToTemper::INNER}, 0.0f, 1, 0.02f},
  488. {{0.05f, 12, MarkerRegionToTemper::INNER}, 0.0f, 2, 0.05f},
  489. {{0.1f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.1f},
  490. {{0.1f, 81, MarkerRegionToTemper::ALL}, 0.0f, 2, 0.2f},
  491. // Margins, distortion
  492. {{0.f, 81, MarkerRegionToTemper::ALL}, 0.05f, 2, 0.01f},
  493. {{0.01f, 64, MarkerRegionToTemper::ALL}, 0.05f, 1, 0.02f},
  494. {{0.05f, 81, MarkerRegionToTemper::ALL}, 0.1f, 2, 0.05f},
  495. {{0.1f, 64, MarkerRegionToTemper::ALL}, 0.15f, 1, 0.1f},
  496. {{0.1f, 64, MarkerRegionToTemper::ALL}, 0.0f, 1, 0.2f},
  497. // no marker detection, too much tempering
  498. {{0.9f, 1, MarkerRegionToTemper::ALL}, 0.05f, 2, 0.0f},
  499. {{0.9f, 1, MarkerRegionToTemper::BORDER}, 0.05f, 2, 0.0f},
  500. {{0.9f, 1, MarkerRegionToTemper::INNER}, 0.05f, 2, 0.0f},
  501. };
  502. // define marker configurations for the 4 markers in each image
  503. const int markerSidePixels = 480; // To simplify the cell division, markerSidePixels is a multiple of 8. (6x6 dict + 2 border bits)
  504. vector<MarkerCreationConfig> markerCreationConfig = {
  505. {0, markerSidePixels, markerRot::ROT_90}, // {id, markerSidePixels, rotation}
  506. {1, markerSidePixels, markerRot::ROT_270},
  507. {2, markerSidePixels, markerRot::NONE},
  508. {3, markerSidePixels, markerRot::ROT_180}
  509. };
  510. // loop over each detector configuration
  511. for (size_t cfgIdx = 0; cfgIdx < detectorConfigs.size(); cfgIdx++) {
  512. ArucoConfidenceTestConfig detCfg = detectorConfigs[cfgIdx];
  513. SCOPED_TRACE(cv::format("detectorConfig=%zu", cfgIdx));
  514. // update detector parameters
  515. params.perspectiveRemoveIgnoredMarginPerCell = detCfg.perspectiveRemoveIgnoredMarginPerCell;
  516. params.markerBorderBits = detCfg.markerBorderBits;
  517. params.detectInvertedMarker = detectInvertedMarker;
  518. detector.setDetectorParameters(params);
  519. // create a blank image large enough to hold 4 markers in a 2x2 grid
  520. const int margin = markerSidePixels / 2;
  521. const int imageSize = (markerSidePixels * 2) + margin * 3;
  522. Mat img(imageSize, imageSize, CV_8UC1, Scalar(255));
  523. vector<markerDetectionGT> groundTruths;
  524. const aruco::Dictionary &dictionary = detector.getDictionary();
  525. // place each marker into the image
  526. for (int row = 0; row < 2; row++) {
  527. for (int col = 0; col < 2; col++) {
  528. int index = row * 2 + col;
  529. MarkerCreationConfig markerCfg = markerCreationConfig[index];
  530. // adjust marker id to be unique for each detector configuration
  531. markerCfg.id += static_cast<int>(cfgIdx * markerCreationConfig.size());
  532. // generate img
  533. Mat markerImg;
  534. markerDetectionGT gt = generateTemperedMarkerImage(markerImg, markerCfg, detCfg.markerTemperingConfig, params, dictionary, detCfg.distortionRatio);
  535. groundTruths.push_back(gt);
  536. // place marker in the image
  537. Point2f topLeft(static_cast<float>(margin + col * (markerSidePixels + margin)),
  538. static_cast<float>(margin + row * (markerSidePixels + margin)));
  539. placeMarker(img, markerImg, topLeft);
  540. }
  541. }
  542. // if testing inverted markers globally, invert the whole image
  543. if (detectInvertedMarker) {
  544. bitwise_not(img, img);
  545. }
  546. // run detection.
  547. vector<vector<Point2f>> corners, rejected;
  548. vector<int> ids;
  549. vector<float> markerConfidence;
  550. detector.detectMarkersWithConfidence(img, corners, ids, markerConfidence, rejected);
  551. ASSERT_EQ(ids.size(), corners.size());
  552. ASSERT_EQ(ids.size(), markerConfidence.size());
  553. std::map<int, float> confidenceById;
  554. for (size_t i = 0; i < ids.size(); i++) {
  555. confidenceById[ids[i]] = markerConfidence[i];
  556. }
  557. // verify that every marker is detected and its confidence is within tolerance
  558. for (const auto& currentGT : groundTruths) {
  559. const bool detected = confidenceById.find(currentGT.id) != confidenceById.end();
  560. EXPECT_EQ(currentGT.expectDetection, detected) << "Marker id: " << currentGT.id;
  561. if (currentGT.expectDetection && detected) {
  562. EXPECT_NEAR(currentGT.confidence, confidenceById[currentGT.id], 0.05)
  563. << "Marker id: " << currentGT.id;
  564. }
  565. }
  566. }
  567. }
  568. /**
  569. * @brief Check max and min size in marker detection parameters
  570. */
  571. class CV_ArucoDetectionMarkerSize : public cvtest::BaseTest {
  572. public:
  573. CV_ArucoDetectionMarkerSize();
  574. protected:
  575. void run(int);
  576. };
  577. CV_ArucoDetectionMarkerSize::CV_ArucoDetectionMarkerSize() {}
  578. void CV_ArucoDetectionMarkerSize::run(int) {
  579. aruco::DetectorParameters params;
  580. aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params);
  581. int markerSide = 20;
  582. int imageSize = 200;
  583. // 10 cases
  584. for(int i = 0; i < 10; i++) {
  585. Mat marker;
  586. int id = 10 + i * 20;
  587. // create synthetic image
  588. Mat img = Mat(imageSize, imageSize, CV_8UC1, Scalar::all(255));
  589. aruco::generateImageMarker(detector.getDictionary(), id, markerSide, marker);
  590. Mat aux = img.colRange(30, 30 + markerSide).rowRange(50, 50 + markerSide);
  591. marker.copyTo(aux);
  592. vector<vector<Point2f> > corners;
  593. vector<int> ids;
  594. // set a invalid minMarkerPerimeterRate
  595. aruco::DetectorParameters detectorParameters = params;
  596. detectorParameters.minMarkerPerimeterRate = min(4., (4. * markerSide) / float(imageSize) + 0.1);
  597. detector.setDetectorParameters(detectorParameters);
  598. detector.detectMarkers(img, corners, ids);
  599. if(corners.size() != 0) {
  600. ts->printf(cvtest::TS::LOG, "Error in DetectorParameters::minMarkerPerimeterRate");
  601. ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
  602. return;
  603. }
  604. // set an valid minMarkerPerimeterRate
  605. detectorParameters = params;
  606. detectorParameters.minMarkerPerimeterRate = max(0., (4. * markerSide) / float(imageSize) - 0.1);
  607. detector.setDetectorParameters(detectorParameters);
  608. detector.detectMarkers(img, corners, ids);
  609. if(corners.size() != 1 || (corners.size() == 1 && ids[0] != id)) {
  610. ts->printf(cvtest::TS::LOG, "Error in DetectorParameters::minMarkerPerimeterRate");
  611. ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
  612. return;
  613. }
  614. // set a invalid maxMarkerPerimeterRate
  615. detectorParameters = params;
  616. detectorParameters.maxMarkerPerimeterRate = min(4., (4. * markerSide) / float(imageSize) - 0.1);
  617. detector.setDetectorParameters(detectorParameters);
  618. detector.detectMarkers(img, corners, ids);
  619. if(corners.size() != 0) {
  620. ts->printf(cvtest::TS::LOG, "Error in DetectorParameters::maxMarkerPerimeterRate");
  621. ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
  622. return;
  623. }
  624. // set an valid maxMarkerPerimeterRate
  625. detectorParameters = params;
  626. detectorParameters.maxMarkerPerimeterRate = max(0., (4. * markerSide) / float(imageSize) + 0.1);
  627. detector.setDetectorParameters(detectorParameters);
  628. detector.detectMarkers(img, corners, ids);
  629. if(corners.size() != 1 || (corners.size() == 1 && ids[0] != id)) {
  630. ts->printf(cvtest::TS::LOG, "Error in DetectorParameters::maxMarkerPerimeterRate");
  631. ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
  632. return;
  633. }
  634. }
  635. }
  636. /**
  637. * @brief Check error correction in marker bits
  638. */
  639. class CV_ArucoBitCorrection : public cvtest::BaseTest {
  640. public:
  641. CV_ArucoBitCorrection();
  642. protected:
  643. void run(int);
  644. };
  645. CV_ArucoBitCorrection::CV_ArucoBitCorrection() {}
  646. void CV_ArucoBitCorrection::run(int) {
  647. aruco::Dictionary dictionary1 = aruco::getPredefinedDictionary(aruco::DICT_6X6_250);
  648. aruco::Dictionary dictionary2 = aruco::getPredefinedDictionary(aruco::DICT_6X6_250);
  649. aruco::DetectorParameters params;
  650. aruco::ArucoDetector detector1(dictionary1, params);
  651. int markerSide = 50;
  652. int imageSize = 150;
  653. // 10 markers
  654. for(int l = 0; l < 10; l++) {
  655. Mat marker;
  656. int id = 10 + l * 20;
  657. Mat currentCodeBytes = dictionary1.bytesList.rowRange(id, id + 1);
  658. aruco::DetectorParameters detectorParameters = detector1.getDetectorParameters();
  659. // 5 valid cases
  660. for(int i = 0; i < 5; i++) {
  661. // how many bit errors (the error is low enough so it can be corrected)
  662. detectorParameters.errorCorrectionRate = 0.2 + i * 0.1;
  663. detector1.setDetectorParameters(detectorParameters);
  664. int errors =
  665. (int)std::floor(dictionary1.maxCorrectionBits * detector1.getDetectorParameters().errorCorrectionRate - 1.);
  666. // create erroneous marker in currentCodeBits
  667. Mat currentCodeBits =
  668. aruco::Dictionary::getBitsFromByteList(currentCodeBytes, dictionary1.markerSize);
  669. for(int e = 0; e < errors; e++) {
  670. currentCodeBits.ptr<unsigned char>()[2 * e] =
  671. !currentCodeBits.ptr<unsigned char>()[2 * e];
  672. }
  673. // add erroneous marker to dictionary2 in order to create the erroneous marker image
  674. Mat currentCodeBytesError = aruco::Dictionary::getByteListFromBits(currentCodeBits);
  675. currentCodeBytesError.copyTo(dictionary2.bytesList.rowRange(id, id + 1));
  676. Mat img = Mat(imageSize, imageSize, CV_8UC1, Scalar::all(255));
  677. dictionary2.generateImageMarker(id, markerSide, marker);
  678. Mat aux = img.colRange(30, 30 + markerSide).rowRange(50, 50 + markerSide);
  679. marker.copyTo(aux);
  680. // try to detect using original dictionary
  681. vector<vector<Point2f> > corners;
  682. vector<int> ids;
  683. detector1.detectMarkers(img, corners, ids);
  684. if(corners.size() != 1 || (corners.size() == 1 && ids[0] != id)) {
  685. ts->printf(cvtest::TS::LOG, "Error in bit correction");
  686. ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
  687. return;
  688. }
  689. }
  690. // 5 invalid cases
  691. for(int i = 0; i < 5; i++) {
  692. // how many bit errors (the error is too high to be corrected)
  693. detectorParameters.errorCorrectionRate = 0.2 + i * 0.1;
  694. detector1.setDetectorParameters(detectorParameters);
  695. int errors =
  696. (int)std::floor(dictionary1.maxCorrectionBits * detector1.getDetectorParameters().errorCorrectionRate + 1.);
  697. // create erroneous marker in currentCodeBits
  698. Mat currentCodeBits =
  699. aruco::Dictionary::getBitsFromByteList(currentCodeBytes, dictionary1.markerSize);
  700. for(int e = 0; e < errors; e++) {
  701. currentCodeBits.ptr<unsigned char>()[2 * e] =
  702. !currentCodeBits.ptr<unsigned char>()[2 * e];
  703. }
  704. // dictionary3 is only composed by the modified marker (in its original form)
  705. aruco::Dictionary _dictionary3 = aruco::Dictionary(
  706. dictionary2.bytesList.rowRange(id, id + 1).clone(),
  707. dictionary1.markerSize,
  708. dictionary1.maxCorrectionBits);
  709. aruco::ArucoDetector detector3(_dictionary3, detector1.getDetectorParameters());
  710. // add erroneous marker to dictionary2 in order to create the erroneous marker image
  711. Mat currentCodeBytesError = aruco::Dictionary::getByteListFromBits(currentCodeBits);
  712. currentCodeBytesError.copyTo(dictionary2.bytesList.rowRange(id, id + 1));
  713. Mat img = Mat(imageSize, imageSize, CV_8UC1, Scalar::all(255));
  714. dictionary2.generateImageMarker(id, markerSide, marker);
  715. Mat aux = img.colRange(30, 30 + markerSide).rowRange(50, 50 + markerSide);
  716. marker.copyTo(aux);
  717. // try to detect using dictionary3, it should fail
  718. vector<vector<Point2f> > corners;
  719. vector<int> ids;
  720. detector3.detectMarkers(img, corners, ids);
  721. if(corners.size() != 0) {
  722. ts->printf(cvtest::TS::LOG, "Error in DetectorParameters::errorCorrectionRate");
  723. ts->set_failed_test_info(cvtest::TS::FAIL_BAD_ACCURACY);
  724. return;
  725. }
  726. }
  727. }
  728. }
  729. typedef CV_ArucoDetectionPerspective CV_AprilTagDetectionPerspective;
  730. typedef CV_ArucoDetectionPerspective CV_InvertedArucoDetectionPerspective;
  731. typedef CV_ArucoDetectionPerspective CV_Aruco3DetectionPerspective;
  732. TEST(CV_InvertedArucoDetectionPerspective, algorithmic) {
  733. CV_InvertedArucoDetectionPerspective test(ArucoAlgParams::DETECT_INVERTED_MARKER);
  734. test.safe_run();
  735. }
  736. TEST(CV_AprilTagDetectionPerspective, algorithmic) {
  737. CV_AprilTagDetectionPerspective test(ArucoAlgParams::USE_APRILTAG);
  738. test.safe_run();
  739. }
  740. TEST(CV_Aruco3DetectionPerspective, algorithmic) {
  741. CV_Aruco3DetectionPerspective test(ArucoAlgParams::USE_ARUCO3);
  742. test.safe_run();
  743. }
  744. TEST(CV_ArucoDetectionSimple, algorithmic) {
  745. CV_ArucoDetectionSimple test;
  746. test.safe_run();
  747. }
  748. TEST(CV_ArucoDetectionPerspective, algorithmic) {
  749. CV_ArucoDetectionPerspective test(ArucoAlgParams::USE_DEFAULT);
  750. test.safe_run();
  751. }
  752. TEST(CV_ArucoDetectionMarkerSize, algorithmic) {
  753. CV_ArucoDetectionMarkerSize test;
  754. test.safe_run();
  755. }
  756. TEST(CV_ArucoBitCorrection, algorithmic) {
  757. CV_ArucoBitCorrection test;
  758. test.safe_run();
  759. }
  760. TEST(CV_ArucoDetectionConfidence, algorithmic) {
  761. runArucoDetectionConfidence(ArucoAlgParams::USE_DEFAULT);
  762. }
  763. TEST(CV_InvertedArucoDetectionConfidence, algorithmic) {
  764. runArucoDetectionConfidence(ArucoAlgParams::DETECT_INVERTED_MARKER);
  765. }
  766. TEST(CV_InvertedFlagArucoDetectionConfidence, algorithmic) {
  767. aruco::DetectorParameters params;
  768. params.maxErroneousBitsInBorderRate = 0.0;
  769. params.errorCorrectionRate = 0.0;
  770. params.perspectiveRemovePixelPerCell = 8;
  771. params.detectInvertedMarker = false;
  772. const aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::DICT_6X6_250);
  773. // create a blank image large enough to hold 4 markers in a 2x2 grid
  774. const int markerSidePixels = 480;
  775. const int margin = markerSidePixels / 2;
  776. const int imageSize = (markerSidePixels * 2) + margin * 3;
  777. Mat img(imageSize, imageSize, CV_8UC1, Scalar(255));
  778. // place 4 markers into the image
  779. for (int row = 0; row < 2; row++) {
  780. for (int col = 0; col < 2; col++) {
  781. const int id = row * 2 + col;
  782. Mat markerImg;
  783. aruco::generateImageMarker(dictionary, id, markerSidePixels, markerImg, params.markerBorderBits);
  784. Point2f topLeft(static_cast<float>(margin + col * (markerSidePixels + margin)),
  785. static_cast<float>(margin + row * (markerSidePixels + margin)));
  786. placeMarker(img, markerImg, topLeft);
  787. }
  788. }
  789. // run detection with detectInvertedMarker = false (baseline)
  790. aruco::ArucoDetector detector(dictionary, params);
  791. vector<vector<Point2f>> corners, rejected;
  792. vector<int> ids;
  793. vector<float> confidenceDefault;
  794. detector.detectMarkersWithConfidence(img, corners, ids, confidenceDefault, rejected);
  795. ASSERT_EQ(ids.size(), corners.size());
  796. ASSERT_EQ(ids.size(), confidenceDefault.size());
  797. std::map<int, float> confidenceByIdDefault;
  798. for (size_t i = 0; i < ids.size(); i++) {
  799. confidenceByIdDefault[ids[i]] = confidenceDefault[i];
  800. }
  801. // run detection with detectInvertedMarker = true, without inverting the image
  802. params.detectInvertedMarker = true;
  803. aruco::ArucoDetector detectorInvertedFlag(dictionary, params);
  804. vector<float> confidenceInvertedFlag;
  805. detectorInvertedFlag.detectMarkersWithConfidence(img, corners, ids, confidenceInvertedFlag, rejected);
  806. ASSERT_EQ(ids.size(), corners.size());
  807. ASSERT_EQ(ids.size(), confidenceInvertedFlag.size());
  808. std::map<int, float> confidenceByIdInvertedFlag;
  809. for (size_t i = 0; i < ids.size(); i++) {
  810. confidenceByIdInvertedFlag[ids[i]] = confidenceInvertedFlag[i];
  811. }
  812. // detectInvertedMarker should not invert/flip confidence for non-inverted markers.
  813. for (int id = 0; id < 4; id++) {
  814. ASSERT_NE(confidenceByIdDefault.find(id), confidenceByIdDefault.end()) << "Marker id: " << id;
  815. ASSERT_NE(confidenceByIdInvertedFlag.find(id), confidenceByIdInvertedFlag.end()) << "Marker id: " << id;
  816. const float confDefault = confidenceByIdDefault[id];
  817. const float confInvertedFlag = confidenceByIdInvertedFlag[id];
  818. EXPECT_GT(confDefault, 0.8f) << "Marker id: " << id;
  819. EXPECT_GT(confInvertedFlag, 0.8f) << "Marker id: " << id;
  820. EXPECT_NEAR(confDefault, confInvertedFlag, 0.2f) << "Marker id: " << id;
  821. }
  822. }
  823. TEST(CV_ArucoDetectMarkers, regression_3192)
  824. {
  825. aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_4X4_50));
  826. vector<int> markerIds;
  827. vector<vector<Point2f> > markerCorners;
  828. string imgPath = cvtest::findDataFile("aruco/regression_3192.png");
  829. Mat image = imread(imgPath);
  830. const size_t N = 2ull;
  831. const int goldCorners[N][8] = { {345,120, 520,120, 520,295, 345,295}, {101,114, 270,112, 276,287, 101,287} };
  832. const int goldCornersIds[N] = { 6, 4 };
  833. map<int, const int*> mapGoldCorners;
  834. for (size_t i = 0; i < N; i++)
  835. mapGoldCorners[goldCornersIds[i]] = goldCorners[i];
  836. detector.detectMarkers(image, markerCorners, markerIds);
  837. ASSERT_EQ(N, markerIds.size());
  838. for (size_t i = 0; i < N; i++)
  839. {
  840. int arucoId = markerIds[i];
  841. ASSERT_EQ(4ull, markerCorners[i].size());
  842. ASSERT_TRUE(mapGoldCorners.find(arucoId) != mapGoldCorners.end());
  843. for (int j = 0; j < 4; j++)
  844. {
  845. EXPECT_NEAR(static_cast<float>(mapGoldCorners[arucoId][j * 2]), markerCorners[i][j].x, 1.f);
  846. EXPECT_NEAR(static_cast<float>(mapGoldCorners[arucoId][j * 2 + 1]), markerCorners[i][j].y, 1.f);
  847. }
  848. }
  849. }
  850. TEST(CV_ArucoDetectMarkers, regression_2492)
  851. {
  852. aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_5X5_50));
  853. aruco::DetectorParameters detectorParameters = detector.getDetectorParameters();
  854. detectorParameters.minMarkerDistanceRate = 0.026;
  855. detector.setDetectorParameters(detectorParameters);
  856. vector<int> markerIds;
  857. vector<vector<Point2f> > markerCorners;
  858. string imgPath = cvtest::findDataFile("aruco/regression_2492.png");
  859. Mat image = imread(imgPath);
  860. const size_t N = 8ull;
  861. const int goldCorners[N][8] = { {179,139, 179,95, 223,95, 223,139}, {99,139, 99,95, 143,95, 143,139},
  862. {19,139, 19,95, 63,95, 63,139}, {256,140, 256,93, 303,93, 303,140},
  863. {256,62, 259,21, 300,23, 297,64}, {99,21, 143,17, 147,60, 103,64},
  864. {69,61, 28,61, 14,21, 58,17}, {174,62, 182,13, 230,19, 223,68} };
  865. const int goldCornersIds[N] = {13, 13, 13, 13, 1, 15, 14, 4};
  866. map<int, vector<const int*> > mapGoldCorners;
  867. for (size_t i = 0; i < N; i++)
  868. mapGoldCorners[goldCornersIds[i]].push_back(goldCorners[i]);
  869. detector.detectMarkers(image, markerCorners, markerIds);
  870. ASSERT_EQ(N, markerIds.size());
  871. for (size_t i = 0; i < N; i++)
  872. {
  873. int arucoId = markerIds[i];
  874. ASSERT_EQ(4ull, markerCorners[i].size());
  875. ASSERT_TRUE(mapGoldCorners.find(arucoId) != mapGoldCorners.end());
  876. float totalDist = 8.f;
  877. for (size_t k = 0ull; k < mapGoldCorners[arucoId].size(); k++)
  878. {
  879. float dist = 0.f;
  880. for (int j = 0; j < 4; j++) // total distance up to 4 points
  881. {
  882. dist += abs(mapGoldCorners[arucoId][k][j * 2] - markerCorners[i][j].x);
  883. dist += abs(mapGoldCorners[arucoId][k][j * 2 + 1] - markerCorners[i][j].y);
  884. }
  885. totalDist = min(totalDist, dist);
  886. }
  887. EXPECT_LT(totalDist, 8.f);
  888. }
  889. }
  890. TEST(CV_ArucoDetectMarkers, regression_contour_24220)
  891. {
  892. aruco::ArucoDetector detector;
  893. vector<int> markerIds;
  894. vector<vector<Point2f> > markerCorners;
  895. string imgPath = cvtest::findDataFile("aruco/failmask9.png");
  896. Mat image = imread(imgPath);
  897. const size_t N = 1ull;
  898. const int goldCorners[8] = {392,175, 99,257, 117,109, 365,44};
  899. const int goldCornersId = 0;
  900. detector.detectMarkers(image, markerCorners, markerIds);
  901. ASSERT_EQ(N, markerIds.size());
  902. ASSERT_EQ(4ull, markerCorners[0].size());
  903. ASSERT_EQ(goldCornersId, markerIds[0]);
  904. for (int j = 0; j < 4; j++)
  905. {
  906. EXPECT_NEAR(static_cast<float>(goldCorners[j * 2]), markerCorners[0][j].x, 1.f);
  907. EXPECT_NEAR(static_cast<float>(goldCorners[j * 2 + 1]), markerCorners[0][j].y, 1.f);
  908. }
  909. }
  910. TEST(CV_ArucoDetectMarkers, regression_26922)
  911. {
  912. const auto arucoDict = aruco::getPredefinedDictionary(aruco::DICT_4X4_1000);
  913. const aruco::GridBoard gridBoard(Size(19, 10), 1, 0.25, arucoDict);
  914. const Size imageSize(7200, 3825);
  915. Mat boardImage;
  916. gridBoard.generateImage(imageSize, boardImage, 75, 1);
  917. const aruco::ArucoDetector detector(arucoDict);
  918. vector<vector<Point2f>> corners;
  919. vector<int> ids;
  920. detector.detectMarkers(boardImage, corners, ids);
  921. EXPECT_EQ(ids.size(), 190ull);
  922. EXPECT_TRUE(find(ids.begin(), ids.end(), 76) != ids.end());
  923. EXPECT_TRUE(find(ids.begin(), ids.end(), 172) != ids.end());
  924. float transformMatrixData[9] = {1, -0.2f, 300, 0.4f, 1, -1000, 0, 0, 1};
  925. const Mat transformMatrix(Size(3, 3), CV_32FC1, transformMatrixData);
  926. Mat warpedImage;
  927. warpPerspective(boardImage, warpedImage, transformMatrix, imageSize);
  928. detector.detectMarkers(warpedImage, corners, ids);
  929. EXPECT_EQ(ids.size(), 133ull);
  930. // markers with id 76 and 172 are on border and should not be detected
  931. EXPECT_FALSE(find(ids.begin(), ids.end(), 76) != ids.end());
  932. EXPECT_FALSE(find(ids.begin(), ids.end(), 172) != ids.end());
  933. }
  934. TEST(CV_ArucoMultiDict, setGetDictionaries)
  935. {
  936. vector<aruco::Dictionary> dictionaries = {aruco::getPredefinedDictionary(aruco::DICT_4X4_50), aruco::getPredefinedDictionary(aruco::DICT_5X5_100)};
  937. aruco::ArucoDetector detector(dictionaries);
  938. vector<aruco::Dictionary> dicts = detector.getDictionaries();
  939. ASSERT_EQ(dicts.size(), 2ul);
  940. EXPECT_EQ(dicts[0].markerSize, 4);
  941. EXPECT_EQ(dicts[1].markerSize, 5);
  942. dictionaries.clear();
  943. dictionaries.push_back(aruco::getPredefinedDictionary(aruco::DICT_6X6_100));
  944. dictionaries.push_back(aruco::getPredefinedDictionary(aruco::DICT_7X7_250));
  945. dictionaries.push_back(aruco::getPredefinedDictionary(aruco::DICT_APRILTAG_25h9));
  946. detector.setDictionaries(dictionaries);
  947. dicts = detector.getDictionaries();
  948. ASSERT_EQ(dicts.size(), 3ul);
  949. EXPECT_EQ(dicts[0].markerSize, 6);
  950. EXPECT_EQ(dicts[1].markerSize, 7);
  951. EXPECT_EQ(dicts[2].markerSize, 5);
  952. auto dict = detector.getDictionary();
  953. EXPECT_EQ(dict.markerSize, 6);
  954. detector.setDictionary(aruco::getPredefinedDictionary(aruco::DICT_APRILTAG_16h5));
  955. dicts = detector.getDictionaries();
  956. ASSERT_EQ(dicts.size(), 3ul);
  957. EXPECT_EQ(dicts[0].markerSize, 4);
  958. EXPECT_EQ(dicts[1].markerSize, 7);
  959. EXPECT_EQ(dicts[2].markerSize, 5);
  960. }
  961. TEST(CV_ArucoMultiDict, noDict)
  962. {
  963. aruco::ArucoDetector detector;
  964. EXPECT_THROW({
  965. detector.setDictionaries({});
  966. }, Exception);
  967. }
  968. TEST(CV_ArucoMultiDict, multiMarkerDetection)
  969. {
  970. const int markerSidePixels = 100;
  971. const int imageSize = markerSidePixels * 2 + 3 * (markerSidePixels / 2);
  972. vector<aruco::Dictionary> usedDictionaries;
  973. // draw synthetic image
  974. Mat img = Mat(imageSize, imageSize, CV_8UC1, Scalar::all(255));
  975. for(int y = 0; y < 2; y++) {
  976. for(int x = 0; x < 2; x++) {
  977. Mat marker;
  978. int id = y * 2 + x;
  979. int dictId = x * 4 + y * 8;
  980. auto dict = aruco::getPredefinedDictionary(dictId);
  981. usedDictionaries.push_back(dict);
  982. aruco::generateImageMarker(dict, id, markerSidePixels, marker);
  983. Point2f firstCorner(markerSidePixels / 2.f + x * (1.5f * markerSidePixels),
  984. markerSidePixels / 2.f + y * (1.5f * markerSidePixels));
  985. Mat aux = img(Rect((int)firstCorner.x, (int)firstCorner.y, markerSidePixels, markerSidePixels));
  986. marker.copyTo(aux);
  987. }
  988. }
  989. img.convertTo(img, CV_8UC3);
  990. aruco::ArucoDetector detector(usedDictionaries);
  991. vector<vector<Point2f> > markerCorners;
  992. vector<int> markerIds;
  993. vector<vector<Point2f> > rejectedImgPts;
  994. vector<int> dictIds;
  995. detector.detectMarkersMultiDict(img, markerCorners, markerIds, rejectedImgPts, dictIds);
  996. ASSERT_EQ(markerIds.size(), 4u);
  997. ASSERT_EQ(dictIds.size(), 4u);
  998. for (size_t i = 0; i < dictIds.size(); ++i) {
  999. EXPECT_EQ(dictIds[i], (int)i);
  1000. }
  1001. }
  1002. TEST(CV_ArucoMultiDict, multiMarkerDoubleDetection)
  1003. {
  1004. const int markerSidePixels = 100;
  1005. const int imageWidth = 2 * markerSidePixels + 3 * (markerSidePixels / 2);
  1006. const int imageHeight = markerSidePixels + 2 * (markerSidePixels / 2);
  1007. vector<aruco::Dictionary> usedDictionaries = {
  1008. aruco::getPredefinedDictionary(aruco::DICT_5X5_50),
  1009. aruco::getPredefinedDictionary(aruco::DICT_5X5_100)
  1010. };
  1011. // draw synthetic image
  1012. Mat img = Mat(imageHeight, imageWidth, CV_8UC1, Scalar::all(255));
  1013. for(int y = 0; y < 2; y++) {
  1014. Mat marker;
  1015. int id = 49 + y;
  1016. auto dict = aruco::getPredefinedDictionary(aruco::DICT_5X5_100);
  1017. aruco::generateImageMarker(dict, id, markerSidePixels, marker);
  1018. Point2f firstCorner(markerSidePixels / 2.f + y * (1.5f * markerSidePixels),
  1019. markerSidePixels / 2.f);
  1020. Mat aux = img(Rect((int)firstCorner.x, (int)firstCorner.y, markerSidePixels, markerSidePixels));
  1021. marker.copyTo(aux);
  1022. }
  1023. img.convertTo(img, CV_8UC3);
  1024. aruco::ArucoDetector detector(usedDictionaries);
  1025. vector<vector<Point2f> > markerCorners;
  1026. vector<int> markerIds;
  1027. vector<vector<Point2f> > rejectedImgPts;
  1028. vector<int> dictIds;
  1029. detector.detectMarkersMultiDict(img, markerCorners, markerIds, rejectedImgPts, dictIds);
  1030. ASSERT_EQ(markerIds.size(), 3u);
  1031. ASSERT_EQ(dictIds.size(), 3u);
  1032. EXPECT_EQ(dictIds[0], 0); // 5X5_50
  1033. EXPECT_EQ(dictIds[1], 1); // 5X5_100
  1034. EXPECT_EQ(dictIds[2], 1); // 5X5_100
  1035. }
  1036. TEST(CV_ArucoMultiDict, serialization)
  1037. {
  1038. aruco::ArucoDetector detector;
  1039. {
  1040. FileStorage fs_out(".json", FileStorage::WRITE + FileStorage::MEMORY);
  1041. ASSERT_TRUE(fs_out.isOpened());
  1042. detector.write(fs_out);
  1043. std::string serialized_string = fs_out.releaseAndGetString();
  1044. FileStorage test_fs(serialized_string, FileStorage::Mode::READ + FileStorage::MEMORY);
  1045. ASSERT_TRUE(test_fs.isOpened());
  1046. aruco::ArucoDetector test_detector;
  1047. test_detector.read(test_fs.root());
  1048. // compare default constructor result
  1049. EXPECT_EQ(aruco::getPredefinedDictionary(aruco::DICT_4X4_50), test_detector.getDictionary());
  1050. }
  1051. detector.setDictionaries({aruco::getPredefinedDictionary(aruco::DICT_4X4_50), aruco::getPredefinedDictionary(aruco::DICT_5X5_100)});
  1052. {
  1053. FileStorage fs_out(".json", FileStorage::WRITE + FileStorage::MEMORY);
  1054. ASSERT_TRUE(fs_out.isOpened());
  1055. detector.write(fs_out);
  1056. std::string serialized_string = fs_out.releaseAndGetString();
  1057. FileStorage test_fs(serialized_string, FileStorage::Mode::READ + FileStorage::MEMORY);
  1058. ASSERT_TRUE(test_fs.isOpened());
  1059. aruco::ArucoDetector test_detector;
  1060. test_detector.read(test_fs.root());
  1061. // check for one additional dictionary
  1062. auto dicts = test_detector.getDictionaries();
  1063. ASSERT_EQ(2ul, dicts.size());
  1064. EXPECT_EQ(aruco::getPredefinedDictionary(aruco::DICT_4X4_50), dicts[0]);
  1065. EXPECT_EQ(aruco::getPredefinedDictionary(aruco::DICT_5X5_100), dicts[1]);
  1066. }
  1067. }
  1068. struct ArucoThreading: public testing::TestWithParam<aruco::CornerRefineMethod>
  1069. {
  1070. struct NumThreadsSetter {
  1071. NumThreadsSetter(const int num_threads)
  1072. : original_num_threads_(getNumThreads()) {
  1073. setNumThreads(num_threads);
  1074. }
  1075. ~NumThreadsSetter() {
  1076. setNumThreads(original_num_threads_);
  1077. }
  1078. private:
  1079. int original_num_threads_;
  1080. };
  1081. };
  1082. TEST_P(ArucoThreading, number_of_threads_does_not_change_results)
  1083. {
  1084. // We are not testing against different dictionaries
  1085. // As we are interested mostly in small images, smaller
  1086. // markers is better -> 4x4
  1087. aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_4X4_50));
  1088. // Height of the test image can be chosen quite freely
  1089. // We aim to test against small images as in those the
  1090. // number of threads has most effect
  1091. const int height_img = 20;
  1092. // Just to get nice white boarder
  1093. const int shift = height_img > 10 ? 5 : 1;
  1094. const int height_marker = height_img-2*shift;
  1095. // Create a test image
  1096. Mat img_marker;
  1097. aruco::generateImageMarker(detector.getDictionary(), 23, height_marker, img_marker, 1);
  1098. // Copy to bigger image to get a white border
  1099. Mat img(height_img, height_img, CV_8UC1, Scalar(255));
  1100. img_marker.copyTo(img(Rect(shift, shift, height_marker, height_marker)));
  1101. aruco::DetectorParameters detectorParameters = detector.getDetectorParameters();
  1102. detectorParameters.cornerRefinementMethod = (int)GetParam();
  1103. detector.setDetectorParameters(detectorParameters);
  1104. vector<vector<Point2f> > original_corners;
  1105. vector<int> original_ids;
  1106. {
  1107. NumThreadsSetter thread_num_setter(1);
  1108. detector.detectMarkers(img, original_corners, original_ids);
  1109. }
  1110. ASSERT_EQ(original_ids.size(), 1ull);
  1111. ASSERT_EQ(original_corners.size(), 1ull);
  1112. int num_threads_to_test[] = { 2, 8, 16, 32, height_img-1, height_img, height_img+1};
  1113. for (size_t i_num_threads = 0; i_num_threads < sizeof(num_threads_to_test)/sizeof(int); ++i_num_threads) {
  1114. NumThreadsSetter thread_num_setter(num_threads_to_test[i_num_threads]);
  1115. vector<vector<Point2f> > corners;
  1116. vector<int> ids;
  1117. detector.detectMarkers(img, corners, ids);
  1118. // If we don't find any markers, the test is broken
  1119. ASSERT_EQ(ids.size(), 1ull);
  1120. // Make sure we got the same result as the first time
  1121. ASSERT_EQ(corners.size(), original_corners.size());
  1122. ASSERT_EQ(ids.size(), original_ids.size());
  1123. ASSERT_EQ(ids.size(), corners.size());
  1124. for (size_t i = 0; i < corners.size(); ++i) {
  1125. EXPECT_EQ(ids[i], original_ids[i]);
  1126. for (size_t j = 0; j < corners[i].size(); ++j) {
  1127. EXPECT_NEAR(corners[i][j].x, original_corners[i][j].x, 0.1f);
  1128. EXPECT_NEAR(corners[i][j].y, original_corners[i][j].y, 0.1f);
  1129. }
  1130. }
  1131. }
  1132. }
  1133. INSTANTIATE_TEST_CASE_P(
  1134. CV_ArucoDetectMarkers, ArucoThreading,
  1135. ::testing::Values(
  1136. aruco::CORNER_REFINE_NONE,
  1137. aruco::CORNER_REFINE_SUBPIX,
  1138. aruco::CORNER_REFINE_CONTOUR,
  1139. aruco::CORNER_REFINE_APRILTAG
  1140. ));
  1141. }} // namespace